commit 4e6cad19475fbd99bd6c10efa73db9dac22b2ebc
parent da6eccc59d645ffb11fe1dfada6e48af6dc65977
Author: Jonathan Sudiaman <jsudiaman@mozilla.com>
Date: Fri, 14 Nov 2025 21:28:05 +0000
Bug 1986951 - Add 3-dot menu and APIs for menu options for domain footer r=tabbrowser-reviewers,fluent-reviewers,bolsson,nsharpley
Add a command set to handle split view commands. Add a menu to split-view-footer which utilizes the command set.
Differential Revision: https://phabricator.services.mozilla.com/D272484
Diffstat:
8 files changed, 242 insertions(+), 101 deletions(-)
diff --git a/browser/base/content/browser-box.inc.xhtml b/browser/base/content/browser-box.inc.xhtml
@@ -29,3 +29,8 @@
<tabpanels id="tabbrowser-tabpanels" flex="1" selectedIndex="0"/>
</tabbox>
</hbox>
+<commandset id="splitViewCommands">
+ <command id="splitViewCmd_separateTabs"/>
+ <command id="splitViewCmd_reverseTabs"/>
+ <command id="splitViewCmd_closeTabs"/>
+</commandset>
diff --git a/browser/components/tabbrowser/content/split-view-footer.js b/browser/components/tabbrowser/content/split-view-footer.js
@@ -61,6 +61,17 @@
</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>
`;
connectedCallback() {
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
@@ -99,6 +99,7 @@
this.pinnedTabsContainer = document.getElementById(
"pinned-tabs-container"
);
+ this.splitViewCommandSet = document.getElementById("splitViewCommands");
ChromeUtils.defineESModuleGetters(this, {
AsyncTabSwitcher:
@@ -8290,6 +8291,20 @@
const { url, description, previewImageURL } = event.detail;
this.setPageInfo(tab, url, description, previewImageURL);
});
+
+ this.splitViewCommandSet.addEventListener("command", event => {
+ switch (event.target.id) {
+ case "splitViewCmd_separateTabs":
+ this.#activeSplitView.unsplitTabs();
+ break;
+ case "splitViewCmd_reverseTabs":
+ this.#activeSplitView.reverseTabs();
+ break;
+ case "splitViewCmd_closeTabs":
+ this.#activeSplitView.close();
+ break;
+ }
+ });
}
translateTabContextMenu() {
diff --git a/browser/components/tabbrowser/content/tabsplitview.js b/browser/components/tabbrowser/content/tabsplitview.js
@@ -159,6 +159,16 @@
}
/**
+ * Reverse order of the tabs in the split view wrapper.
+ */
+ reverseTabs() {
+ const [firstTab, secondTab] = this.#tabs;
+ gBrowser.moveTabBefore(secondTab, firstTab);
+ this.#tabs = [secondTab, firstTab];
+ gBrowser.showSplitViewPanels(this.#tabs);
+ }
+
+ /**
* Close all tabs in the split view wrapper and delete the split view.
*/
close() {
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser.toml b/browser/components/tabbrowser/test/browser/tabs/browser.toml
@@ -594,6 +594,8 @@ skip-if = [
"os == 'mac' && os_version == '10.15' && processor == 'x86_64'", # Bug 1994508
]
+["browser_tab_splitview_footer.js"]
+
["browser_tab_splitview_keyboard_focus.js"]
["browser_tab_tooltips.js"]
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js
@@ -4,10 +4,7 @@
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
- set: [
- ["sidebar.verticalTabs", true],
- ["dom.security.https_first", false],
- ],
+ set: [["sidebar.verticalTabs", true]],
});
});
@@ -240,100 +237,3 @@ add_task(async function test_resize_split_view_panels() {
splitView.close();
});
-
-add_task(async function test_split_view_panel_footers() {
- const tab1 = await addTabAndLoadBrowser();
- const tab2 = await addTabAndLoadBrowser();
- await BrowserTestUtils.switchTab(gBrowser, tab1);
-
- info("Activate split view.");
- const splitView = gBrowser.addTabSplitView([tab1, tab2]);
- await checkSplitViewPanelVisible(tab1, true);
- await checkSplitViewPanelVisible(tab2, true);
-
- const panel1 = document.getElementById(tab1.linkedPanel);
- const panel2 = document.getElementById(tab2.linkedPanel);
- const panel1Footer = panel1.querySelector("split-view-footer");
- const panel2Footer = panel2.querySelector("split-view-footer");
-
- info("Focus the first panel.");
- await SimpleTest.promiseFocus(tab1.linkedBrowser);
- Assert.ok(
- BrowserTestUtils.isHidden(panel1Footer),
- "First (active) panel does not contain a footer."
- );
- Assert.ok(
- BrowserTestUtils.isVisible(panel2Footer),
- "Second (inactive) panel contains a footer."
- );
- Assert.equal(
- panel2Footer.uriElement.textContent,
- "example.com",
- "Footer displays the domain name of the site."
- );
-
- info("Focus the second panel.");
- await SimpleTest.promiseFocus(tab2.linkedBrowser);
- Assert.ok(
- BrowserTestUtils.isVisible(panel1Footer),
- "First panel now contains a footer."
- );
- Assert.ok(
- BrowserTestUtils.isHidden(panel2Footer),
- "Second panel no longer contains a footer."
- );
-
- info("Navigate to a different location.");
- const promiseLoaded = BrowserTestUtils.browserLoaded(tab1.linkedBrowser);
- BrowserTestUtils.startLoadingURIString(tab1.linkedBrowser, "about:robots");
- await promiseLoaded;
- Assert.equal(
- panel1Footer.uriElement.textContent,
- "about:robots",
- "Footer displays the new location."
- );
-
- splitView.close();
-});
-
-add_task(async function test_split_view_security_warning() {
- const tab1 = await addTabAndLoadBrowser();
- const tab2 = await addTabAndLoadBrowser();
- await BrowserTestUtils.switchTab(gBrowser, tab1);
-
- info("Activate split view.");
- const splitView = gBrowser.addTabSplitView([tab1, tab2]);
- await checkSplitViewPanelVisible(tab1, true);
- await checkSplitViewPanelVisible(tab2, true);
- await SimpleTest.promiseFocus(tab1.linkedBrowser);
-
- const inactivePanel = document.getElementById(tab2.linkedPanel);
- const footer = inactivePanel.querySelector("split-view-footer");
- Assert.ok(
- BrowserTestUtils.isHidden(footer.securityElement),
- "No security warning for HTTPS."
- );
-
- info("Load an insecure website.");
- let promiseLoaded = BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
- BrowserTestUtils.startLoadingURIString(
- tab2.linkedBrowser,
- "http://example.com/" // eslint-disable-line @microsoft/sdl/no-insecure-url
- );
- await promiseLoaded;
- Assert.ok(
- BrowserTestUtils.isVisible(footer.securityElement),
- "Security warning for HTTP."
- );
-
- info("Load a local site.");
- promiseLoaded = BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
- BrowserTestUtils.startLoadingURIString(tab2.linkedBrowser, "about:robots");
- await promiseLoaded;
- Assert.ok(
- BrowserTestUtils.isHidden(footer.securityElement),
- "No security warning for local sites."
- );
-
- splitView.close();
-});
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
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.security.https_first", false]],
+ });
+});
+
+async function setupSplitView() {
+ info("Create new tabs.");
+ const tabs = [
+ BrowserTestUtils.addTab(gBrowser, "https://example.com"),
+ BrowserTestUtils.addTab(gBrowser, "https://example.com"),
+ ];
+ await Promise.all(
+ tabs.map(({ linkedBrowser }) =>
+ BrowserTestUtils.browserLoaded(linkedBrowser)
+ )
+ );
+
+ info("Add tabs into an active split view.");
+ await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
+ const splitView = gBrowser.addTabSplitView(tabs);
+ for (const tab of tabs) {
+ const tabPanel = document.getElementById(tab.linkedPanel);
+ await BrowserTestUtils.waitForMutationCondition(
+ tabPanel,
+ { attributes: true },
+ () => tabPanel.classList.contains("split-view-panel")
+ );
+ }
+
+ return { tabs, splitView };
+}
+
+async function activateCommand(panel, command) {
+ const footerMenu = panel.querySelector(".split-view-footer-menu");
+ const promiseShown = BrowserTestUtils.waitForPopupEvent(footerMenu, "shown");
+ footerMenu.openPopup();
+ await promiseShown;
+ const item = footerMenu.querySelector(`menuitem[command="${command}"]`);
+ footerMenu.activateItem(item);
+}
+
+add_task(async function test_displayed_over_inactive_panel() {
+ const { tabs, splitView } = await setupSplitView();
+ const [tab1, tab2] = tabs;
+
+ const panel1 = document.getElementById(tab1.linkedPanel);
+ const panel2 = document.getElementById(tab2.linkedPanel);
+ const panel1Footer = panel1.querySelector("split-view-footer");
+ const panel2Footer = panel2.querySelector("split-view-footer");
+
+ info("Focus the first panel.");
+ await SimpleTest.promiseFocus(tab1.linkedBrowser);
+ Assert.ok(
+ BrowserTestUtils.isHidden(panel1Footer),
+ "First (active) panel does not contain a footer."
+ );
+ Assert.ok(
+ BrowserTestUtils.isVisible(panel2Footer),
+ "Second (inactive) panel contains a footer."
+ );
+ Assert.equal(
+ panel2Footer.uriElement.textContent,
+ "example.com",
+ "Footer displays the domain name of the site."
+ );
+
+ info("Focus the second panel.");
+ await SimpleTest.promiseFocus(tab2.linkedBrowser);
+ Assert.ok(
+ BrowserTestUtils.isVisible(panel1Footer),
+ "First panel now contains a footer."
+ );
+ Assert.ok(
+ BrowserTestUtils.isHidden(panel2Footer),
+ "Second panel no longer contains a footer."
+ );
+
+ info("Navigate to a different location.");
+ const promiseLoaded = BrowserTestUtils.browserLoaded(tab1.linkedBrowser);
+ BrowserTestUtils.startLoadingURIString(tab1.linkedBrowser, "about:robots");
+ await promiseLoaded;
+ Assert.equal(
+ panel1Footer.uriElement.textContent,
+ "about:robots",
+ "Footer displays the new location."
+ );
+
+ splitView.close();
+});
+
+add_task(async function test_security_warning() {
+ const { tabs, splitView } = await setupSplitView();
+ const [tab1, tab2] = tabs;
+ await SimpleTest.promiseFocus(tab1.linkedBrowser);
+
+ const inactivePanel = document.getElementById(tab2.linkedPanel);
+ const footer = inactivePanel.querySelector("split-view-footer");
+ Assert.ok(
+ BrowserTestUtils.isHidden(footer.securityElement),
+ "No security warning for HTTPS."
+ );
+
+ info("Load an insecure website.");
+ let promiseLoaded = BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
+ BrowserTestUtils.startLoadingURIString(
+ tab2.linkedBrowser,
+ "http://example.com/" // eslint-disable-line @microsoft/sdl/no-insecure-url
+ );
+ await promiseLoaded;
+ Assert.ok(
+ BrowserTestUtils.isVisible(footer.securityElement),
+ "Security warning for HTTP."
+ );
+
+ info("Load a local site.");
+ promiseLoaded = BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
+ BrowserTestUtils.startLoadingURIString(tab2.linkedBrowser, "about:robots");
+ await promiseLoaded;
+ Assert.ok(
+ BrowserTestUtils.isHidden(footer.securityElement),
+ "No security warning for local sites."
+ );
+
+ splitView.close();
+});
+
+add_task(async function test_menu_separate_tabs() {
+ const { tabs, splitView } = await setupSplitView();
+ const [tab1, tab2] = tabs;
+ await SimpleTest.promiseFocus(tab1.linkedBrowser);
+ const inactivePanel = document.getElementById(tab2.linkedPanel);
+
+ info("Separate split view tabs.");
+ await activateCommand(inactivePanel, "splitViewCmd_separateTabs");
+ await BrowserTestUtils.waitForMutationCondition(
+ document.getElementById("tabbrowser-tabs"),
+ { childList: true, subtree: true },
+ () => !splitView.isConnected
+ );
+
+ for (const tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function test_menu_reverse_tabs() {
+ const { tabs, splitView } = await setupSplitView();
+ await SimpleTest.promiseFocus(tabs[0].linkedBrowser);
+ const [panel1, panel2] = tabs.map(({ linkedPanel }) =>
+ document.getElementById(linkedPanel)
+ );
+
+ info("Reverse split view tabs.");
+ await activateCommand(panel2, "splitViewCmd_reverseTabs");
+ await BrowserTestUtils.waitForMutationCondition(
+ panel1,
+ { attributes: true },
+ () => panel1.getAttribute("column") == 1
+ );
+ await BrowserTestUtils.waitForMutationCondition(
+ panel2,
+ { attributes: true },
+ () => panel2.getAttribute("column") == 0
+ );
+
+ splitView.close();
+});
+
+add_task(async function test_menu_close_tabs() {
+ const { tabs } = await setupSplitView();
+ const [tab1, tab2] = tabs;
+ await SimpleTest.promiseFocus(tab1.linkedBrowser);
+ const inactivePanel = document.getElementById(tab2.linkedPanel);
+
+ info("Close split view tabs.");
+ const promiseTabsClosed = Promise.all(
+ tabs.map(t => BrowserTestUtils.waitForTabClosing(t))
+ );
+ await activateCommand(inactivePanel, "splitViewCmd_closeTabs");
+ await promiseTabsClosed;
+});
diff --git a/browser/locales/en-US/browser/tabbrowser.ftl b/browser/locales/en-US/browser/tabbrowser.ftl
@@ -386,4 +386,15 @@ tab-context-separate-split-view =
.accesskey = t
tab-context-badge-new = New
+## Manage Split View (icon in the address bar & three-dot menu in the footer)
+
+# "Separate" is a verb, as in "separate the split view tabs and display them normally".
+split-view-menuitem-separate-tabs =
+ .label = Separate Tabs
+# "Reverse" is a verb, as in "reverse the order of split view tabs".
+split-view-menuitem-reverse-tabs =
+ .label = Reverse Tabs
+split-view-menuitem-close-both-tabs =
+ .label = Close Both Tabs
+
##