Bug 1893655 - Set up the tabstrip to work vertically r=dao,sidebar-reviewers,desktop-theme-reviewers,tabbrowser-reviewers,Gijs
authorSarah Clements <sclements@mozilla.com>
Sat, 15 Jun 2024 10:43:19 +0000 (13 months ago)
changeset 743140 1dc1daaeb25612bc4be8868dbd766f9566c6ac31
parent 743139 4132a83fedbd1b69b8c1613f6c13286364f3cc30
child 743141 dc5e463eb0f570b9b89aef8cddd47f6659bdd16e
push id41904
push usernfay@mozilla.com
push dateSat, 15 Jun 2024 21:31:13 +0000 (13 months ago)
treeherdermozilla-central@d00c580fa44d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdao, sidebar-reviewers, desktop-theme-reviewers, tabbrowser-reviewers, Gijs
bugs1893655
milestone129.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1893655 - Set up the tabstrip to work vertically r=dao,sidebar-reviewers,desktop-theme-reviewers,tabbrowser-reviewers,Gijs * Add ability to move tabstrip into the sidebar based on a pref Differential Revision: https://phabricator.services.mozilla.com/D212287
browser/app/profile/firefox.js
browser/base/content/browser-box.inc.xhtml
browser/components/sidebar/browser-sidebar.js
browser/components/sidebar/sidebar-main.css
browser/components/sidebar/sidebar-main.mjs
browser/components/sidebar/tests/browser/browser.toml
browser/components/sidebar/tests/browser/browser_vertical_tabs.js
browser/components/tabbrowser/content/tabs.js
browser/themes/shared/tabbrowser/tabs.css
toolkit/content/widgets/arrowscrollbox.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1897,16 +1897,17 @@ pref("pdfjs.previousHandler.alwaysAskBef
 
 // Try to convert PDFs sent as octet-stream
 pref("pdfjs.handleOctetStream", true);
 
 // Is the sidebar positioned ahead of the content browser
 pref("sidebar.position_start", true);
 pref("sidebar.revamp", false);
 pref("sidebar.main.tools", "history,syncedtabs");
+pref("sidebar.verticalTabs", false);
 
 pref("browser.ml.chat.enabled", false);
 pref("browser.ml.chat.prompt.prefix", 'I’m on page "%currentTabTitle%" with "%selection|12000%" selected. ');
 pref("browser.ml.chat.prompts.0", '{"label":"Summarize","value":"Please summarize the selection using precise and concise language. Highlight the main themes and conclusions. Use headers and bulleted lists in the summary, to make it scannable. Maintain the meaning of the selection."}');
 pref("browser.ml.chat.prompts.1", '{"label":"Simplify language","value":"Please rewrite the selection in plain, clear language suitable for a general audience without specialized knowledge. Use all of the following tactics: simple vocabulary; short sentences; active voice; examples where applicable to make explanations clearer; explanations for jargon and technical terms; headers and bulleted lists for scannability. Maintain factual accuracy while simplifying."}');
 pref("browser.ml.chat.prompts.2", '{"label":"Quiz me","value":"Please create questions related to the selection. Ask the questions one by one. Wait for my response before moving on to the next question. Evaluate each response. Ask a variety of types of questions, like multiple choice, true or false and short answer."}');
 pref("browser.ml.chat.provider", "");
 
--- a/browser/base/content/browser-box.inc.xhtml
+++ b/browser/base/content/browser-box.inc.xhtml
@@ -1,15 +1,18 @@
 # 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/.
 
 <hbox flex="1" id="browser">
   <box context="sidebar-context-menu" id="sidebar-main" hidden="true">
-    <html:sidebar-main flex="1"></html:sidebar-main>
+    <html:sidebar-main flex="1">
+    <html:div id="vertical-tabs" slot="tabstrip">
+    </html:div>
+  </html:sidebar-main>
   </box>
   <vbox id="sidebar-box" hidden="true" class="chromeclass-extrachrome">
     <box id="sidebar-header" align="center">
       <toolbarbutton id="sidebar-switcher-target" class="tabbable" aria-expanded="false">
         <image id="sidebar-icon" consumeanchor="sidebar-switcher-target"/>
         <label id="sidebar-title" crop="end" control="sidebar"/>
         <image id="sidebar-switcher-arrow"/>
       </toolbarbutton>
--- a/browser/components/sidebar/browser-sidebar.js
+++ b/browser/components/sidebar/browser-sidebar.js
@@ -239,16 +239,20 @@ var SidebarController = {
     });
 
     if (this.sidebarRevampEnabled) {
       await import("chrome://browser/content/sidebar/sidebar-main.mjs");
       document.getElementById("sidebar-main").hidden = !window.toolbar.visible;
       document.getElementById("sidebar-header").hidden = true;
       this._sidebarMain = document.querySelector("sidebar-main");
       mainResizeObserver.observe(this._sidebarMain);
+
+      if (this.sidebarVerticalTabsEnabled) {
+        this.toggleTabstrip();
+      }
     } else {
       this._switcherTarget.addEventListener("command", () => {
         this.toggleSwitcherPanel();
       });
       this._switcherTarget.addEventListener("keydown", event => {
         this.handleKeydown(event);
       });
     }
