diff --git a/browser/components/tabbrowser/content/tabs.js b/browser/components/tabbrowser/content/tabs.js index 676bb0e53d26..24a9df9d99d8 100644 --- a/browser/components/tabbrowser/content/tabs.js +++ b/browser/components/tabbrowser/content/tabs.js @@ -41,6 +41,8 @@ #maxTabsPerRow; #dragOverCreateGroupTimer; + #mustUpdateTabMinHeight = false; + #tabMinHeight = 36; constructor() { super(); @@ -92,6 +94,18 @@ return (!tab.pinned || !arePositioningPinnedTabs()) && tab.visible; }; + // Override for performance reasons. This is the size of a single element + // that can be scrolled when using mouse wheel scrolling. If we don't do + // this then arrowscrollbox computes this value by calling + // _getScrollableElements and dividing the box size by that number. + // However in the tabstrip case we already know the answer to this as, + // when we're overflowing, it is always the same as the tab min width or + // height. + Object.defineProperty(this.arrowScrollbox, "lineScrollAmount", { + get: () => + this.verticalMode ? this.#tabMinHeight : this._tabMinWidthPref, + }); + this.baseConnect(); this._blockDblClick = false; @@ -163,6 +177,7 @@ } ); this.#updateTabMinWidth(this._tabMinWidthPref); + this.#updateTabMinHeight(); CustomizableUI.addListener(this); this._updateNewTabVisibility(); @@ -202,6 +217,7 @@ this._positionPinnedTabs(); this.#updateTabMinWidth(); + this.#updateTabMinHeight(); let indicatorTabs = gBrowser.visibleTabs.filter(tab => { return ( @@ -1529,7 +1545,11 @@ node = this.arrowScrollbox.lastChild; } - return node.before(tab); + node.before(tab); + + if (this.#mustUpdateTabMinHeight) { + this.#updateTabMinHeight(); + } } #updateTabMinWidth(val) { @@ -1544,6 +1564,53 @@ } } + #updateTabMinHeight() { + if (!this.verticalMode || !window.toolbar.visible) { + this.#mustUpdateTabMinHeight = false; + return; + } + + // Find at least one tab we can scroll to. + let firstScrollableTab = this.visibleTabs.find( + this.arrowScrollbox._canScrollToElement + ); + + if (!firstScrollableTab) { + // If not, we're in a pickle. We should never get here except if we + // also don't use the outcome of this work (because there's nothing to + // scroll so we don't care about the scrollbox size). + // So just set a flag so we re-run once we do have a new tab. + this.#mustUpdateTabMinHeight = true; + return; + } + + let { height } = + window.windowUtils.getBoundsWithoutFlushing(firstScrollableTab); + + // Use the current known height or a sane default. + this.#tabMinHeight = height || 36; + + // The height we got may be incorrect if a flush is pending so re-check it after + // a flush completes. + window + .promiseDocumentFlushed(() => {}) + .then( + () => { + height = + window.windowUtils.getBoundsWithoutFlushing( + firstScrollableTab + ).height; + + if (height) { + this.#tabMinHeight = height; + } + }, + () => { + /* ignore errors */ + } + ); + } + get _isCustomizing() { return document.documentElement.hasAttribute("customizing"); } @@ -1791,6 +1858,7 @@ uiDensityChanged() { this._positionPinnedTabs(); this._updateCloseButtons(); + this.#updateTabMinHeight(); this._handleTabSelect(true); } diff --git a/browser/components/tabbrowser/test/browser/tabs/browser.toml b/browser/components/tabbrowser/test/browser/tabs/browser.toml index 7fd4004b7f9b..ea2dc3994e66 100644 --- a/browser/components/tabbrowser/test/browser/tabs/browser.toml +++ b/browser/components/tabbrowser/test/browser/tabs/browser.toml @@ -321,6 +321,8 @@ support-files = ["tab_that_opens_dialog.html"] ["browser_restore_isAppTab.js"] run-if = ["crashreporter"] # test requires crashreporter due to 1536221 +["browser_scroll_size_determination.js"] + ["browser_selectTabAtIndex.js"] ["browser_switch_by_scrolling.js"] diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_scroll_size_determination.js b/browser/components/tabbrowser/test/browser/tabs/browser_scroll_size_determination.js new file mode 100644 index 000000000000..685256c8d7da --- /dev/null +++ b/browser/components/tabbrowser/test/browser/tabs/browser_scroll_size_determination.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that when opening a new window with vertical tabs turned + * on/off, wheel events with DOM_DELTA_LINE deltaMode successfully + * scroll the tabstrip. + */ +async function scrolling_works(useVerticalTabs, uiDensity) { + info(`Testing UI density ${uiDensity}`); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["sidebar.revamp", useVerticalTabs], + ["sidebar.verticalTabs", useVerticalTabs], + ], + }); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + + win.gUIDensity.update(win.gUIDensity[uiDensity]); + + await BrowserTestUtils.overflowTabs(registerCleanupFunction, win, { + overflowAtStart: false, + overflowTabFactor: 3, + }); + + await TestUtils.waitForCondition(() => { + return Array.from(win.gBrowser.tabs).every(tab => tab._fullyOpen); + }); + + win.gBrowser.pinTab(win.gBrowser.tabs[0]); + + let firstScrollableTab = win.gBrowser.tabs[1]; + + // Scroll back to start. + win.gBrowser.selectedTab = firstScrollableTab; + await TestUtils.waitForTick(); + await win.promiseDocumentFlushed(() => {}); + + // Check we're scrolled so the first scrollable tab is at the top. + let { arrowScrollbox } = win.gBrowser.tabContainer; + let side = useVerticalTabs ? "top" : "left"; + let boxStart = arrowScrollbox.getBoundingClientRect()[side]; + let firstPoint = boxStart + 5; + Assert.equal( + gBrowser.tabs.indexOf(arrowScrollbox._elementFromPoint(firstPoint)), + gBrowser.tabs.indexOf(firstScrollableTab), + "First tab should be scrolled into view." + ); + + // Scroll. + EventUtils.synthesizeWheel( + arrowScrollbox, + 10, + 10, + { + wheel: true, + deltaY: 1, + deltaMode: WheelEvent.DOM_DELTA_LINE, + }, + win + ); + + // Check that some other tab is scrolled into view. + try { + await TestUtils.waitForCondition(() => { + return arrowScrollbox._elementFromPoint(firstPoint) != firstScrollableTab; + }); + } catch (ex) { + Assert.ok(false, `Failed to see scroll, error: ${ex}`); + } + Assert.notEqual( + win.gBrowser.tabs.indexOf(arrowScrollbox._elementFromPoint(firstPoint)), + win.gBrowser.tabs.indexOf(firstScrollableTab), + "First tab should be scrolled out of view." + ); + + await SpecialPowers.popPrefEnv(); + + await BrowserTestUtils.closeWindow(win); +} + +add_task(async function test_vertical_scroll() { + for (let density of ["MODE_NORMAL", "MODE_COMPACT", "MODE_TOUCH"]) { + await scrolling_works(true, density); + } +}); + +add_task(async function test_horizontal_scroll() { + for (let density of ["MODE_NORMAL", "MODE_COMPACT", "MODE_TOUCH"]) { + await scrolling_works(false, density); + } +}); diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs index 0ba7474ceffb..367929e1e12f 100644 --- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs +++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs @@ -1928,8 +1928,11 @@ export var BrowserTestUtils = { if (!params.hasOwnProperty("overflowTabFactor")) { params.overflowTabFactor = 1.1; } - let index = params.overflowAtStart ? 0 : undefined; let { gBrowser } = win; + let overflowDirection = gBrowser.tabContainer.verticalMode + ? "height" + : "width"; + let index = params.overflowAtStart ? 0 : undefined; let arrowScrollbox = gBrowser.tabContainer.arrowScrollbox; if (arrowScrollbox.hasAttribute("overflowing")) { return; @@ -1949,12 +1952,12 @@ export var BrowserTestUtils = { arrowScrollbox.smoothScroll = originalSmoothScroll; }); - let width = ele => ele.getBoundingClientRect().width; - let tabMinWidth = parseInt( - win.getComputedStyle(gBrowser.selectedTab).minWidth - ); + let size = ele => ele.getBoundingClientRect()[overflowDirection]; + let tabMinSize = gBrowser.tabContainer.verticalMode + ? size(gBrowser.selectedTab) + : parseInt(win.getComputedStyle(gBrowser.selectedTab).minWidth); let tabCountForOverflow = Math.ceil( - (width(arrowScrollbox) / tabMinWidth) * params.overflowTabFactor + (size(arrowScrollbox) / tabMinSize) * params.overflowTabFactor ); while (gBrowser.tabs.length < tabCountForOverflow) { promises.push(