tor-browser

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

commit 9d81115cc1a202769a482c841f008e88ef8ab39d
parent 0a4af333097db749a73108dcbfdbe062aa1cc2b9
Author: Kelly Cochrane <kcochrane@mozilla.com>
Date:   Thu,  8 Jan 2026 15:41:02 +0000

Bug 2002656 - Add container indicators to about:opentabs page r=tabbrowser-reviewers,nsharpley

Differential Revision: https://phabricator.services.mozilla.com/D277876

Diffstat:
Mbrowser/components/sidebar/sidebar-tab-list.css | 1+
Mbrowser/components/sidebar/sidebar-tab-list.mjs | 32+++++++++++++++++++++++++++++++-
Mbrowser/components/sidebar/sidebar-tab-row.css | 10++++++++++
Mbrowser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_about_opentabs.js | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 182 insertions(+), 1 deletion(-)

diff --git a/browser/components/sidebar/sidebar-tab-list.css b/browser/components/sidebar/sidebar-tab-list.css @@ -5,6 +5,7 @@ sidebar-tab-row { border-radius: var(--border-radius-medium); height: var(--size-item-large); + align-items: center; &:hover { background-color: var(--button-background-color-ghost-hover); diff --git a/browser/components/sidebar/sidebar-tab-list.mjs b/browser/components/sidebar/sidebar-tab-list.mjs @@ -180,6 +180,7 @@ export class SidebarTabList extends FxviewTabListBase { compact .currentActiveElementId=${this.currentActiveElementId} .closeRequested=${tabItem.closeRequested} + .containerObj=${tabItem.containerObj} .fxaDeviceId=${ifDefined(tabItem.fxaDeviceId)} .favicon=${tabItem.icon} .guid=${tabItem.guid} @@ -220,6 +221,7 @@ customElements.define("sidebar-tab-list", SidebarTabList); export class SidebarTabRow extends FxviewTabRowBase { static properties = { + containerObj: { type: Object }, guid: { type: String }, selected: { type: Boolean, reflect: true }, indicators: { type: Array }, @@ -233,6 +235,25 @@ export class SidebarTabRow extends FxviewTabRowBase { HTMLElement.prototype.focus.call(this); } + #getContainerClasses() { + let containerClasses = ["fxview-tab-row-container-indicator", "icon"]; + if (this.containerObj) { + let { icon, color } = this.containerObj; + containerClasses.push(`identity-icon-${icon}`); + containerClasses.push(`identity-color-${color}`); + } + return containerClasses; + } + + #containerIndicatorTemplate() { + let tabList = this.getRootNode().host; + let tabsToCheck = tabList.tabItems; + return html`${when( + tabsToCheck.some(tab => tab.containerObj), + () => html`<span class=${this.#getContainerClasses().join(" ")}></span>` + )}`; + } + secondaryButtonTemplate() { return html`${when( this.secondaryL10nId && this.secondaryActionClass, @@ -256,6 +277,15 @@ export class SidebarTabRow extends FxviewTabRowBase { render() { return html` ${this.stylesheets()} + ${when( + this.containerObj, + () => html` + <link + rel="stylesheet" + href="chrome://browser/content/usercontext/usercontext.css" + /> + ` + )} <link rel="stylesheet" href="chrome://browser/content/sidebar/sidebar-tab-row.css" @@ -283,7 +313,7 @@ export class SidebarTabRow extends FxviewTabRowBase { > ${this.faviconTemplate()} ${this.titleTemplate()} </a> - ${this.secondaryButtonTemplate()} + ${this.secondaryButtonTemplate()} ${this.#containerIndicatorTemplate()} `; } } diff --git a/browser/components/sidebar/sidebar-tab-row.css b/browser/components/sidebar/sidebar-tab-row.css @@ -99,3 +99,13 @@ .fxview-tab-row-main.activemedia-blocked .fxview-tab-row-favicon::after { background-image: url("chrome://browser/skin/tabbrowser/tab-audio-blocked-small.svg"); } + +.fxview-tab-row-container-indicator { + height: var(--size-item-small); + width: var(--size-item-small); + background-image: var(--identity-icon); + /* stylelint-disable-next-line stylelint-plugin-mozilla/use-size-tokens */ + background-size: cover; + -moz-context-properties: fill; + fill: var(--identity-icon-color); +} diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_about_opentabs.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview_about_opentabs.js @@ -209,3 +209,143 @@ add_task(async function test_contextMenuMoveTabsToNewSplitView() { BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); } }); + +add_task(async function test_containerIndicators() { + const tab1 = await addTab(); + const tab2 = await addTab(); + + // Load a page in a container tab + let userContextId = 1; + let containerTab = BrowserTestUtils.addTab( + gBrowser, + "http://mochi.test:8888/", + { + userContextId, + } + ); + + await BrowserTestUtils.browserLoaded( + containerTab.linkedBrowser, + false, + "http://mochi.test:8888/" + ); + + // Click the first tab in our test split view to make sure the default tab at the + // start of the tab strip is deselected + EventUtils.synthesizeMouseAtCenter(tab1, {}); + + // Test adding split view with one tab and new tab + + let tabToClick = tab1; + EventUtils.synthesizeMouseAtCenter(tab1, {}); + let openTabsPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "about:opentabs" + ); + let tabContainer = document.getElementById("tabbrowser-tabs"); + let splitViewCreated = BrowserTestUtils.waitForEvent( + tabContainer, + "SplitViewCreated" + ); + await withTabMenu(tabToClick, async moveTabToNewSplitViewItem => { + await BrowserTestUtils.waitForMutationCondition( + moveTabToNewSplitViewItem, + { attributes: true }, + () => + !moveTabToNewSplitViewItem.hidden && + !moveTabToNewSplitViewItem.disabled, + "moveTabToNewSplitViewItem is visible and not disabled" + ); + Assert.ok( + !moveTabToNewSplitViewItem.hidden && !moveTabToNewSplitViewItem.disabled, + "moveTabToNewSplitViewItem is visible and not disabled" + ); + + info("Click menu option to add new split view"); + moveTabToNewSplitViewItem.click(); + await splitViewCreated; + await openTabsPromise; + info("about:opentabs has been opened"); + Assert.equal( + gBrowser.selectedTab.linkedBrowser.currentURI.spec, + "about:opentabs", + "about:opentabs is active in split view" + ); + }); + + let splitview = tab1.splitview; + + Assert.equal(tab1.splitview, splitview, `tab1 is in split view`); + let aboutOpenTabsDocument = + gBrowser.selectedTab.linkedBrowser.contentDocument; + let openTabsComponent = await TestUtils.waitForCondition( + () => aboutOpenTabsDocument.querySelector("splitview-opentabs"), + "Open tabs component rendered" + ); + await TestUtils.waitForCondition( + () => openTabsComponent.nonSplitViewUnpinnedTabs?.length, + "Open tabs component has rendered items" + ); + + Assert.equal( + openTabsComponent.nonSplitViewUnpinnedTabs.length, + 3, + "3 tabs are shown in the open tabs list" + ); + + await TestUtils.waitForCondition( + () => openTabsComponent.sidebarTabList.shadowRoot, + "Open tabs component has shadowRoot" + ); + await openTabsComponent.sidebarTabList.updateComplete; + await BrowserTestUtils.waitForMutationCondition( + openTabsComponent.sidebarTabList.shadowRoot, + { childList: true, subtree: true }, + () => openTabsComponent.sidebarTabList.rowEls.length === 3, + "Tabs are shown in the open tabs list" + ); + + Assert.ok( + openTabsComponent.sidebarTabList.rowEls[1].__url === + tab2.linkedBrowser.currentURI.spec && + openTabsComponent.sidebarTabList.rowEls[2].__url === + containerTab.linkedBrowser.currentURI.spec, + "tab2 and tab3 are listed on the about:opentabs page" + ); + + await TestUtils.waitForCondition( + () => + containerTab.getAttribute("usercontextid") === userContextId.toString(), + "The container tab doesn't have the usercontextid attribute." + ); + + let containerTabElem; + + await TestUtils.waitForCondition( + () => + Array.from(openTabsComponent.sidebarTabList.rowEls).some(rowEl => { + let hasContainerObj; + if (rowEl.containerObj?.icon) { + containerTabElem = rowEl; + hasContainerObj = rowEl.containerObj; + } + + return hasContainerObj; + }), + "The container tab element isn't marked in about:opentabs." + ); + + Assert.ok( + containerTabElem.shadowRoot + .querySelector(".fxview-tab-row-container-indicator") + .classList.contains("identity-color-blue"), + "The container color is blue." + ); + + info("The open tab is marked as a container tab."); + + splitview.unsplitTabs(); + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs.at(-1)); + } +});