tor-browser

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

commit da8b29b1ad4ecf62939e62a8fce8ca61bcab5d74
parent b652076c527679b102ff06d4926b2277c263ec43
Author: Jonathan Sudiaman <jsudiaman@mozilla.com>
Date:   Wed, 29 Oct 2025 14:00:05 +0000

Bug 1986950 - Implement footer for inactive tab/content area r=tabbrowser-reviewers,desktop-theme-reviewers,sclements,dao

The inactive view includes a small footer that displays the favicon and domain name of the site.

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

Diffstat:
Mbrowser/base/content/browser-main.js | 1+
Abrowser/components/tabbrowser/content/split-view-footer.js | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/tabbrowser/content/tabbrowser.js | 25+++++++++++++++++++++----
Mbrowser/components/tabbrowser/jar.mn | 1+
Mbrowser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js | 112++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mbrowser/themes/shared/tabbrowser/content-area.css | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mtoolkit/content/widgets/tabbox.js | 3+++
7 files changed, 398 insertions(+), 17 deletions(-)

diff --git a/browser/base/content/browser-main.js b/browser/base/content/browser-main.js @@ -21,6 +21,7 @@ Services.scriptloader.loadSubScript("chrome://browser/content/browser-unified-extensions.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/drag-and-drop.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tab-stacking.js", this); + Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/split-view-footer.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tab.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabbrowser.js", this); Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabgroup.js", this); diff --git a/browser/components/tabbrowser/content/split-view-footer.js b/browser/components/tabbrowser/content/split-view-footer.js @@ -0,0 +1,212 @@ +/* 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/. */ + +"use strict"; + +// This is loaded into chrome windows with the subscript loader. Wrap in +// a block to prevent accidentally leaking globals onto `window`. +{ + ChromeUtils.defineESModuleGetters(this, { + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", + }); + + /** + * A footer which appears in the corner of the inactive panel in split view. + * + * The footer displays the favicon and domain name of the site. + */ + class MozSplitViewFooter extends MozXULElement { + #initialized = false; + + #isInsecure = false; + /** @type {HTMLSpanElement} */ + securityElement = null; + + /** @type {HTMLImageElement} */ + iconElement = null; + #iconSrc = ""; + + /** @type {HTMLSpanElement} */ + uriElement = null; + /** @type {nsIURI} */ + #uri = null; + + #browserProgressListener = { + QueryInterface: ChromeUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference, + ]), + onLocationChange: (aWebProgress, aRequest, aLocation) => { + if (aWebProgress?.isTopLevel && aLocation) { + this.#updateUri(aLocation); + } + }, + onSecurityChange: (aWebProgress, aRequest, aState) => + this.#toggleInsecure( + !!( + aState & Ci.nsIWebProgressListener.STATE_IS_INSECURE || + aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN + ) + ), + }; + + /** @type {XULElement} */ + #tab = null; + + static markup = ` + <hbox class="split-view-security-warning" hidden=""> + <html:img role="presentation" src="chrome://global/skin/icons/security-broken.svg" /> + <html:span data-l10n-id="urlbar-trust-icon-notsecure-label"></html:span> + </hbox> + <html:img class="split-view-icon" hidden="" role="presentation"/> + <html:span class="split-view-uri"></html:span> + `; + + connectedCallback() { + if (this.#initialized) { + return; + } + this.appendChild(this.constructor.fragment); + + this.securityElement = this.querySelector(".split-view-security-warning"); + this.iconElement = this.querySelector(".split-view-icon"); + this.uriElement = this.querySelector(".split-view-uri"); + + // Ensure these elements are up-to-date, as this info may have been set + // prior to inserting this element into the DOM. + this.#updateSecurityElement(); + this.#updateIconElement(); + this.#updateUriElement(); + + this.#initialized = true; + } + + disconnectedCallback() { + this.#resetTab(); + } + + handleEvent(e) { + switch (e.type) { + case "TabAttrModified": + for (const attribute of e.detail.changed) { + this.#handleTabAttrModified(attribute); + } + break; + } + } + + #handleTabAttrModified(attribute) { + switch (attribute) { + case "image": + this.#updateIconSrc(this.#tab.image); + break; + } + } + + /** + * Update the insecure flag and refresh the security warning visibility. + * + * @param {boolean} isInsecure + */ + #toggleInsecure(isInsecure) { + this.#isInsecure = isInsecure; + if (this.securityElement) { + this.#updateSecurityElement(); + } + } + + #updateSecurityElement() { + const isWebsite = + this.#uri.schemeIs("http") || this.#uri.schemeIs("https"); + this.securityElement.hidden = !isWebsite || !this.#isInsecure; + } + + /** + * Update the footer icon to the given source URI. + * + * @param {string} iconSrc + */ + #updateIconSrc(iconSrc) { + this.#iconSrc = iconSrc; + if (this.iconElement) { + this.#updateIconElement(); + } + } + + #updateIconElement() { + if (this.#iconSrc) { + this.iconElement.setAttribute("src", this.#iconSrc); + } else { + this.iconElement.removeAttribute("src"); + } + this.iconElement.hidden = !this.#iconSrc; + } + + /** + * Update the footer URI display with the formatted domain string. + * + * @param {nsIURI} uri + */ + #updateUri(uri) { + this.#uri = uri; + if (this.uriElement) { + this.#updateUriElement(); + } + if (this.securityElement) { + this.#updateSecurityElement(); + } + } + + #updateUriElement() { + const uriString = this.#uri + ? BrowserUtils.formatURIForDisplay(this.#uri) + : ""; + this.uriElement.textContent = uriString; + } + + /** + * Link the footer to the provided tab so it stays in sync. + * + * @param {MozTabbrowserTab} tab + */ + setTab(tab) { + this.#resetTab(); + + // Track favicon changes. + this.#updateIconSrc(tab.image); + tab.addEventListener("TabAttrModified", this); + + // Track URI and security changes. + this.#updateUri(tab.linkedBrowser.currentURI); + const securityState = tab.linkedBrowser.securityUI.state; + this.#toggleInsecure( + !!( + securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE || + securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN + ) + ); + tab.linkedBrowser.addProgressListener( + this.#browserProgressListener, + Ci.nsIWebProgress.NOTIFY_LOCATION | Ci.nsIWebProgress.NOTIFY_SECURITY + ); + + this.#tab = tab; + } + + /** + * Remove the footer's association with the current tab. + */ + #resetTab() { + if (this.#tab) { + this.#tab.removeEventListener("TabAttrModified", this); + this.#tab.linkedBrowser?.removeProgressListener( + this.#browserProgressListener + ); + } + this.#tab = null; + } + } + + customElements.define("split-view-footer", MozSplitViewFooter); +} diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js @@ -3255,6 +3255,7 @@ const panels = []; for (const tab of tabs) { this._insertBrowser(tab); + this.#insertSplitViewFooter(tab); tab.linkedBrowser.docShellIsActive = true; panels.push(tab.linkedPanel); } @@ -3273,6 +3274,21 @@ } /** + * Ensures the split view footer exists for the given tab. + * + * @param {MozTabbrowserTab} tab + */ + #insertSplitViewFooter(tab) { + const panelEl = document.getElementById(tab.linkedPanel); + if (panelEl.querySelector("split-view-footer")) { + return; + } + const footer = document.createXULElement("split-view-footer"); + footer.setTab(tab); + panelEl.appendChild(footer); + } + + /** * @param {string} id * @param {string} color * @param {boolean} collapsed @@ -8586,10 +8602,6 @@ modifiedAttrs.push("progress"); } - if (modifiedAttrs.length) { - gBrowser._tabAttrModified(this.mTab, modifiedAttrs); - } - if (aWebProgress.isTopLevel) { let isSuccessful = Components.isSuccessCode(aStatus); if (!isSuccessful && !this.mTab.isEmpty) { @@ -8634,6 +8646,7 @@ !(originalLocation.spec in FAVICON_DEFAULTS) ) { this.mTab.removeAttribute("image"); + modifiedAttrs.push("image"); } else { // Bug 1804166: Allow new tabs to set the favicon correctly if the // new tabs behavior is set to open a blank page @@ -8650,6 +8663,10 @@ if (this.mTab.selected) { gBrowser._isBusy = false; } + + if (modifiedAttrs.length) { + gBrowser._tabAttrModified(this.mTab, modifiedAttrs); + } } if (ignoreBlank) { diff --git a/browser/components/tabbrowser/jar.mn b/browser/components/tabbrowser/jar.mn @@ -8,6 +8,7 @@ browser.jar: content/browser/tabbrowser/browser-fullZoom.js (content/browser-fullZoom.js) content/browser/tabbrowser/drag-and-drop.js (content/drag-and-drop.js) content/browser/tabbrowser/tab-stacking.js (content/tab-stacking.js) + content/browser/tabbrowser/split-view-footer.js (content/split-view-footer.js) content/browser/tabbrowser/tab.js (content/tab.js) content/browser/tabbrowser/tab-hover-preview.mjs (content/tab-hover-preview.mjs) content/browser/tabbrowser/tabbrowser.js (content/tabbrowser.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,17 +4,18 @@ add_setup(async function () { await SpecialPowers.pushPrefEnv({ - set: [["sidebar.verticalTabs", true]], + set: [ + ["sidebar.verticalTabs", true], + ["dom.security.https_first", false], + ], }); }); registerCleanupFunction(async function () { - await SpecialPowers.pushPrefEnv({ - set: [ - ["sidebar.verticalTabs", false], - ["sidebar.revamp", false], - ], - }); + Services.prefs.clearUserPref("sidebar.revamp"); + Services.prefs.clearUserPref( + "browser.toolbarbuttons.introduced.sidebar-button" + ); }); async function addTabAndLoadBrowser() { @@ -239,3 +240,100 @@ 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/themes/shared/tabbrowser/content-area.css b/browser/themes/shared/tabbrowser/content-area.css @@ -155,7 +155,7 @@ --panel-min-width: 140px; .split-view-panel { - position: static; + position: relative; flex: 1; min-width: var(--panel-min-width); max-width: calc(100% - var(--panel-min-width)); @@ -165,6 +165,7 @@ .browserStack { border-radius: var(--border-radius-medium); overflow: clip; + outline: var(--border-width) solid var(--border-color); .deck-selected > .browserContainer > & { outline: var(--focus-outline); @@ -190,9 +191,51 @@ .split-view-panel[width] { flex: none; } + + /* Display split view footer within inactive panels. */ + .split-view-panel:not(.deck-selected) > split-view-footer { + display: inherit; + } } } +/* Split view */ + +split-view-footer { + max-width: calc(100% - var(--space-xsmall)); + display: none; + position: absolute; + inset-block-end: 0; + inset-inline-end: 0; + + border-block-start: 1px solid var(--border-color); + border-inline-start: 1px solid var(--border-color); + border-start-start-radius: var(--border-radius-medium); + border-start-end-radius: 0; + border-end-end-radius: var(--border-radius-medium); + border-end-start-radius: 0; + + padding-block: var(--space-xxsmall); + padding-inline: var(--space-small) var(--space-xsmall); +} + +.split-view-icon { + width: var(--icon-size); + height: var(--icon-size); +} + +.split-view-uri { + overflow: hidden; + text-overflow: ellipsis; +} + +split-view-footer, +.split-view-security-warning { + align-items: center; + gap: var(--space-small); + white-space: nowrap; +} + /* Status panel */ #statuspanel { @@ -252,13 +295,11 @@ } } -#statuspanel-label { +#statuspanel-label, +split-view-footer { color-scheme: env(-moz-content-preferred-color-scheme); - margin: 0; - padding: 2px 4px; background-color: -moz-dialog; - border: 1px none ThreeDShadow; - border-top-style: solid; + border-color: ThreeDShadow; color: -moz-dialogText; text-shadow: none; @@ -267,6 +308,14 @@ border-color: light-dark(#ddd, hsl(240, 1%, 40%)); color: light-dark(#444, rgb(249, 249, 250)); } +} + +#statuspanel-label { + margin: 0; + padding: 2px 4px; + border-width: 1px; + border-style: none; + border-top-style: solid; #statuspanel:not([mirror]) > &:-moz-locale-dir(ltr), #statuspanel[mirror] > &:-moz-locale-dir(rtl) { diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.js @@ -277,6 +277,7 @@ handleEvent(e) { switch (e.type) { + case "click": case "focus": { const browser = e.currentTarget; const tab = browser.getTabBrowser().getTabForBrowser(browser); @@ -361,6 +362,7 @@ const panelEl = document.getElementById(panel); panelEl?.classList.add("split-view-panel"); panelEl?.setAttribute("column", i); + panelEl?.querySelector("browser")?.addEventListener("click", this); panelEl?.querySelector("browser")?.addEventListener("focus", this); } this.#splitViewPanels = newPanels; @@ -382,6 +384,7 @@ const panelEl = document.getElementById(panel); panelEl?.classList.remove("split-view-panel"); panelEl?.removeAttribute("column"); + panelEl?.querySelector("browser")?.removeEventListener("click", this); panelEl?.querySelector("browser")?.removeEventListener("focus", this); if (updateArray) { const index = this.#splitViewPanels.indexOf(panel);