commit 96eccf5af235e2f592e45fda4e79e6194448fc74
parent fc6f1cb437ff6188b95f248cac91278db55cd0f4
Author: Kelly Cochrane <kcochrane@mozilla.com>
Date: Fri, 12 Dec 2025 14:45:36 +0000
Bug 1997878 - Preserve width of backgrounded split view panels to prevent issues with PiP when switching tabs r=desktop-theme-reviewers,tabbrowser-reviewers,jules,jsudiaman,emilio
Differential Revision: https://phabricator.services.mozilla.com/D275630
Diffstat:
9 files changed, 54 insertions(+), 132 deletions(-)
diff --git a/browser/components/sessionstore/test/marionette/manifest.toml b/browser/components/sessionstore/test/marionette/manifest.toml
@@ -20,8 +20,6 @@ skip-if = [
["test_restore_sidebar_automatic.py"]
-["test_restore_split_view.py"]
-
["test_restore_windows_after_close_last_tabs.py"]
skip-if = [
"os == 'mac' && os_version == '10.15' && arch == 'x86_64'",
diff --git a/browser/components/sessionstore/test/marionette/test_restore_split_view.py b/browser/components/sessionstore/test/marionette/test_restore_split_view.py
@@ -1,97 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 0.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/0.0/.
-
-import os
-import sys
-
-# add this directory to the path
-sys.path.append(os.path.dirname(__file__))
-
-from session_store_test_case import SessionStoreTestCase
-
-
-def inline(title):
- return f"data:text/html;charset=utf-8,<html><head><title>{title}</title></head><body></body></html>"
-
-
-class TestSessionRestoreSplitView(SessionStoreTestCase):
- """
- Test the interactions between Session Restore and Split View.
- """
-
- def setUp(self):
- super().setUp(
- startup_page=1,
- include_private=False,
- restore_on_demand=True,
- test_windows=set(
- [
- (
- inline("Tab 1"),
- inline("Tab 2"),
- inline("Tab 3"),
- ),
- ]
- ),
- )
-
- def test_add_inactive_tabs_to_split_view(self):
- """
- When we restart with some tabs, we defer loading and setting up those
- tabs until they become active.
-
- Ensure that adding these tabs to a split view triggers that.
- """
- self.assertEqual(
- len(self.marionette.chrome_window_handles),
- 1,
- msg="Should have 1 window open.",
- )
-
- # There are currently three tabs open.
- # Switch to the first one, and then restart.
- self.marionette.execute_script("gBrowser.selectTabAtIndex(0)")
- self.marionette.restart()
- self.marionette.set_context("chrome")
-
- # After restart:
- # Tab 1 is active.
- # Tab 2 & Tab 3 are inactive, and haven't been activated yet.
- self.assertEqual(
- self.marionette.execute_script("return gBrowser.tabs.length"),
- 3,
- msg="Should have 3 tabs open.",
- )
- self.assertEqual(
- self.marionette.execute_script(
- "return gBrowser.tabContainer.selectedIndex"
- ),
- 0,
- msg="First tab should be selected.",
- )
-
- # Create a split view with the inactive tabs (Tab 2 & Tab 3).
- # Select Tab 2 to activate the split view.
- # Wait for both tabs in the split view to finish restoring.
- self.marionette.execute_async_script(
- """
- let [resolve] = arguments;
- gBrowser.addTabSplitView([gBrowser.tabs[1], gBrowser.tabs[2]]);
- let promiseTabsRestored = new Promise(resolve => {
- let tabsRemaining = 2;
- function handleTabRestored() {
- if (!--tabsRemaining) {
- gBrowser.tabContainer.removeEventListener(
- "SSTabRestored",
- handleTabRestored
- );
- resolve();
- }
- }
- gBrowser.tabContainer.addEventListener("SSTabRestored", handleTabRestored);
- });
- gBrowser.selectTabAtIndex(1);
- promiseTabsRestored.then(resolve);
- """
- )
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
@@ -3318,6 +3318,19 @@
}
/**
+ * Toggle split view active attribute
+ *
+ * @param {boolean} isActive
+ * @param {MozTabbrowserTab[]} tabs
+ */
+ setIsSplitViewActive(isActive, tabs) {
+ for (const tab of tabs) {
+ this.tabpanels.setSplitViewPanelActive(isActive, tab.linkedPanel);
+ }
+ this.tabpanels.isSplitViewActive = gBrowser.selectedTab.splitview;
+ }
+
+ /**
* Ensures the split view footer exists for the given tab.
*
* @param {MozTabbrowserTab} tab
diff --git a/browser/components/tabbrowser/content/tabsplitview.js b/browser/components/tabbrowser/content/tabsplitview.js
@@ -164,9 +164,11 @@
/**
* Show all Split View tabs in the content area.
*/
- #activate() {
+ #activate(skipShowPanels = false) {
updateUrlbarButton.arm();
- gBrowser.showSplitViewPanels(this.#tabs);
+ if (!skipShowPanels) {
+ gBrowser.showSplitViewPanels(this.#tabs);
+ }
this.container.dispatchEvent(
new CustomEvent("TabSplitViewActivate", {
detail: { tabs: this.#tabs, splitview: this },
@@ -178,8 +180,10 @@
/**
* Remove Split View tabs from the content area.
*/
- #deactivate() {
- gBrowser.hideSplitViewPanels(this.#tabs);
+ #deactivate(skipHidePanels = false) {
+ if (!skipHidePanels) {
+ gBrowser.hideSplitViewPanels(this.#tabs);
+ }
updateUrlbarButton.arm();
this.container.dispatchEvent(
new CustomEvent("TabSplitViewDeactivate", {
@@ -242,6 +246,7 @@
}
if (this.hasActiveTab) {
this.#activate();
+ gBrowser.setIsSplitViewActive(true, this.#tabs);
}
}
@@ -250,6 +255,7 @@
*/
unsplitTabs() {
gBrowser.unsplitTabs(this);
+ gBrowser.setIsSplitViewActive(false, this.#tabs);
}
/**
@@ -284,10 +290,11 @@
*/
on_TabSelect(event) {
this.hasActiveTab = event.target.splitview === this;
+ gBrowser.setIsSplitViewActive(this.hasActiveTab, this.#tabs);
if (this.hasActiveTab) {
- this.#activate();
+ this.#activate(true);
} else {
- this.#deactivate();
+ this.#deactivate(true);
}
}
}
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_splitview.js
@@ -28,19 +28,8 @@ async function checkSplitViewPanelVisible(tab, isVisible) {
await BrowserTestUtils.waitForMutationCondition(
panel,
{ attributes: true },
- () => panel.classList.contains("split-view-panel") == isVisible
+ () => panel.classList.contains("split-view-panel-active") == isVisible
);
- if (isVisible) {
- Assert.ok(
- gBrowser.splitViewBrowsers.includes(tab.linkedBrowser),
- "Split view panel is active."
- );
- } else {
- Assert.ok(
- !gBrowser.splitViewBrowsers.includes(tab.linkedBrowser),
- "Split view panel is inactive."
- );
- }
}
function dragSplitter(deltaX, splitter) {
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
@@ -29,12 +29,18 @@ async function setupSplitView() {
info("Add tabs into an active split view.");
await BrowserTestUtils.switchTab(gBrowser, tabs[0]);
const splitView = gBrowser.addTabSplitView(tabs);
+ const tabpanels = document.getElementById("tabbrowser-tabpanels");
+ await BrowserTestUtils.waitForMutationCondition(
+ tabpanels,
+ { attributes: true },
+ () => tabpanels.hasAttribute("splitview")
+ );
for (const tab of tabs) {
const tabPanel = document.getElementById(tab.linkedPanel);
await BrowserTestUtils.waitForMutationCondition(
tabPanel,
{ attributes: true },
- () => tabPanel.classList.contains("split-view-panel")
+ () => tabPanel.classList.contains("split-view-panel-active")
);
}
diff --git a/browser/themes/shared/tabbrowser/content-area.css b/browser/themes/shared/tabbrowser/content-area.css
@@ -156,16 +156,21 @@
-moz-user-focus: none !important;
}
- &[splitview] {
+ .split-view-panel {
--panel-min-width: 140px;
+ min-width: var(--panel-min-width);
+ max-width: calc(100% - var(--panel-min-width));
+ width: 49.4%;
+ }
- .split-view-panel {
- position: relative;
- flex: 1;
- min-width: var(--panel-min-width);
- max-width: calc(100% - var(--panel-min-width));
+ &[splitview] {
+ .split-view-panel.split-view-panel-active {
margin: var(--space-xsmall);
+ flex: 1;
+ position: relative;
+ width: unset;
}
+
/* Ensure any dialogs are clipped in an inactive/not-selected panel. */
:root:not([inDOMFullscreen]) & > .split-view-panel:not(.deck-selected) {
overflow: clip;
diff --git a/toolkit/content/widgets/tabbox.js b/toolkit/content/widgets/tabbox.js
@@ -367,10 +367,6 @@
}
set splitViewPanels(newPanels) {
- const oldPanels = this.#splitViewPanels;
- for (const panel of oldPanels) {
- this.removePanelFromSplitView(panel, false);
- }
for (const [i, panel] of newPanels.entries()) {
const panelEl = document.getElementById(panel);
panelEl?.classList.add("split-view-panel");
@@ -381,7 +377,7 @@
}
}
this.#splitViewPanels = newPanels;
- this.#isSplitViewActive = !!newPanels.length;
+ this.isSplitViewActive = !!newPanels.length;
}
get splitViewPanels() {
@@ -409,10 +405,10 @@
this.#splitViewPanels.splice(index, 1);
}
}
- this.#isSplitViewActive = !!this.#splitViewPanels.length;
+ this.isSplitViewActive = !!this.#splitViewPanels.length;
}
- set #isSplitViewActive(isActive) {
+ set isSplitViewActive(isActive) {
this.toggleAttribute("splitview", isActive);
this.splitViewSplitter.hidden = !isActive;
const selectedPanel = this.selectedPanel;
@@ -426,6 +422,11 @@
// offsets it.
this.selectedPanel = selectedPanel;
}
+
+ setSplitViewPanelActive(isActive, panel) {
+ const panelEl = document.getElementById(panel);
+ panelEl?.classList.toggle("split-view-panel-active", isActive);
+ }
}
MozXULElement.implementCustomInterface(MozTabpanels, [
diff --git a/toolkit/content/xul.css b/toolkit/content/xul.css
@@ -461,7 +461,7 @@ deck > *|*:not(:-moz-native-anonymous) {
}
tabpanels > .deck-selected,
-tabpanels > .split-view-panel,
+tabpanels > .split-view-panel-active,
tabpanels > .split-view-splitter,
deck > .deck-selected {
-moz-subtree-hidden-only-visually: 0;