tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbrowser/base/content/browser-box.inc.xhtml | 5+++++
Mbrowser/components/tabbrowser/content/split-view-footer.js | 11+++++++++++
Mbrowser/components/tabbrowser/content/tabbrowser.js | 15+++++++++++++++
Mbrowser/components/tabbrowser/content/tabsplitview.js | 10++++++++++
Mbrowser/components/tabbrowser/test/browser/tabs/browser.toml | 2++
Mbrowser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js | 102+------------------------------------------------------------------------------
Abrowser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_footer.js | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/locales/en-US/browser/tabbrowser.ftl | 11+++++++++++
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 + ##