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)),
Math.round(parseInt(pinnedTabComputedStyle.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,23 +1532,29 @@ 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
pref is not set to "expand-on-hover" as opposed to when the "sidebar-expand-on-hover" attribute
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. */
/* We need these rules to apply at all times when the sidebar.visibility
pref is not set to "expand-on-hover" as opposed to when the "sidebar-expand-on-hover" attribute
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;
}
}
}