feat: Add "Separate from pinned tab" when resetting pinned tab with CMD, p=#12710

This commit is contained in:
Lukas
2026-03-14 10:06:09 +01:00
committed by GitHub
parent a629866c28
commit 55c079d4ba
5 changed files with 231 additions and 8 deletions

View File

@@ -46,5 +46,6 @@ tabbrowser-reset-pin-button =
zen-tab-sublabel =
{ $tabSubtitle ->
[zen-default-pinned] Back to pinned url
[zen-default-pinned-cmd] Separate from pinned tab
*[other] { $tabSubtitle }
}

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb894fa1d4 100644
index 836bee14d2b63604688ebe477a5d915a5e99b305..5f60aa3bedd4f80b887ea3e050fd86a21a6b280a 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -110,7 +110,26 @@ index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb
}
get splitview() {
@@ -489,6 +515,8 @@
@@ -444,6 +470,10 @@
: this;
gBrowser.warmupTab(tabToWarm);
+ if (event.target.classList.contains("tab-reset-pin-button")) {
+ gZenPinnedTabManager.onResetPinButtonMouseOver(this, event);
+ }
+
// If the previous target wasn't part of this tab then this is a mouseenter event.
if (!this.contains(event.relatedTarget)) {
this._mouseenter();
@@ -455,6 +485,7 @@
if (!this.contains(event.relatedTarget)) {
this._mouseleave();
}
+ gZenPinnedTabManager.onResetPinButtonMouseOut(this);
}
on_dragstart(event) {
@@ -489,6 +520,8 @@
this.style.MozUserFocus = "ignore";
} else if (
event.target.classList.contains("tab-close-button") ||
@@ -119,7 +138,7 @@ index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb
event.target.classList.contains("tab-icon-overlay") ||
event.target.classList.contains("tab-audio-button")
) {
@@ -543,6 +571,10 @@
@@ -543,16 +576,21 @@
this.style.MozUserFocus = "";
}
@@ -130,7 +149,40 @@ index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb
on_click(event) {
if (event.button != 0) {
return;
@@ -603,6 +635,14 @@
}
- if (event.getModifierState("Accel") || event.shiftKey) {
+ if (event.shiftKey) {
return;
}
if (
+ !event.getModifierState("Accel") &&
gBrowser.multiSelectedTabsCount > 0 &&
!event.target.classList.contains("tab-close-button") &&
!event.target.classList.contains("tab-icon-overlay") &&
@@ -564,8 +602,9 @@
}
if (
- event.target.classList.contains("tab-icon-overlay") ||
- event.target.classList.contains("tab-audio-button")
+ !event.getModifierState("Accel") &&
+ (event.target.classList.contains("tab-icon-overlay") ||
+ event.target.classList.contains("tab-audio-button"))
) {
if (this.activeMediaBlocked) {
if (this.multiselected) {
@@ -583,7 +622,7 @@
return;
}
- if (event.target.classList.contains("tab-close-button")) {
+ if (!event.getModifierState("Accel") && event.target.classList.contains("tab-close-button")) {
if (this.multiselected) {
gBrowser.removeMultiSelectedTabs(
lazy.TabMetrics.userTriggeredContext(
@@ -603,6 +642,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -138,14 +190,14 @@ index 836bee14d2b63604688ebe477a5d915a5e99b305..7e105a1ae07657b0a0e664a8e3d9d2eb
+ if (event.target.classList.contains("tab-reset-pin-button")) {
+ gZenPinnedTabManager._onTabResetPinButton(event, this, 'reset');
+ gBrowser.tabContainer._blockDblClick = true;
+ } else if (event.target.classList.contains("tab-reset-button")) {
+ } else if (!event.getModifierState("Accel") && event.target.classList.contains("tab-reset-button")) {
+ gZenPinnedTabManager.onCloseTabShortcut(event, this);
+ gBrowser.tabContainer._blockDblClick = true;
+ }
}
on_dblclick(event) {
@@ -626,6 +666,8 @@
@@ -626,6 +673,8 @@
animate: true,
triggeringEvent: event,
});

View File

@@ -115,7 +115,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
_onTabResetPinButton(event, tab) {
event.stopPropagation();
this._resetTabToStoredState(tab);
if (event.getModifierState("Accel")) {
let newTab = gBrowser.duplicateTab(tab, true);
newTab.addEventListener("SSTabRestored", () => {
this._resetTabToStoredState(tab);
}, { once: true });
} else {
this._resetTabToStoredState(tab);
}
gBrowser.selectedTab = tab;
}
@@ -170,6 +177,37 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
_onAccelKeyChange(e) {
let tab = this._tabWithResetPinButtonHovered;
if (!tab) {
return;
}
let accelHeld = e.getModifierState("Accel") || (e.metaKey && e.type == "keydown");
this._setResetPinSublabel(tab, accelHeld);
// Up <-> down events until the mouse leaves the button.
// When hovered with accelHeld, we should listen to the next keyup event
let nextEvent = accelHeld ? "keyup" : "keydown";
let handler = (nextE) => this._onAccelKeyChange(nextE);
window.addEventListener(nextEvent, handler, { once: true });
}
_setResetPinSublabel(tab, accelHeld) {
let label = tab.querySelector(".zen-tab-sublabel");
document.l10n.setArgs(label, {
tabSubtitle: accelHeld ? "zen-default-pinned-cmd" : "zen-default-pinned",
});
}
onResetPinButtonMouseOver(tab, event) {
this._tabWithResetPinButtonHovered = tab;
this._onAccelKeyChange(event);
}
onResetPinButtonMouseOut(tab) {
this._setResetPinSublabel(tab, false);
delete this._tabWithResetPinButtonHovered;
}
resetPinnedTab(tab) {
if (!tab) {
tab = TabContextMenu.contextTab;
@@ -812,7 +850,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
tab.removeAttribute("zen-show-sublabel");
const label = tab.querySelector(".zen-tab-sublabel");
window.document.l10n.setArgs(label, {
document.l10n.setArgs(label, {
tabSubtitle: "zen-default-pinned",
});
}

View File

@@ -15,6 +15,8 @@ prefs = ["zen.workspaces.separate-essentials=false"]
["browser_pinned_nounload_reset.js"]
["browser_pinned_reset_button.js"]
["browser_pinned_reset_noswitch.js"]
["browser_pinned_switch.js"]

View File

@@ -0,0 +1,130 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
async function pinAndNavigateTab(url, navigateTo) {
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
gBrowser.pinTab(tab);
await gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
await new Promise((r) => setTimeout(r, 500));
BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, navigateTo);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, navigateTo);
return tab;
}
add_task(async function test_ResetPinButton_SelectsTab() {
const tab = await pinAndNavigateTab("https://example.com/1", "https://example.com/2");
// Open another tab and select it
const otherTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "https://example.com/other");
Assert.notEqual(gBrowser.selectedTab, tab, "The pinned tab should not be selected initially");
// Simulate clicking the reset pin button (without Accel key)
gZenPinnedTabManager._onTabResetPinButton(
{ stopPropagation() {}, getModifierState() { return false; } },
tab
);
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise((r) => setTimeout(r, 100));
Assert.strictEqual(gBrowser.selectedTab, tab, "The pinned tab should be selected after reset");
ok(!tab.hasAttribute("zen-pinned-changed"), "zen-pinned-changed should be removed after reset");
gBrowser.removeTab(otherTab);
gBrowser.removeTab(tab);
});
add_task(async function test_ResetPinButton_CmdClick_DuplicatesAndResets() {
const originalUrl = "https://example.com/1";
const navigatedUrl = "https://example.com/2";
const tab = await pinAndNavigateTab(originalUrl, navigatedUrl);
const tabCountBefore = gBrowser.tabs.length;
// Simulate CMD+click on the reset pin button
gZenPinnedTabManager._onTabResetPinButton(
{ stopPropagation() {}, getModifierState() { return true; } },
tab
);
// Wait for the duplicate tab to be restored
const restoredEvent = await BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored");
const newTab = restoredEvent.target;
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
await new Promise((r) => setTimeout(r, 100));
Assert.equal(gBrowser.tabs.length, tabCountBefore + 1, "A new tab should be created from the duplicate");
Assert.equal(
newTab.linkedBrowser.currentURI.spec,
navigatedUrl,
"The duplicated tab should have the navigated URL"
);
ok(!newTab.pinned, "The duplicated tab should not be pinned");
Assert.strictEqual(gBrowser.selectedTab, tab, "The pinned tab should be selected after CMD+click reset");
ok(!tab.hasAttribute("zen-pinned-changed"), "zen-pinned-changed should be removed after reset");
Assert.equal(
tab.linkedBrowser.currentURI.spec,
originalUrl,
"The pinned tab should be reset to the original URL"
);
gBrowser.removeTab(newTab);
gBrowser.removeTab(tab);
});
add_task(async function test_Hover_SublabelChangesWithAccelKey() {
const tab = await pinAndNavigateTab("https://example.com/1", "https://example.com/2");
// Track calls to document.l10n.setArgs to verify sublabel updates
const sublabelArgs = [];
const label = tab.querySelector(".zen-tab-sublabel");
const origSetArgs = document.l10n.setArgs;
document.l10n.setArgs = (el, args) => {
if (el === label) {
sublabelArgs.push(args.tabSubtitle);
}
origSetArgs.call(document.l10n, el, args);
};
try {
// Simulate hovering with no modifier key held
gZenPinnedTabManager.onResetPinButtonMouseOver(tab, {
getModifierState() { return false; },
metaKey: false,
type: "mouseover",
});
Assert.equal(sublabelArgs.at(-1), "zen-default-pinned", "Sublabel should show default text on hover without Accel");
// Simulate pressing CMD while hovering
gZenPinnedTabManager._onAccelKeyChange({
getModifierState() { return true; },
metaKey: true,
type: "keydown",
});
Assert.equal(sublabelArgs.at(-1), "zen-default-pinned-cmd", "Sublabel should show CMD text when Accel key is pressed");
// Simulate releasing CMD while still hovering
gZenPinnedTabManager._onAccelKeyChange({
getModifierState() { return false; },
metaKey: false,
type: "keyup",
});
Assert.equal(sublabelArgs.at(-1), "zen-default-pinned", "Sublabel should revert to default text when Accel key is released");
// Simulate mouse out
gZenPinnedTabManager.onResetPinButtonMouseOut(tab);
Assert.equal(sublabelArgs.at(-1), "zen-default-pinned", "Sublabel should show default text after mouse out");
ok(!gZenPinnedTabManager._tabWithResetPinButtonHovered, "Hovered tab reference should be cleared after mouse out");
} finally {
document.l10n.setArgs = origSetArgs;
}
gBrowser.removeTab(tab);
});