commit bce47e0d449442c49541013464ef0ba4beb13642
parent 8fcfdc31706b47475b09cd0a9dce75151eb16c8c
Author: Jonathan Sudiaman <jsudiaman@mozilla.com>
Date: Thu, 4 Dec 2025 14:57:20 +0000
Bug 1986986 - Add a non-removable urlbar indicator for split view r=tabbrowser-reviewers,desktop-theme-reviewers,urlbar-reviewers,fluent-reviewers,bolsson,nsharpley,jteow
- Refactor split-view-footer menu into a reusable popup in main-popupset.
- Add split view icon.
- Leverage activate/deactivate handlers to control button visibility.
Differential Revision: https://phabricator.services.mozilla.com/D273225
Diffstat:
13 files changed, 154 insertions(+), 22 deletions(-)
diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml
@@ -416,6 +416,16 @@
id="sidebar-synced-tabs-context-copy-link"/>
</menupopup>
+ <menupopup id="split-view-menu">
+ <menuitem command="splitViewCmd_separateTabs"
+ data-l10n-id="split-view-menuitem-separate-tabs"/>
+ <menuitem command="splitViewCmd_reverseTabs"
+ data-l10n-id="split-view-menuitem-reverse-tabs"/>
+ <menuseparator/>
+ <menuitem command="splitViewCmd_closeTabs"
+ data-l10n-id="split-view-menuitem-close-both-tabs"/>
+ </menupopup>
+
<menupopup id="toolbar-context-menu">
<menuitem id="toolbar-context-manage-extension"
data-lazy-l10n-id="toolbar-context-menu-manage-extension"
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -389,6 +389,13 @@
<toolbarbutton id="urlbar-zoom-button"
tooltip="dynamic-shortcut-tooltip"
hidden="true"/>
+ <hbox id="split-view-button"
+ class="urlbar-page-action"
+ role="button"
+ data-l10n-id="urlbar-split-view-button"
+ hidden="true">
+ <image class="urlbar-icon" id="split-view-button-icon" />
+ </hbox>
<hbox id="pageActionButton"
class="urlbar-page-action"
role="button"
diff --git a/browser/base/content/navigator-toolbox.js b/browser/base/content/navigator-toolbox.js
@@ -195,7 +195,8 @@ document.addEventListener(
#tracking-protection-icon-container,
#identity-icon-box,
#identity-permission-box,
- #translations-button
+ #translations-button,
+ #split-view-button
`);
if (!element) {
return;
@@ -284,6 +285,12 @@ document.addEventListener(
FullPageTranslationsPanel.open(event);
break;
+ case "split-view-button":
+ if (isLeftClick) {
+ gBrowser.openSplitViewMenu(element);
+ }
+ break;
+
default:
throw new Error(`Missing case for #${element.id}`);
}
@@ -311,7 +318,8 @@ document.addEventListener(
#downloads-button,
#fxa-toolbar-menu-button,
#unified-extensions-button,
- #library-button
+ #library-button,
+ #split-view-button
`);
if (!element) {
return;
@@ -397,6 +405,12 @@ document.addEventListener(
PanelUI.showSubView("appMenu-libraryView", element, event);
break;
+ case "split-view-button":
+ if (isLikeLeftClick) {
+ gBrowser.openSplitViewMenu(element);
+ }
+ break;
+
default:
throw new Error(`Missing case for #${element.id}`);
}
diff --git a/browser/components/tabbrowser/content/split-view-footer.js b/browser/components/tabbrowser/content/split-view-footer.js
@@ -61,17 +61,8 @@
</hbox>
<html:img class="split-view-icon" hidden="" role="presentation"/>
<html:span class="split-view-uri"></html:span>
- <toolbarbutton type="menu" image="chrome://global/skin/icons/more.svg">
- <menupopup class="split-view-footer-menu">
- <menuitem command="splitViewCmd_separateTabs"
- data-l10n-id="split-view-menuitem-separate-tabs"/>
- <menuitem command="splitViewCmd_reverseTabs"
- data-l10n-id="split-view-menuitem-reverse-tabs"/>
- <menuseparator/>
- <menuitem command="splitViewCmd_closeTabs"
- data-l10n-id="split-view-menuitem-close-both-tabs"/>
- </menupopup>
- </toolbarbutton>
+ <toolbarbutton image="chrome://global/skin/icons/more.svg"
+ data-l10n-id="urlbar-split-view-button" />
`;
connectedCallback() {
@@ -83,6 +74,7 @@
this.securityElement = this.querySelector(".split-view-security-warning");
this.iconElement = this.querySelector(".split-view-icon");
this.uriElement = this.querySelector(".split-view-uri");
+ this.menuButtonElement = this.querySelector("toolbarbutton");
// Ensure these elements are up-to-date, as this info may have been set
// prior to inserting this element into the DOM.
@@ -90,6 +82,8 @@
this.#updateIconElement();
this.#updateUriElement();
+ this.menuButtonElement.addEventListener("command", this);
+
this.#initialized = true;
}
@@ -99,6 +93,9 @@
handleEvent(e) {
switch (e.type) {
+ case "command":
+ gBrowser.openSplitViewMenu(this.menuButtonElement);
+ break;
case "TabAttrModified":
for (const attribute of e.detail.changed) {
this.#handleTabAttrModified(attribute);
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
@@ -388,6 +388,10 @@
/** @type {MozTabSplitViewWrapper} */
#activeSplitView = null;
+ get activeSplitView() {
+ return this.#activeSplitView;
+ }
+
/**
* List of browsers which are currently in an active Split View.
*
@@ -3306,6 +3310,11 @@
panelEl.appendChild(footer);
}
+ openSplitViewMenu(anchorElement) {
+ const menu = document.getElementById("split-view-menu");
+ menu.openPopup(anchorElement, "after_start");
+ }
+
/**
* @param {string} id
* @param {string} color
@@ -7793,10 +7802,10 @@
break;
}
case "TabSplitViewActivate":
- this.#activeSplitView = aEvent.originalTarget;
+ this.#activeSplitView = aEvent.detail.splitview;
break;
case "TabSplitViewDeactivate":
- if (this.#activeSplitView === aEvent.originalTarget) {
+ if (this.#activeSplitView === aEvent.detail.splitview) {
this.#activeSplitView = null;
}
break;
diff --git a/browser/components/tabbrowser/content/tabsplitview.js b/browser/components/tabbrowser/content/tabsplitview.js
@@ -7,6 +7,31 @@
// This is loaded into chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
+ ChromeUtils.defineESModuleGetters(this, {
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+ });
+
+ /**
+ * A shared task which updates the urlbar indicator whenever:
+ * - A split view is activated or deactivated.
+ * - The active tab of a split view changes.
+ * - The order of tabs in a split view changes.
+ *
+ * @type {DeferredTask}
+ */
+ const updateUrlbarButton = new DeferredTask(() => {
+ const { activeSplitView, selectedTab } = gBrowser;
+ const button = document.getElementById("split-view-button");
+ if (activeSplitView) {
+ const activeIndex = activeSplitView.tabs.indexOf(selectedTab);
+ button.hidden = false;
+ button.setAttribute("data-active-index", activeIndex);
+ } else {
+ button.hidden = true;
+ button.removeAttribute("data-active-index");
+ }
+ }, 0);
+
class MozTabSplitViewWrapper extends MozXULElement {
/** @type {MutationObserver} */
#tabChangeObserver;
@@ -116,9 +141,10 @@
*/
#activate() {
gBrowser.showSplitViewPanels(this.#tabs);
- this.dispatchEvent(
+ updateUrlbarButton.arm();
+ this.container.dispatchEvent(
new CustomEvent("TabSplitViewActivate", {
- detail: { tabs: this.#tabs },
+ detail: { tabs: this.#tabs, splitview: this },
bubbles: true,
})
);
@@ -129,9 +155,10 @@
*/
#deactivate() {
gBrowser.hideSplitViewPanels(this.#tabs);
- this.dispatchEvent(
+ updateUrlbarButton.arm();
+ this.container.dispatchEvent(
new CustomEvent("TabSplitViewDeactivate", {
- detail: { tabs: this.#tabs },
+ detail: { tabs: this.#tabs, splitview: this },
bubbles: true,
})
);
@@ -180,6 +207,7 @@
gBrowser.moveTabBefore(secondTab, firstTab);
this.#tabs = [secondTab, firstTab];
gBrowser.showSplitViewPanels(this.#tabs);
+ updateUrlbarButton.arm();
}
/**
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js
@@ -15,6 +15,8 @@ registerCleanupFunction(async function () {
);
});
+const urlbarButton = document.getElementById("split-view-button");
+
async function addTabAndLoadBrowser() {
const tab = BrowserTestUtils.addTab(gBrowser, "https://example.com");
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
@@ -126,6 +128,18 @@ add_task(async function test_split_view_panels() {
for (const tab of splitView.tabs) {
await checkSplitViewPanelVisible(tab, true);
}
+ await BrowserTestUtils.waitForMutationCondition(
+ urlbarButton,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => BrowserTestUtils.isVisible(urlbarButton)
+ );
+
+ info("Open split view menu.");
+ const menu = document.getElementById("split-view-menu");
+ const promiseMenuShown = BrowserTestUtils.waitForPopupEvent(menu, "shown");
+ EventUtils.synthesizeMouseAtCenter(urlbarButton, {});
+ await promiseMenuShown;
+ menu.hidePopup();
info("Select tabs using tab panels.");
await SimpleTest.promiseFocus(tab1.linkedBrowser);
@@ -134,6 +148,11 @@ add_task(async function test_split_view_panels() {
panel.classList.contains("deck-selected"),
"First panel is selected."
);
+ await BrowserTestUtils.waitForMutationCondition(
+ urlbarButton,
+ { attributes: true, attributeFilter: ["data-active-index"] },
+ () => urlbarButton.dataset.activeIndex == "0"
+ );
await SimpleTest.promiseFocus(tab2.linkedBrowser);
panel = document.getElementById(tab2.linkedPanel);
@@ -141,6 +160,11 @@ add_task(async function test_split_view_panels() {
panel.classList.contains("deck-selected"),
"Second panel is selected."
);
+ await BrowserTestUtils.waitForMutationCondition(
+ urlbarButton,
+ { attributes: true },
+ () => urlbarButton.dataset.activeIndex == "1"
+ );
info("Switch to a non-split view tab.");
await BrowserTestUtils.switchTab(gBrowser, originalTab);
@@ -158,6 +182,11 @@ add_task(async function test_split_view_panels() {
splitView.unsplitTabs();
await checkSplitViewPanelVisible(tab1, false);
await checkSplitViewPanelVisible(tab2, false);
+ await BrowserTestUtils.waitForMutationCondition(
+ urlbarButton,
+ { attributes: true },
+ () => BrowserTestUtils.isHidden(urlbarButton)
+ );
BrowserTestUtils.removeTab(tab1);
BrowserTestUtils.removeTab(tab2);
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_footer.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_footer.js
@@ -42,9 +42,13 @@ async function setupSplitView() {
}
async function activateCommand(panel, command) {
- const footerMenu = panel.querySelector(".split-view-footer-menu");
+ const footerMenu = document.getElementById("split-view-menu");
const promiseShown = BrowserTestUtils.waitForPopupEvent(footerMenu, "shown");
- footerMenu.openPopup();
+ const { menuButtonElement } = panel.querySelector("split-view-footer");
+ // Only the urlbar menu is focusable, not the footer menu.
+ AccessibilityUtils.setEnv({ focusableRule: false });
+ EventUtils.synthesizeMouseAtCenter(menuButtonElement, {});
+ AccessibilityUtils.resetEnv();
await promiseShown;
const item = footerMenu.querySelector(`menuitem[command="${command}"]`);
footerMenu.activateItem(item);
diff --git a/browser/locales/en-US/browser/browser.ftl b/browser/locales/en-US/browser/browser.ftl
@@ -181,6 +181,10 @@ urlbar-star-edit-bookmark =
urlbar-star-add-bookmark =
.tooltiptext = Bookmark this page ({ $shortcut })
+urlbar-split-view-button =
+ .tooltiptext = Split view
+ .aria-label = Split view
+
## Page Action Context Menu
page-action-manage-extension2 =
diff --git a/browser/themes/shared/icons/split-view-left-16.svg b/browser/themes/shared/icons/split-view-left-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+ <svg width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 15h-1.5v-1H2c-1.103 0-2-.897-2-2V4c0-1.103.897-2 2-2h5.25V1h1.5v14zM14 2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3.75v-1.5H14a.5.5 0 0 0 .5-.5V4a.5.5 0 0 0-.5-.5h-3.75V2H14z" /></svg>
+\ No newline at end of file
diff --git a/browser/themes/shared/icons/split-view-right-16.svg b/browser/themes/shared/icons/split-view-right-16.svg
@@ -0,0 +1,4 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+ <svg width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 2H14c1.103 0 2 .897 2 2v8c0 1.103-.897 2-2 2H8.75v1h-1.5V1h1.5v1zM5.75 3.5H2a.5.5 0 0 0-.5.5v8a.5.5 0 0 0 .5.5h3.75V14H2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3.75v1.5z" /></svg>
+\ No newline at end of file
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
@@ -236,6 +236,8 @@
skin/classic/browser/sidebars.svg (../shared/icons/sidebars.svg)
skin/classic/browser/sidebars-right.svg (../shared/icons/sidebars-right.svg)
skin/classic/browser/sort.svg (../shared/icons/sort.svg)
+ skin/classic/browser/split-view-left-16.svg (../shared/icons/split-view-left-16.svg)
+ skin/classic/browser/split-view-right-16.svg (../shared/icons/split-view-right-16.svg)
skin/classic/browser/stop-to-reload.svg (../shared/icons/stop-to-reload.svg)
skin/classic/browser/subtract-circle-fill.svg (../shared/icons/subtract-circle-fill.svg)
skin/classic/browser/success-animation.svg (../shared/icons/success-animation.svg)
diff --git a/browser/themes/shared/urlbar-searchbar.css b/browser/themes/shared/urlbar-searchbar.css
@@ -48,7 +48,7 @@
#urlbar:not([actiontype="switchtab"]) > .urlbar-input-container > #urlbar-label-box > #urlbar-label-switchtab,
#urlbar:not([actiontype="extension"]) > .urlbar-input-container > #urlbar-label-box > #urlbar-label-extension,
.urlbar-input-container[pageproxystate="invalid"] > #page-action-buttons > .urlbar-page-action,
-#identity-box.chromeUI ~ #page-action-buttons > .urlbar-page-action:not(#star-button-box),
+#identity-box.chromeUI ~ #page-action-buttons > .urlbar-page-action:not(#star-button-box, #split-view-button),
#urlbar[usertyping] > .urlbar-input-container > #page-action-buttons > #urlbar-zoom-button,
.urlbar:is(:not([usertyping]), :not([focused])) > .urlbar-input-container > .urlbar-go-button,
.urlbar-revert-button-container {
@@ -535,6 +535,24 @@
height: 16px;
}
+#split-view-button {
+ &[data-active-index="0"] > #split-view-button-icon {
+ list-style-image: url("chrome://browser/skin/split-view-left-16.svg");
+ }
+ &[data-active-index="1"] > #split-view-button-icon {
+ list-style-image: url("chrome://browser/skin/split-view-right-16.svg");
+ }
+
+ &:dir(rtl) {
+ &[data-active-index="0"] > #split-view-button-icon {
+ list-style-image: url("chrome://browser/skin/split-view-right-16.svg");
+ }
+ &[data-active-index="1"] > #split-view-button-icon {
+ list-style-image: url("chrome://browser/skin/split-view-left-16.svg");
+ }
+ }
+}
+
#pageAction-panel-bookmark,
#star-button {
list-style-image: url("chrome://browser/skin/bookmark-hollow.svg");