diff --git a/browser/ui/tabs/brave_tab_prefs.cc b/browser/ui/tabs/brave_tab_prefs.cc
index 8f90164d8e9..90dfd50122a 100644
--- a/browser/ui/tabs/brave_tab_prefs.cc
+++ b/browser/ui/tabs/brave_tab_prefs.cc
@@ -41,6 +41,7 @@ void RegisterBraveProfilePrefs(PrefRegistrySimple* registry) {
}
registry->RegisterBooleanPref(kAlwaysHideTabCloseButton, false);
+ registry->RegisterBooleanPref(kMiddleClickCloseTabEnabled, true);
}
void MigrateBraveProfilePrefs(PrefService* prefs) {
diff --git a/browser/ui/tabs/brave_tab_prefs.h b/browser/ui/tabs/brave_tab_prefs.h
index 2f443b90a75..e99d4a58e09 100644
--- a/browser/ui/tabs/brave_tab_prefs.h
+++ b/browser/ui/tabs/brave_tab_prefs.h
@@ -40,6 +40,9 @@ inline constexpr char kSharedPinnedTab[] = "brave.tabs.shared_pinned_tab";
inline constexpr char kAlwaysHideTabCloseButton[] =
"brave.tabs.always_hide_tab_close_button";
+inline constexpr char kMiddleClickCloseTabEnabled[] =
+ "brave.tabs.middle_click_close_tab_enabled";
+
void RegisterBraveProfilePrefs(PrefRegistrySimple* registry);
void MigrateBraveProfilePrefs(PrefService* prefs);
diff --git a/browser/ui/views/tabs/brave_tab_strip.cc b/browser/ui/views/tabs/brave_tab_strip.cc
index a68426b0748..16afe095220 100644
--- a/browser/ui/views/tabs/brave_tab_strip.cc
+++ b/browser/ui/views/tabs/brave_tab_strip.cc
@@ -53,6 +53,8 @@ BraveTabStrip::BraveTabStrip(std::unique_ptr controller)
controller_->GetProfile()->GetPrefs(),
base::BindRepeating(&BraveTabStrip::OnAlwaysHideCloseButtonPrefChanged,
base::Unretained(this)));
+ middle_click_close_tab_enabled_.Init(brave_tabs::kMiddleClickCloseTabEnabled,
+ controller_->GetProfile()->GetPrefs());
}
BraveTabStrip::~BraveTabStrip() = default;
@@ -101,6 +103,10 @@ bool BraveTabStrip::CanPaintThrobberToLayer() const {
return false;
}
+bool BraveTabStrip::CanCloseTabViaMiddleButtonClick() const {
+ return *middle_click_close_tab_enabled_;
+}
+
bool BraveTabStrip::ShouldDrawStrokes() const {
if (ShouldShowVerticalTabs()) {
// Prevent root view from drawing lines. For vertical tabs stroke , we
diff --git a/browser/ui/views/tabs/brave_tab_strip.h b/browser/ui/views/tabs/brave_tab_strip.h
index eecd20e020e..355deca8db5 100644
--- a/browser/ui/views/tabs/brave_tab_strip.h
+++ b/browser/ui/views/tabs/brave_tab_strip.h
@@ -43,6 +43,7 @@ class BraveTabStrip : public TabStrip {
bool ShouldAlwaysHideCloseButton() const override;
bool IsVerticalTabsFloating() const override;
bool CanPaintThrobberToLayer() const override;
+ bool CanCloseTabViaMiddleButtonClick() const override;
private:
FRIEND_TEST_ALL_PREFIXES(VerticalTabStripBrowserTest, ScrollBarVisibility);
@@ -63,6 +64,7 @@ class BraveTabStrip : public TabStrip {
static constexpr float kBraveMinimumContrastRatioForOutlines = 1.0816f;
BooleanPrefMember always_hide_close_button_;
+ BooleanPrefMember middle_click_close_tab_enabled_;
base::WeakPtrFactory weak_factory_{this};
};
diff --git a/browser/ui/views/tabs/brave_tab_unittest.cc b/browser/ui/views/tabs/brave_tab_unittest.cc
index 0305bac3402..fcd424125a5 100644
--- a/browser/ui/views/tabs/brave_tab_unittest.cc
+++ b/browser/ui/views/tabs/brave_tab_unittest.cc
@@ -115,6 +115,7 @@ class BraveTabRenamingUnitTest : public BraveTabTest {
SetCustomTitleForTab,
(Tab * tab, const std::optional& title),
(override));
+ MOCK_METHOD(void, CloseTab, (Tab * tab, CloseTabSource source), (override));
};
BraveTabRenamingUnitTest() = default;
@@ -287,3 +288,59 @@ TEST_F(BraveTabTest, ShouldAlwaysHideTabCloseButton) {
views::test::RunScheduledLayout(&tab);
EXPECT_FALSE(tab.close_button_for_test()->GetVisible());
}
+
+TEST_F(BraveTabTest, CanCloseTabViaMiddleButtonClick) {
+ testing::NiceMock
+ tab_slot_controller;
+ auto tab = std::make_unique(&tab_slot_controller);
+ tab_slot_controller.set_active_tab(tab.get());
+
+ // Create a widget to host the tab
+ auto widget =
+ CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET);
+ widget->SetContentsView(std::move(tab));
+ auto* tab_ptr = static_cast(widget->GetContentsView());
+ tab_ptr->SetBoundsRect({0, 0, 100, 50});
+ views::test::RunScheduledLayout(tab_ptr);
+
+ // Default should be enabled (true)
+ EXPECT_TRUE(tab_slot_controller.CanCloseTabViaMiddleButtonClick());
+
+ // Simulate middle mouse button click - should close the tab
+ ui::MouseEvent press_event(ui::EventType::kMousePressed, gfx::Point(50, 25),
+ gfx::Point(), base::TimeTicks(),
+ ui::EF_MIDDLE_MOUSE_BUTTON, 0);
+ ui::MouseEvent release_event(
+ ui::EventType::kMouseReleased, gfx::Point(50, 25), gfx::Point(),
+ base::TimeTicks(), ui::EF_MIDDLE_MOUSE_BUTTON, 0);
+ testing::Mock::VerifyAndClearExpectations(&tab_slot_controller);
+ EXPECT_CALL(tab_slot_controller,
+ CloseTab(tab_ptr, CloseTabSource::kFromMouse))
+ .Times(1);
+ tab_ptr->OnMousePressed(press_event);
+ tab_ptr->OnMouseReleased(release_event);
+
+ // Test disabling middle click to close
+ tab_slot_controller.set_can_close_tab_via_middle_button_click(false);
+ EXPECT_FALSE(tab_slot_controller.CanCloseTabViaMiddleButtonClick());
+
+ // Simulate middle mouse button click - should NOT close the tab
+ testing::Mock::VerifyAndClearExpectations(&tab_slot_controller);
+ EXPECT_CALL(tab_slot_controller,
+ CloseTab(tab_ptr, CloseTabSource::kFromMouse))
+ .Times(0);
+ tab_ptr->OnMousePressed(press_event);
+ tab_ptr->OnMouseReleased(release_event);
+
+ // Test re-enabling middle click to close
+ tab_slot_controller.set_can_close_tab_via_middle_button_click(true);
+ EXPECT_TRUE(tab_slot_controller.CanCloseTabViaMiddleButtonClick());
+
+ // Simulate middle mouse button click - should close the tab again
+ testing::Mock::VerifyAndClearExpectations(&tab_slot_controller);
+ EXPECT_CALL(tab_slot_controller,
+ CloseTab(tab_ptr, CloseTabSource::kFromMouse))
+ .Times(1);
+ tab_ptr->OnMousePressed(press_event);
+ tab_ptr->OnMouseReleased(release_event);
+}
diff --git a/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.cc b/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.cc
index a2e2748f26f..4295df69a39 100644
--- a/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.cc
+++ b/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.cc
@@ -13,6 +13,10 @@ bool FakeTabSlotController::ShouldAlwaysHideCloseButton() const {
return should_always_hide_close_button_;
}
+bool FakeTabSlotController::CanCloseTabViaMiddleButtonClick() const {
+ return can_close_tab_via_middle_button_click_;
+}
+
bool FakeTabSlotController::IsVerticalTabsFloating() const {
return false;
}
diff --git a/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h b/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h
index bf3edbcae41..902cd6fc445 100644
--- a/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h
+++ b/chromium_src/chrome/browser/ui/views/tabs/fake_tab_slot_controller.h
@@ -12,18 +12,23 @@
IsGroupCollapsed(__VA_ARGS__) const override; \
const Browser* GetBrowser()
-// Add override for ShouldAlwaysHideTabCloseButton() and setters to control its
-// return value in tests.
-#define ShouldCompactLeadingEdge() \
- ShouldCompactLeadingEdge() const override; \
- \
- private: \
- bool should_always_hide_close_button_ = false; \
- \
- public: \
- void set_should_always_hide_close_button(bool hide) { \
- should_always_hide_close_button_ = hide; \
- } \
+// Add override for
+// ShouldAlwaysHideTabCloseButton()/CanCloseTabViaMiddleButtonClick() and
+// setters to control its return value in tests.
+#define ShouldCompactLeadingEdge() \
+ ShouldCompactLeadingEdge() const override; \
+ \
+ private: \
+ bool should_always_hide_close_button_ = false; \
+ bool can_close_tab_via_middle_button_click_ = true; \
+ \
+ public: \
+ void set_should_always_hide_close_button(bool hide) { \
+ should_always_hide_close_button_ = hide; \
+ } \
+ void set_can_close_tab_via_middle_button_click(bool enabled) { \
+ can_close_tab_via_middle_button_click_ = enabled; \
+ } \
bool ShouldAlwaysHideCloseButton()
// Add override for IsVerticalTabsFloating()
@@ -31,8 +36,14 @@
EndDrag(__VA_ARGS__) override; \
bool IsVerticalTabsFloating() const
+// Add override for CanCloseTabViaMiddleButtonClick()
+#define CanPaintThrobberToLayer() \
+ CanPaintThrobberToLayer() const override; \
+ bool CanCloseTabViaMiddleButtonClick()
+
#include // IWYU pragma: export
+#undef CanPaintThrobberToLayer
#undef EndDrag
#undef ShouldCompactLeadingEdge
#undef IsGroupCollapsed
diff --git a/chromium_src/chrome/browser/ui/views/tabs/tab_slot_controller.h b/chromium_src/chrome/browser/ui/views/tabs/tab_slot_controller.h
index b728a0b38d1..4443a8668db 100644
--- a/chromium_src/chrome/browser/ui/views/tabs/tab_slot_controller.h
+++ b/chromium_src/chrome/browser/ui/views/tabs/tab_slot_controller.h
@@ -24,8 +24,15 @@
EndDrag(__VA_ARGS__) = 0; \
virtual bool IsVerticalTabsFloating() const
+// Add a method to TabSlotController to determine whether tabs can be closed via
+// middle mouse button click.
+#define CanPaintThrobberToLayer() \
+ CanPaintThrobberToLayer() const = 0; \
+ virtual bool CanCloseTabViaMiddleButtonClick()
+
#include // IWYU pragma: export
+#undef CanPaintThrobberToLayer
#undef EndDrag
#undef ShouldCompactLeadingEdge
#undef IsGroupCollapsed
diff --git a/chromium_src/chrome/browser/ui/views/tabs/tab_strip.cc b/chromium_src/chrome/browser/ui/views/tabs/tab_strip.cc
index d5a9e3a5f83..3286eb335fa 100644
--- a/chromium_src/chrome/browser/ui/views/tabs/tab_strip.cc
+++ b/chromium_src/chrome/browser/ui/views/tabs/tab_strip.cc
@@ -65,6 +65,10 @@ bool TabStrip::ShouldAlwaysHideCloseButton() const {
return false;
}
+bool TabStrip::CanCloseTabViaMiddleButtonClick() const {
+ return true;
+}
+
bool TabStrip::IsVerticalTabsFloating() const {
return false;
}
diff --git a/chromium_src/chrome/browser/ui/views/tabs/tab_strip.h b/chromium_src/chrome/browser/ui/views/tabs/tab_strip.h
index c0291d3cc0f..22db1e96c1a 100644
--- a/chromium_src/chrome/browser/ui/views/tabs/tab_strip.h
+++ b/chromium_src/chrome/browser/ui/views/tabs/tab_strip.h
@@ -19,8 +19,10 @@ class BraveTabHoverCardController;
friend class BraveVerticalTabStripRegionView; \
void UpdateHoverCard
-#define ShouldDrawStrokes \
- UnUsed() { return true; } \
+#define ShouldDrawStrokes \
+ UnUsed() { \
+ return true; \
+ } \
virtual bool ShouldDrawStrokes
#define GetDragContext \
Unused_GetDragContext() { \
@@ -30,6 +32,7 @@ class BraveTabHoverCardController;
friend class BraveTabDragContext; \
const Browser* GetBrowser() const override; \
bool ShouldAlwaysHideCloseButton() const override; \
+ bool CanCloseTabViaMiddleButtonClick() const override; \
bool IsVerticalTabsFloating() const override; \
static constexpr bool IsUsingBraveTabHoverCardController() { \
return std::is_same_v, \
diff --git a/patches/chrome-browser-ui-views-tabs-tab.cc.patch b/patches/chrome-browser-ui-views-tabs-tab.cc.patch
index a2ab4c560bc..66ccfd70052 100644
--- a/patches/chrome-browser-ui-views-tabs-tab.cc.patch
+++ b/patches/chrome-browser-ui-views-tabs-tab.cc.patch
@@ -1,5 +1,5 @@
diff --git a/chrome/browser/ui/views/tabs/tab.cc b/chrome/browser/ui/views/tabs/tab.cc
-index 722f2832b44b4fe57fe1738edd7ccfecc7f629ab..011d7c8a7a9efe127c35b6b23123830289948b0a 100644
+index 722f2832b44b4fe57fe1738edd7ccfecc7f629ab..129598d1a22484a9d14987213ce47bf013717a32 100644
--- a/chrome/browser/ui/views/tabs/tab.cc
+++ b/chrome/browser/ui/views/tabs/tab.cc
@@ -376,6 +376,7 @@ void Tab::Layout(PassKey) {
@@ -18,3 +18,12 @@ index 722f2832b44b4fe57fe1738edd7ccfecc7f629ab..011d7c8a7a9efe127c35b6b231238302
} else if (showing_close_button_) {
// Allow the title to overlay the close button's empty border padding.
title_right = close_x - after_title_padding;
+@@ -621,7 +623,7 @@ void Tab::OnMouseReleased(const ui::MouseEvent& event) {
+ // Close tab on middle click, but only if the button is released over the tab
+ // (normal windows behavior is to discard presses of a UI element where the
+ // releases happen off the element).
+- if (event.IsOnlyMiddleMouseButton()) {
++ if (event.IsOnlyMiddleMouseButton() && controller_->CanCloseTabViaMiddleButtonClick()) {
+ if (HitTestPoint(event.location())) {
+ controller_->CloseTab(this, CloseTabSource::kFromMouse);
+ } else if (closing_) {
diff --git a/rewrite/chrome/browser/ui/views/tabs/tab.cc.toml b/rewrite/chrome/browser/ui/views/tabs/tab.cc.toml
new file mode 100644
index 00000000000..e6cc1638be8
--- /dev/null
+++ b/rewrite/chrome/browser/ui/views/tabs/tab.cc.toml
@@ -0,0 +1,22 @@
+# Copyright (c) 2025 The Brave Authors. All rights reserved.
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at https://mozilla.org/MPL/2.0/.
+
+[[substitution]]
+description = 'Check if tab can be closed via middle mouse button click based on prefs'
+pattern = 'if (event.IsOnlyMiddleMouseButton())'
+replace = 'if (event.IsOnlyMiddleMouseButton() && controller_->CanCloseTabViaMiddleButtonClick())'
+count = 1
+
+[[substitution]]
+description = 'Add BRAVE_UI_VIEWS_TABS_TAB_LAYOUT_ADJUST_ICON_POSITION'
+re_pattern = '(gfx::Rect favicon_bounds\(start, contents_rect\.y\(\), 0, 0\);\s+if \(showing_icon_\) \{\n)'
+replace = '\1 BRAVE_UI_VIEWS_TABS_TAB_LAYOUT_ADJUST_ICON_POSITION\n'
+count = 1
+
+[[substitution]]
+description = 'Add BRAVE_UI_VIEWS_TABS_TAB_ALERT_INDICATOR_POSITION'
+re_pattern = '(if\s*\(showing_alert_indicator_\)\s*\{\n\s*title_right\s*=\s*alert_indicator_button_->x\(\)\s*-\s*after_title_padding;)'
+replace = '\1\n BRAVE_UI_VIEWS_TABS_TAB_ALERT_INDICATOR_POSITION'
+