commit e0722384b722bd5d13103eb93ccd0ccfbad6e49b
parent d7a6ef996dd69a3c32a6f95d24bd8fae8df64355
Author: Jeremy Swinarton <jswinarton@mozilla.com>
Date: Tue, 28 Oct 2025 19:12:07 +0000
Bug 1980036: Stop THP/TGHP from suppressing panel open requests from the other panel element r=sthompson,tabbrowser-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D270239
Diffstat:
2 files changed, 69 insertions(+), 16 deletions(-)
diff --git a/browser/components/tabbrowser/content/tab-hover-preview.mjs b/browser/components/tabbrowser/content/tab-hover-preview.mjs
@@ -186,6 +186,9 @@ class TabPanel extends Panel {
/** @type {DOMElement|null} */
#thumbnailElement;
+ /** @type {TabHoverPanelSet} */
+ #panelSet;
+
constructor(panel, panelSet) {
super();
@@ -202,7 +205,7 @@ class TabPanel extends Panel {
);
this.panelElement = panel;
- this.panelSet = panelSet;
+ this.#panelSet = panelSet;
this.win = this.panelElement.ownerGlobal;
@@ -244,12 +247,12 @@ class TabPanel extends Panel {
) {
this.#updatePreview();
}
- this.panelSet.panelOpener.execute(() => {
- if (!this.panelSet.shouldActivate()) {
+ this.#panelSet.panelOpener.execute(() => {
+ if (!this.#panelSet.shouldActivate()) {
return;
}
this.panelElement.openPopup(this.#tab, this.popupOptions);
- });
+ }, this);
this.win.addEventListener("TabSelect", this);
this.panelElement.addEventListener("popupshowing", this);
this.#tab.addEventListener("TabAttrModified", this);
@@ -273,7 +276,8 @@ class TabPanel extends Panel {
this.panelElement.removeEventListener("popupshowing", this);
this.win.removeEventListener("TabSelect", this);
this.panelElement.hidePopup();
- this.panelSet.panelOpener.setZeroDelay();
+ this.#panelSet.panelOpener.clear(this);
+ this.#panelSet.panelOpener.setZeroDelay();
}
getPrettyURI(uri) {
@@ -503,7 +507,7 @@ class TabGroupPanel extends Panel {
return;
}
this.#doOpenPanel();
- });
+ }, this);
}
/**
@@ -550,6 +554,7 @@ class TabGroupPanel extends Panel {
}
this.panelElement.hidePopup();
+ this.#panelSet.panelOpener.clear(this);
this.#panelSet.panelOpener.setZeroDelay();
}
@@ -696,6 +701,12 @@ class TabPreviewPanelTimedFunction {
/** @type {boolean} */
#useZeroDelay;
+ /** @type {function(): void | null} */
+ #target;
+
+ /** @type {TabPanel} */
+ #from;
+
constructor(zeroDelayTime, win) {
XPCOMUtils.defineLazyPreferenceGetter(
this,
@@ -708,9 +719,38 @@ class TabPreviewPanelTimedFunction {
this.#timer = null;
this.#useZeroDelay = false;
+
+ this.#target = null;
+ this.#from = null;
}
- execute(target) {
+ /**
+ * Execute a function after a delay, according to the following rules:
+ * - By default, execute the function after the time specified by `ui.tooltip.delay_ms`.
+ * - If a timer is already active, the timer will not be restarted, but the
+ * function to be executed will be set to the one from the most recent
+ * call (see notes below)
+ * - If the zero delay has been set with `setZeroDelay`, the function will
+ * invoke immediately
+ *
+ * Multiple calls to `execute` within the delay will not invoke the function
+ * each time. The original delay will be preserved (i.e. the function will
+ * execute after `ui.tooltip.delay_ms` from the first call) but the function
+ * that is executed may be updated by subsequent calls to execute. This
+ * ensures that if the panel we want to open changes (e.g. if a user hovers
+ * over a tab, then quickly switches to a tab group before the delay
+ * expires), the delay is not restarted, which would cause a longer than
+ * usual time to open.
+ *
+ * @param {function(): void | null} target
+ * The function to execute
+ * @param {TabPanel} from
+ * The calling panel
+ */
+ execute(target, from) {
+ this.#target = target;
+ this.#from = from;
+
if (this.delayActive) {
return;
}
@@ -721,22 +761,39 @@ class TabPreviewPanelTimedFunction {
this.#timer = this.#win.setTimeout(
() => {
this.#timer = null;
- target();
+ this.#target();
},
this.#useZeroDelay ? 0 : this._prefPreviewDelay
);
}
- clear() {
- if (this.#timer) {
+ /**
+ * Clear the timer, if it is active, for example when a user moves off a panel.
+ * This has the effect of suppressing the delayed function execution.
+ *
+ * @param {TabPanel} from
+ * The calling panel. This must be the same as the panel that most recently
+ * called `execute`. If it is not, the call will be ignored. This is
+ * necessary to prevent, e.g., the tab hover panel from inadvertently
+ * cancelling the opening of the tab group hover panel in cases where the
+ * user quickly hovers between tabs and tab groups before the panel fully
+ * opens.
+ */
+ clear(from) {
+ if (from == this.#from && this.#timer) {
this.#win.clearTimeout(this.#timer);
this.#timer = null;
+ this.#from = null;
}
}
+ /**
+ * Temporarily suppress the delay mechanism.
+ *
+ * The delay will automatically reactivate after a set interval, which is
+ * configured by the constructor.
+ */
setZeroDelay() {
- this.clear();
-
if (this.#useZeroDelay) {
return;
}
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser.toml b/browser/components/tabbrowser/test/browser/tabs/browser.toml
@@ -580,10 +580,6 @@ tags = "vertical-tabs"
["browser_tab_preview.js"]
tags = "vertical-tabs"
-skip-if = [
- "os == 'linux' && os_version == '24.04' && processor == 'x86_64' && swgl", # Bug 1980036
- "os == 'mac' && os_version == '10.15' && processor == 'x86_64'", # Bug 1980036
-]
["browser_tab_splitview.js"]