Bug 1941189 - Make vertical pinned tabs container adjustable r=desktop-theme-reviewers,tabbrowser-reviewers,sidebar-reviewers,jsudiaman,nsharpley,sfoster

Differential Revision: https://phabricator.services.mozilla.com/D246484
This commit is contained in:
Kelly Cochrane
2025-05-02 21:39:48 +00:00
committed by kcochrane@mozilla.com
parent 19cb41d4a1
commit 40832e607b
6 changed files with 324 additions and 16 deletions

View File

@@ -57,7 +57,7 @@
# DisplayPortUtils::MaybeCreateDisplayPortInFirstScrollFrameEncountered based
# the current structure that we may want to revisit.
<arrowscrollbox id="vertical-pinned-tabs-container" orient="vertical" tabindex="-1"></arrowscrollbox>
<html:div id="vertical-pinned-tabs-container-separator"></html:div>
<splitter orient="vertical" id="vertical-pinned-tabs-splitter" resizebefore="sibling" resizeafter="none" hidden="true"/>
<arrowscrollbox id="tabbrowser-arrowscrollbox" orient="horizontal" flex="1" clicktoscroll="" scrolledtostart="" scrolledtoend="">
<tab is="tabbrowser-tab" class="tabbrowser-tab" selected="true" visuallyselected="" fadein=""/>
<hbox id="tabbrowser-arrowscrollbox-periphery">

View File

