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:
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));
+ }
+});