@@ -1055,16 +1059,53 @@ var SidebarController = {
         menu.removeAttribute("checked");
         if (triggerbutton) {
           triggerbutton.removeAttribute("checked");
           updateToggleControlLabel(triggerbutton);
         }
       }
     }
   },
+
+  toggleTabstrip() {
+    let tabStrip = document.getElementById("tabbrowser-tabs");
+    let arrowScrollbox = document.getElementById("tabbrowser-arrowscrollbox");
+    let verticalTabs = document.getElementById("vertical-tabs");
+
+    let tabsToolbarWidgets = CustomizableUI.getWidgetIdsInArea("TabsToolbar");
+    let tabstripPlacement = tabsToolbarWidgets.findIndex(
+      item => item == "tabbrowser-tabs"
+    );
+
+    if (this.sidebarVerticalTabsEnabled) {
+      arrowScrollbox.setAttribute("orient", "vertical");
+      tabStrip.setAttribute("orient", "vertical");
+      tabStrip.removeAttribute("overflow");
+      tabStrip._positionPinnedTabs();
+      verticalTabs.append(tabStrip);
+    } else {
+      arrowScrollbox.setAttribute("orient", "horizontal");
+      tabStrip.removeAttribute("orient");
+
+      // make sure we put the tabstrip back in its original position in the TabsToolbar
+      if (tabstripPlacement < tabsToolbarWidgets.length) {
+        document
+          .getElementById("TabsToolbar-customization-target")
+          .insertBefore(
+            tabStrip,
+            document.getElementById(tabsToolbarWidgets[tabstripPlacement + 1])
+          );
+      } else {
+        document
+          .getElementById("TabsToolbar-customization-target")
+          .append(tabStrip);
+      }
+    }
+    verticalTabs.toggleAttribute("activated", this.sidebarVerticalTabsEnabled);
+  },
 };
 
 // Add getters related to the position here, since we will want them
 // available for both startDelayedLoad and init.
 XPCOMUtils.defineLazyPreferenceGetter(
   SidebarController,
   "_positionStart",
   SidebarController.POSITION_START_PREF,
@@ -1078,8 +1119,15 @@ XPCOMUtils.defineLazyPreferenceGetter(
   false
 );
 XPCOMUtils.defineLazyPreferenceGetter(
   SidebarController,
   "sidebarRevampTools",
   "sidebar.main.tools",
   "history, syncedtabs"
 );
+XPCOMUtils.defineLazyPreferenceGetter(
+  SidebarController,
+  "sidebarVerticalTabsEnabled",
+  "sidebar.verticalTabs",
+  false,
+  SidebarController.toggleTabstrip.bind(SidebarController)
+);
--- a/browser/components/sidebar/sidebar-main.css
+++ b/browser/components/sidebar/sidebar-main.css
@@ -2,17 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
 
 .wrapper {
   display: grid;
   grid-template-rows: auto 1fr;
   box-sizing: border-box;
   height: 100%;
-  padding: var(--space-medium);
+  min-width: 50px;
+  padding-inline-start: var(--space-medium);
   border-inline-end: 1px solid var(--chrome-content-separator-color);
   background-color: var(--sidebar-background-color);
   color: var(--sidebar-text-color);
   :host([positionend]) & {
     border-inline-start: 1px solid var(--chrome-content-separator-color);
     border-inline-end: none;
   }
 }
--- a/browser/components/sidebar/sidebar-main.mjs
+++ b/browser/components/sidebar/sidebar-main.mjs
@@ -15,17 +15,17 @@ import "chrome://global/content/elements
 
 /**
  * Sidebar with expanded and collapsed states that provides entry points
  * to various sidebar panels and sidebar extensions.
  */
 export default class SidebarMain extends MozLitElement {
   static properties = {
     bottomActions: { type: Array },
-    expanded: { type: Boolean },
+    expanded: { type: Boolean, reflect: true },
     selectedView: { type: String },
     sidebarItems: { type: Array },
     open: { type: Boolean },
   };
 
   static queries = {
     allButtons: { all: "moz-button" },
     extensionButtons: { all: ".tools-and-extensions > moz-button[extension]" },
@@ -82,19 +82,23 @@ export default class SidebarMain extends
     window.removeEventListener("SidebarItemChanged", this);
     window.removeEventListener("SidebarItemRemoved", this);
   }
 
   onSidebarPopupShowing(event) {
     // Store the context menu target which holds the id required for managing sidebar items
     this.contextMenuTarget =
       event.explicitOriginalTarget.flattenedTreeParentNode;
-    if (!this.contextMenuTarget.getAttribute("extensionId")) {
-      event.preventDefault();
+    if (
+      this.contextMenuTarget.getAttribute("extensionId") ||
+      this.contextMenuTarget.className.includes("tab")
+    ) {
+      return;
     }
+    event.preventDefault();
   }
 
   async manageExtension() {
     await window.BrowserAddonUI.manageAddon(
       this.contextMenuTarget.getAttribute("extensionId"),
       "sidebar-context-menu"
     );
   }
@@ -214,16 +218,17 @@ export default class SidebarMain extends
 
   render() {
     return html`
       <link
         rel="stylesheet"
         href="chrome://browser/content/sidebar/sidebar-main.css"
       />
       <div class="wrapper">
+        <slot name="tabstrip"></slot>
         <button-group
           class="tools-and-extensions actions-list"
           orientation="vertical"
         >
           ${repeat(
             this.getToolsAndExtensions().values(),
             action => action.view,
             action => this.entrypointTemplate(action)
--- a/browser/components/sidebar/tests/browser/browser.toml
+++ b/browser/components/sidebar/tests/browser/browser.toml
@@ -1,14 +1,14 @@
 [DEFAULT]
 support-files = ["head.js"]
 
-["browser_adopt_sidebar_from_opener.js"]
+["browser_a11y_sidebar.js"]
 
-["browser_a11y_sidebar.js"]
+["browser_adopt_sidebar_from_opener.js"]
 
 ["browser_customize_sidebar.js"]
 
 ["browser_domfullscreen_sidebar.js"]
 
 ["browser_extensions_sidebar.js"]
 
 ["browser_hide_sidebar_on_popup.js"]
@@ -18,9 +18,11 @@ support-files = ["head.js"]
 ["browser_sidebar_context_menu.js"]
 
 ["browser_sidebar_max_width.js"]
 
 ["browser_sidebar_prefs.js"]
 
 ["browser_toolbar_sidebar_button.js"]
 
+["browser_vertical_tabs.js"]
+
 ["browser_view_sidebar_menu.js"]
new file mode 100644
--- /dev/null
+++ b/browser/components/sidebar/tests/browser/browser_vertical_tabs.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+   https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(() =>
+  SpecialPowers.pushPrefEnv({
+    set: [
+      ["sidebar.revamp", true],
+      ["sidebar.verticalTabs", false],
+    ],
+  })
+);
+registerCleanupFunction(() => SpecialPowers.popPrefEnv());
+
+add_task(async function test_toggle_vertical_tabs() {
+  const win = await BrowserTestUtils.openNewBrowserWindow();
+  await waitForBrowserWindowActive(win);
+  const { document } = win;
+  const sidebar = document.querySelector("sidebar-main");
+  ok(sidebar, "Sidebar is shown.");
+
+  let tabStrip = document.getElementById("tabbrowser-tabs");
+  let defaultTabstripParent = document.getElementById(
+    "TabsToolbar-customization-target"
+  );
+  let verticalTabs = document.querySelector("#vertical-tabs");
+  ok(
+    !BrowserTestUtils.isVisible(verticalTabs),
+    "Vertical tabs slot is not visible"
+  );
+
+  is(
+    tabStrip.parentNode,
+    defaultTabstripParent,
+    "Tabstrip is in default horizontal position"
+  );
+  is(
+    tabStrip.nextElementSibling.id,
+    "new-tab-button",
+    "Tabstrip is before the new tab button"
+  );
+
+  // flip the pref to move the tabstrip into the sidebar
+  await SpecialPowers.pushPrefEnv({ set: [["sidebar.verticalTabs", true]] });
+  ok(BrowserTestUtils.isVisible(verticalTabs), "Vertical tabs slot is visible");
+  is(
+    tabStrip.parentNode,
+    verticalTabs,
+    "Tabstrip is slotted into the sidebar vertical tabs container"
+  );
+  is(win.gBrowser.tabs.length, 1, "Tabstrip now has one tab");
+
+  // make sure the tab context menu still works
+  const contextMenu = document.getElementById("tabContextMenu");
+  win.gBrowser.selectedTab.focus();
+  EventUtils.synthesizeMouseAtCenter(
+    win.gBrowser.selectedTab,
+    {
+      type: "contextmenu",
+      button: 2,
+    },
+    win
+  );
+
+  await openAndWaitForContextMenu(contextMenu, win.gBrowser.selectedTab, () => {
+    document.getElementById("context_openANewTab").click();
+  });
+
+  is(win.gBrowser.tabs.length, 2, "Tabstrip now has two tabs");
+
+  // flip the pref to move the tabstrip horizontally
+  await SpecialPowers.pushPrefEnv({ set: [["sidebar.verticalTabs", false]] });
+
+  ok(
+    !BrowserTestUtils.isVisible(verticalTabs),
+    "Vertical tabs slot is not visible"
+  );
+  is(
+    tabStrip.parentNode,
+    defaultTabstripParent,
+    "Tabstrip is in default horizontal position"
+  );
+  is(
+    tabStrip.nextElementSibling.id,
+    "new-tab-button",
+    "Tabstrip is before the new tab button"
+  );
+
+  await BrowserTestUtils.closeWindow(win);
+});
--- a/browser/components/tabbrowser/content/tabs.js
+++ b/browser/components/tabbrowser/content/tabs.js
@@ -1177,17 +1177,18 @@
       );
 
       arrowScrollbox.shadowRoot.addEventListener("overflow", event => {
         // Ignore overflow events:
         // - from nested scrollable elements
         // - for vertical orientation
         if (
           event.originalTarget != arrowScrollbox.scrollbox ||
-          event.detail == 0
+          event.detail == 0 ||
+          event.originalTarget.getAttribute("orient") == "vertical"
         ) {
           return;
         }
 
         this.toggleAttribute("overflow", true);
         this._positionPinnedTabs();
         this._updateCloseButtons();
         this._handleTabSelect(true);
--- a/browser/themes/shared/tabbrowser/tabs.css
+++ b/browser/themes/shared/tabbrowser/tabs.css
@@ -495,17 +495,17 @@
   margin-inline-end: calc(var(--inline-tab-padding) / -2);
   width: 24px;
   height: 24px;
   padding: 6px;
   border-radius: var(--tab-border-radius);
   list-style-image: url(chrome://global/skin/icons/close-12.svg);
 
   &[pinned],
-  #tabbrowser-tabs[closebuttons="activetab"] > #tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-content > &:not([selected]) {
+  #tabbrowser-tabs[closebuttons="activetab"]:not([orient="vertical"]) > #tabbrowser-arrowscrollbox > .tabbrowser-tab > .tab-stack > .tab-content > &:not([selected]) {
     display: none;
   }
 }
 
 /* The following rulesets allow showing more of the tab title */
 .tabbrowser-tab:not([labelendaligned], :hover) > .tab-stack > .tab-content > .tab-close-button {
   padding-inline-start: 0;
   width: 18px;
@@ -715,16 +715,51 @@
 
   &:not([scrolledtostart=true])::part(scrollbutton-up):hover:active,
   &:not([scrolledtoend=true])::part(scrollbutton-down):hover:active {
     background-color: var(--toolbarbutton-active-background);
     color: inherit;
   }
 }
 
+/* Vertical tabs styling */
+#tabbrowser-arrowscrollbox[orient="vertical"] {
+  overflow-y: auto;
+
+  &::part(scrollbutton-up),
+  &::part(scrollbutton-down) {
+    display: none;
+  }
+
+  &::part(scrollbox-clip) {
+    min-height: inherit;
+  }
+}
+
+#vertical-tabs {
+  overflow-y: hidden;
+  scrollbar-width: thin;
+  display: none;
+
+  &[activated] {
+    display: flex;
+  }
+}
+
+sidebar-main:not([expanded]) > #vertical-tabs > #tabbrowser-tabs[orient="vertical"] .tabbrowser-tab {
+  /* TODO look into handlings this by setting --tab-min-width in tabs.js in bug 1899336. */
+  min-width: inherit;
+  width: inherit;
+}
+
+sidebar-main:not([expanded]) > #vertical-tabs > #tabbrowser-tabs[orient="vertical"] .tab-close-button,
+sidebar-main[expanded] > #vertical-tabs > #tabbrowser-tabs[orient="vertical"] .tab-close-button:not([selected]) {
+  display: none;
+}
+
 /* Tab drag and drop */
 
 .tab-drop-indicator {
   width: 12px;
   margin-inline-start: -12px;
   background: url(chrome://browser/skin/tabbrowser/tab-drag-indicator.svg) no-repeat center;
   position: relative;
   z-index: 2;
--- a/toolkit/content/widgets/arrowscrollbox.js
+++ b/toolkit/content/widgets/arrowscrollbox.js
@@ -25,17 +25,17 @@
       <spacer part="overflow-start-indicator"/>
       <box class="scrollbox-clip" part="scrollbox-clip" flex="1">
         <scrollbox part="scrollbox" flex="1">
           <html:slot/>
         </scrollbox>
       </box>
       <spacer part="overflow-end-indicator"/>
       <toolbarbutton id="scrollbutton-down" part="scrollbutton-down" keyNav="false" data-l10n-id="overflow-scroll-button-forwards"/>
-    `;
+      `;
     }
 
     constructor() {
       super();
       this.attachShadow({ mode: "open" });
       this.shadowRoot.appendChild(this.fragment);
 
       this.scrollbox = this.shadowRoot.querySelector("scrollbox");
@@ -126,31 +126,32 @@
 
       this.scrollbox.addEventListener("scrollend", event => {
         this.on_scrollend(event);
         this.dispatchEvent(new Event("scrollend"));
       });
     }
 
     connectedCallback() {
+      this.removeAttribute("overflowing");
+
       if (this.hasConnected) {
         return;
       }
       this.hasConnected = true;
 
       document.l10n.connectRoot(this.shadowRoot);
 
       if (!this.hasAttribute("smoothscroll")) {
         this.smoothScroll = Services.prefs.getBoolPref(
           "toolkit.scrollbox.smoothScroll",
           true
         );
       }
 
-      this.removeAttribute("overflowing");
       this.initializeAttributeInheritance();
       this._updateScrollButtonsDisabledState();
     }
 
     get fragment() {
       if (!this.constructor.hasOwnProperty("_fragment")) {
         this.constructor._fragment = MozXULElement.parseXULToFragment(
           this.markup