@@ -32,11 +32,20 @@ XPCOMUtils.defineLazyPreferenceGetter(
* When sidebar.visibility pref value is "always-show", the toolbar button serves to toggle this property
* @property {boolean} launcherDragActive
* Whether the launcher is currently being dragged.
* @property {boolean} pinnedTabsDragActive
* Whether the pinned tabs container is currently being dragged.
* @property {boolean} launcherHoverActive
* Whether the launcher is currently being hovered.
* @property {number} launcherWidth
* Current width of the sidebar launcher.
* @property {number} expandedLauncherWidth
* Width of the expanded launcher
* @property {number} pinnedTabsHeight
* Current height of the pinned tabs container
* @property {number} expandedPinnedTabsHeight
* Height of the pinned tabs container when the sidebar is expanded
* @property {number} collapsedPinnedTabsHeight
* Height of the pinned tabs container when the sidebar is collapsed
*/
const LAUNCHER_MINIMUM_WIDTH = 100;
@@ -66,6 +75,7 @@ export class SidebarState {
launcherHoverActive: false,
launcherVisible: false,
panelOpen: false,
pinnedTabsDragActive: false,
});
/**
@@ -122,6 +132,26 @@ export class SidebarState {
return this.#controller._box;
}
/**
* Get the pinned tabs container element.
*
* @returns {XULElement}
*/
get #pinnedTabsContainerEl() {
return this.#controller._pinnedTabsContainer;
}
/**
* Get the items-wrapper part of the pinned tabs container element.
*
* @returns {XULElement}
*/
get #pinnedTabsItemsWrapper() {
return this.#pinnedTabsContainerEl.shadowRoot.querySelector(
"[part=items-wrapper]"
);
}
/**
* Get window object from the controller.
*/
@@ -193,6 +223,10 @@ export class SidebarState {
case "panelOpen":
// we need to know if we have a command value before finalizing panelOpen
break;
case "expandedPinnedTabsHeight":
case "collapsedPinnedTabsHeight":
this.#updatePinnedTabsHeight();
break;
default:
this[key] = value;
}
@@ -240,6 +274,9 @@ export class SidebarState {
expandedLauncherWidth: convertToInt(this.expandedLauncherWidth),
launcherExpanded: this.launcherExpanded,
launcherVisible: this.launcherVisible,
pinnedTabsHeight: this.pinnedTabsHeight,
expandedPinnedTabsHeight: this.expandedPinnedTabsHeight,
collapsedPinnedTabsHeight: this.collapsedPinnedTabsHeight,
};
// omit any properties with undefined values'
for (let [key, value] of Object.entries(props)) {
@@ -290,6 +327,24 @@ export class SidebarState {
this.#launcherContainerEl.style.maxWidth = `calc(${SIDEBAR_MAXIMUM_WIDTH} - ${width}px)`;
}
get expandedPinnedTabsHeight() {
return this.#props.expandedPinnedTabsHeight;
}
set expandedPinnedTabsHeight(height) {
this.#props.expandedPinnedTabsHeight = height;
this.#updatePinnedTabsHeight();
}
get collapsedPinnedTabsHeight() {
return this.#props.collapsedPinnedTabsHeight;
}
set collapsedPinnedTabsHeight(height) {
this.#props.collapsedPinnedTabsHeight = height;
this.#updatePinnedTabsHeight();
}
get defaultLauncherVisible() {
if (!this.revampEnabled) {
return false;
@@ -390,6 +445,12 @@ export class SidebarState {
if (!this.launcherDragActive) {
this.#updateLauncherWidth();
}
if (
!this.pinnedTabsDragActive &&
this.#controller.sidebarRevampVisibility !== "expand-on-hover"
) {
this.#updatePinnedTabsHeight();
}
}
get launcherDragActive() {
@@ -429,6 +490,34 @@ export class SidebarState {
rootEl.toggleAttribute("sidebar-launcher-drag-active", active);
}
get pinnedTabsDragActive() {
return this.#props.pinnedTabsDragActive;
}
set pinnedTabsDragActive(active) {
this.#props.pinnedDragActive = active;
let itemsWrapperHeight =
this.#controllerGlobal.windowUtils.getBoundsWithoutFlushing(
this.#pinnedTabsItemsWrapper
).height;
if (this.pinnedTabsHeight > itemsWrapperHeight) {
this.pinnedTabsHeight = itemsWrapperHeight;
if (this.#props.launcherExpanded) {
this.expandedPinnedTabsHeight = this.pinnedTabsHeight;
} else {
this.collapsedPinnedTabsHeight = this.pinnedTabsHeight;
}
} else if (!active) {
// Store the user-preferred pinned tabs height.
if (this.#props.launcherExpanded) {
this.expandedPinnedTabsHeight = this.pinnedTabsHeight;
} else {
this.collapsedPinnedTabsHeight = this.pinnedTabsHeight;
}
}
}
get launcherHoverActive() {
return this.#props.launcherHoverActive;
}
@@ -478,6 +567,31 @@ export class SidebarState {
);
}
get pinnedTabsHeight() {
return this.#props.pinnedTabsHeight;
}
set pinnedTabsHeight(height) {
this.#props.pinnedTabsHeight = height;
if (this.launcherExpanded) {
this.expandedPinnedTabsHeight = height;
} else {
this.collapsedPinnedTabsHeight = height;
}
}
/**
* When the sidebar is expanded/collapsed, resize the pinned tabs container to the user-preferred
* height (if available).
*/
#updatePinnedTabsHeight() {
if (this.launcherExpanded && this.expandedPinnedTabsHeight) {
this.#pinnedTabsContainerEl.style.height = `${this.expandedPinnedTabsHeight}px`;
} else if (!this.launcherExpanded && this.collapsedPinnedTabsHeight) {
this.#pinnedTabsContainerEl.style.height = `${this.collapsedPinnedTabsHeight}px`;
}
}
#updateTabbrowser(isSidebarShown) {
this.#controllerGlobal.document
.getElementById("tabbrowser-tabbox")

View File

@@ -252,6 +252,8 @@ var SidebarController = {
lastOpenedId: null,
_box: null,
_pinnedTabsContainer: null,
_pinnedTabsItemsWrapper: null,
// The constructor of this label accesses the browser element due to the
// control="sidebar" attribute, so avoid getting this label during startup.
get _title() {
@@ -334,6 +336,10 @@ var SidebarController = {
return this._launcherSplitter.getAttribute("state") === "dragging";
},
get isPinnedTabsDragging() {
return this._pinnedTabsSplitter.getAttribute("state") === "dragging";
},
init() {
// Initialize global state manager.
this.SidebarManager;
@@ -343,11 +349,21 @@ var SidebarController = {
this._state = new this.SidebarState(this);
}
this._pinnedTabsContainer = document.getElementById(
"vertical-pinned-tabs-container"
);
this._pinnedTabsItemsWrapper =
this._pinnedTabsContainer.shadowRoot.querySelector(
"[part=items-wrapper]"
);
this._box = document.getElementById("sidebar-box");
this._splitter = document.getElementById("sidebar-splitter");
this._launcherSplitter = document.getElementById(
"sidebar-launcher-splitter"
);
this._pinnedTabsSplitter = document.getElementById(
"vertical-pinned-tabs-splitter"
);
this._reversePositionButton = document.getElementById(
"sidebar-reverse-position"
);
@@ -416,6 +432,7 @@ var SidebarController = {
this._splitter.addEventListener("command", this._browserResizeObserver);
}
this._enableLauncherDragging();
this._enablePinnedTabsSplitterDragging();
// Record Glean metrics.
this.recordVisibilitySetting();
@@ -436,6 +453,7 @@ var SidebarController = {
this._switcherListenersAdded = true;
}
this._disableLauncherDragging();
this._disablePinnedTabsDragging();
}
// We need to update the tab strip for vertical tabs during init
// as there will be no tabstrip-orientation-change event
@@ -514,6 +532,7 @@ var SidebarController = {
}
this._splitter.removeEventListener("command", this._browserResizeObserver);
this._disableLauncherDragging();
this._disablePinnedTabsDragging();
},
/**
@@ -1313,6 +1332,66 @@ var SidebarController = {
this._launcherSplitter.hidden = false;
},
/**
* Enable the splitter which can be used to resize the pinned tabs container.
*/
_enablePinnedTabsSplitterDragging() {
if (!this._pinnedTabsSplitter.hidden) {
// Already showing the launcher splitter with observers connected.
// Nothing to do.
return;
}
this._pinnedTabsResizeObserver = new ResizeObserver(([entry]) => {
if (this.isPinnedTabsDragging) {
this._state.pinnedTabsDragActive = true;
}
if (
(entry.contentBoxSize[0].blockSize ===
this._state.expandedPinnedTabsHeight &&
this._state.launcherExpanded) ||
(entry.contentBoxSize[0].blockSize ===
this._state.collapsedPinnedTabsHeight &&
!this._state.launcherExpanded)
) {
// condition already met, no need to re-update
return;
}
this._state.pinnedTabsHeight = entry.contentBoxSize[0].blockSize;
});
this._itemsWrapperResizeObserver = new ResizeObserver(async () => {
await window.promiseDocumentFlushed(() => {
// Adjust pinned tabs container height if needed
let itemsWrapperHeight = window.windowUtils.getBoundsWithoutFlushing(
this._pinnedTabsItemsWrapper
).height;
requestAnimationFrame(() => {
if (this._state.pinnedTabsHeight > itemsWrapperHeight) {
this._state.pinnedTabsHeight = itemsWrapperHeight;
if (this._state.launcherExpanded) {
this._state.expandedPinnedTabsHeight =
this._state.pinnedTabsHeight;
} else {
this._state.collapsedPinnedTabsHeight =
this._state.pinnedTabsHeight;
}
}
});
});
});
this._pinnedTabsResizeObserver.observe(this._pinnedTabsContainer);
this._itemsWrapperResizeObserver.observe(this._pinnedTabsItemsWrapper);
this._pinnedTabsDropHandler = () =>
(this._state.pinnedTabsDragActive = false);
this._pinnedTabsSplitter.addEventListener(
"command",
this._pinnedTabsDropHandler
);
this._pinnedTabsSplitter.hidden = false;
},
/**
* Disable the launcher splitter and remove any active observers.
*/
@@ -1328,6 +1407,20 @@ var SidebarController = {
this._launcherSplitter.hidden = true;
},
/**
* Disable the pinned tabs splitter and remove any active observers.
*/
_disablePinnedTabsDragging() {
if (this._pinnedTabsResizeObserver) {
this._pinnedTabsResizeObserver.disconnect();
}
if (this._itemsWrapperResizeObserver) {
this._itemsWrapperResizeObserver.disconnect();
}
this._pinnedTabsSplitter.hidden = true;
},
_loadSidebarExtension(commandID) {
let sidebar = this.sidebars.get(commandID);
if (typeof sidebar?.onload === "function") {
@@ -2020,6 +2113,11 @@ var SidebarController = {
this.mouseOverTask?.finalize();
}
}
document.documentElement.toggleAttribute(
"sidebar-expand-on-hover",
isEnabled
);
},
/**
@@ -2183,6 +2281,11 @@ XPCOMUtils.defineLazyPreferenceGetter(
!SidebarController.inSingleTabWindow
) {
SidebarController.recordTabsLayoutSetting(newValue);
if (newValue) {
SidebarController._enablePinnedTabsSplitterDragging();
} else {
SidebarController._disablePinnedTabsDragging();
}
}
}
);

View File

@@ -31,7 +31,7 @@ async function dragLauncher(deltaX, shouldExpand) {
info(`Drag the launcher by ${deltaX} px.`);
const { sidebarMain, _launcherSplitter: splitter } = SidebarController;
EventUtils.synthesizeMouseAtCenter(splitter, { type: "mousedown" });
await mouseMoveInChunks(splitter, deltaX, 10);
await mouseMoveInChunksHorizontal(splitter, deltaX, 10);
EventUtils.synthesizeMouse(splitter, 0, 0, { type: "mouseup" });
info(`The sidebar should be ${shouldExpand ? "expanded" : "collapsed"}.`);
@@ -44,7 +44,24 @@ async function dragLauncher(deltaX, shouldExpand) {
AccessibilityUtils.resetEnv();
}
async function mouseMoveInChunks(el, deltaX, numberOfChunks) {
async function dragPinnedTabs(deltaY) {
AccessibilityUtils.setEnv({ mustHaveAccessibleRule: false });
// Let the pinned tabs splitter stabilize before attempting a drag-and-drop.
await waitForRepaint();
info(`Drag the launcher by ${deltaY} px.`);
const { _pinnedTabsSplitter: splitter } = SidebarController;
EventUtils.synthesizeMouseAtCenter(splitter, { type: "mousedown" });
await mouseMoveInChunksVertical(splitter, deltaY, 10);
EventUtils.synthesizeMouse(splitter, 0, 0, { type: "mouseup" });
info(`The pinned tabs container has been expanded.`);
AccessibilityUtils.resetEnv();
}
async function mouseMoveInChunksHorizontal(el, deltaX, numberOfChunks) {
let chunkIndex = 0;
const chunkSize = deltaX / numberOfChunks;
const finished = Promise.withResolvers();
@@ -64,10 +81,35 @@ async function mouseMoveInChunks(el, deltaX, numberOfChunks) {
await finished.promise;
}
async function mouseMoveInChunksVertical(el, deltaY, numberOfChunks) {
let chunkIndex = 0;
const chunkSize = deltaY / numberOfChunks;
const finished = Promise.withResolvers();
function synthesizeMouseMove() {
info(`chunkSize: ${chunkSize}`);
// mousemove by a single chunk. Queue up the next chunk if necessary.
EventUtils.synthesizeMouse(el, 0, chunkSize, { type: "mousemove" });
if (++chunkIndex === numberOfChunks) {
finished.resolve();
} else {
requestAnimationFrame(synthesizeMouseMove);
}
}
await waitForRepaint();
requestAnimationFrame(synthesizeMouseMove);
await finished.promise;
}
function getLauncherWidth({ SidebarController } = window) {
return SidebarController.sidebarContainer.style.width;
}
function getPinnedTabsHeight({ SidebarController } = window) {
return SidebarController._pinnedTabsContainer.clientHeight;
}
add_task(async function test_drag_expand_and_collapse() {
await dragLauncher(200, true);
ok(getLauncherWidth(), "Launcher width set.");
@@ -159,4 +201,38 @@ add_task(async function test_resize_after_toggling_revamp() {
parseInt(originalWidth),
"Vertical tab strip was resized."
);
await dragLauncher(-200, true);
});
add_task(async function test_resize_of_pinned_tabs() {
await SidebarController.initializeUIState({
launcherExpanded: true,
});
info("Open 10 new tabs using the new tab button.");
for (let i = 0; i < 10; i++) {
await BrowserTestUtils.openNewForegroundTab(
gBrowser,
`data:text/html,<title>${i + 1}</title>`
);
gBrowser.pinTab(gBrowser.selectedTab);
}
await SidebarController.waitUntilStable();
info("Resize the pinned tabs container.");
const originalHeight = getPinnedTabsHeight();
await dragPinnedTabs(200, true);
await SidebarController.waitUntilStable();
const newHeight = getPinnedTabsHeight();
info(`original: ${originalHeight}, new: ${newHeight}`);
Assert.greater(
parseInt(newHeight),
parseInt(originalHeight),
"Pinned tabs container was resized."
);
while (gBrowser.tabs.length > 1) {
BrowserTestUtils.removeTab(gBrowser.tabs.at(-1));
}
});

View File

@@ -201,9 +201,10 @@ add_task(async function test_expand_on_hover_pinned_tabs() {
SidebarController.sidebarMain.hasAttribute("expanded"),
"The launcher is expanded"
);
is(
Math.round(parseInt(verticalTabsComputedStyle.width)),
Assert.less(
Math.round(parseInt(verticalTabsComputedStyle.width)) %
Math.round(parseInt(pinnedTabComputedStyle.width)),
10,
"The pinned tabs are full width when expanded"
);

View File

@@ -105,6 +105,8 @@
--tab-group-color-gray-pale: #F2F9FF;
--tab-group-label-text-dark: var(--color-gray-100);
/* 5px of padding-block are adding to .tabbrowser-tab */
--tab-height-with-margin-padding: calc(5px + var(--tab-min-height) + (2 * var(--tab-block-margin)));
}
/* stylelint-disable-next-line media-query-no-invalid */
@@ -1351,7 +1353,8 @@ tab-group {
/* Vertical tabs styling */
#tabbrowser-arrowscrollbox[orient="vertical"] {
min-height: 1px;
/* Don't allow resizing below the height of 3 tabs */
min-height: calc(3 * var(--tab-height-with-margin-padding));
&::part(scrollbutton-up),
&::part(scrollbutton-down) {
@@ -1424,8 +1427,8 @@ tab-group {
}
#vertical-pinned-tabs-container {
/* Fit slightly more than 5 tabs + padding before overflowing */
max-height: calc(5 * var(--tabstrip-min-height) + var(--space-large));
/* Don't allow resizing below the height of 1 row of pinned tabs */
min-height: var(--tab-height-with-margin-padding);
&:empty {
display: none;
@@ -1473,6 +1476,10 @@ tab-group {
}
}
&::part(items-wrapper) {
flex: 0 1 0;
}
:root:not([sidebar-expand-on-hover]) & {
--tab-inline-padding: calc((var(--tab-collapsed-background-width) + 2 *
var(--tab-pinned-margin-inline-expanded) - var(--icon-size-default)) / 2);
@@ -1488,6 +1495,7 @@ tab-group {
minmax(var(--tab-pinned-min-width-expanded), auto)
);
display: grid;
grid-auto-rows: var(--tab-height-with-margin-padding);
}
.tab-label-container {
@@ -1524,15 +1532,21 @@ tab-group {
}
}
#vertical-pinned-tabs-container-separator {
#vertical-pinned-tabs-splitter {
display: block;
border-bottom: var(--tabstrip-inner-border);
border-top: var(--tabstrip-inner-border);
margin-inline: var(--tab-inner-inline-margin);
min-height: 2px;
#vertical-pinned-tabs-container:empty + & {
display: none;
}
&:hover {
background-color: var(--focus-outline-color);
border-radius: var(--border-radius-medium);
}
/* stylelint-disable-next-line media-query-no-invalid */
@media not -moz-pref("sidebar.visibility", "expand-on-hover") {
/* We need these rules to apply at all times when the sidebar.visibility
@@ -1540,7 +1554,7 @@ tab-group {
has not been added to root. There are certain scenarios when that attribute is temporarily
removed from root such as when toggling the sidebar to expand with the toolbar button. */
#tabbrowser-tabs[expanded] > & {
display: none;
border: none;
}
}
}