tor-browser

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

SidebarState.sys.mjs (24493B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 XPCOMUtils.defineLazyPreferenceGetter(
     10  lazy,
     11  "verticalTabsEnabled",
     12  "sidebar.verticalTabs"
     13 );
     14 
     15 const DEFAULT_LAUNCHER_VISIBLE = false;
     16 
     17 /**
     18 * The properties that make up a sidebar's UI state.
     19 *
     20 * @typedef {object} SidebarStateProps
     21 *
     22 * @property {boolean} command
     23 *   The id of the current sidebar panel. The panel may be closed and still have a command value.
     24 *   Re-opening the sidebar panel will then load the current command id.
     25 * @property {boolean} panelOpen
     26 *   Whether there is an open panel.
     27 * @property {number} panelWidth
     28 *   Current width of the sidebar panel.
     29 * @property {boolean} launcherVisible
     30 *   Whether the launcher is visible.
     31 *   This is always true when the sidebar.visibility pref value is "always-show", and toggle between true/false when visibility is "hide-sidebar"
     32 * @property {boolean} launcherExpanded
     33 *   Whether the launcher is expanded.
     34 *   When sidebar.visibility pref value is "always-show", the toolbar button serves to toggle this property
     35 * @property {boolean} launcherDragActive
     36 *   Whether the launcher is currently being dragged.
     37 * @property {boolean} pinnedTabsDragActive
     38 *   Whether the pinned tabs container is currently being dragged.
     39 * @property {boolean} toolsDragActive
     40 *   Whether the tools container is currently being dragged.
     41 * @property {boolean} launcherHoverActive
     42 *   Whether the launcher is currently being hovered.
     43 * @property {number} launcherWidth
     44 *   Current width of the sidebar launcher.
     45 * @property {number} expandedLauncherWidth
     46 *   Width of the expanded launcher
     47 * @property {number} pinnedTabsHeight
     48 *   Current height of the pinned tabs container
     49 * @property {number} expandedPinnedTabsHeight
     50 *   Height of the pinned tabs container when the sidebar is expanded
     51 * @property {number} collapsedPinnedTabsHeight
     52 *   Height of the pinned tabs container when the sidebar is collapsed
     53 * @property {number} toolsHeight
     54 *   Current height of the tools container
     55 * @property {number} expandedToolsHeight
     56 *   Height of the tools container when the sidebar is expanded
     57 * @property {number} collapsedToolsHeight
     58 *   Height of the tools container when the sidebar is collapsed
     59 */
     60 
     61 const LAUNCHER_MINIMUM_WIDTH = 100;
     62 const SIDEBAR_MAXIMUM_WIDTH = "75vw";
     63 
     64 const LEGACY_USED_PREF = "sidebar.old-sidebar.has-used";
     65 const REVAMP_USED_PREF = "sidebar.new-sidebar.has-used";
     66 
     67 /**
     68 * A reactive data store for the sidebar's UI state. Similar to Lit's
     69 * ReactiveController, any updates to properties can potentially trigger UI
     70 * updates, or updates to other properties.
     71 */
     72 export class SidebarState {
     73  #controller = null;
     74  /** @type {SidebarStateProps} */
     75  #props = {
     76    ...SidebarState.defaultProperties,
     77  };
     78  #launcherEverVisible = false;
     79 
     80  /** @type {SidebarStateProps} */
     81  static defaultProperties = Object.freeze({
     82    command: "",
     83    launcherDragActive: false,
     84    launcherExpanded: false,
     85    launcherHoverActive: false,
     86    launcherVisible: false,
     87    panelOpen: false,
     88    pinnedTabsDragActive: false,
     89    toolsDragActive: false,
     90  });
     91 
     92  /**
     93   * Construct a new SidebarState.
     94   *
     95   * @param {SidebarController} controller
     96   *   The controller this state belongs to. SidebarState is instantiated
     97   *   per-window, thereby allowing us to retrieve DOM elements attached to
     98   *   the controller.
     99   */
    100  constructor(controller) {
    101    this.#controller = controller;
    102    this.revampEnabled = controller.sidebarRevampEnabled;
    103    this.revampVisibility = controller.sidebarRevampVisibility;
    104 
    105    if (this.revampEnabled) {
    106      this.#props.launcherVisible = this.defaultLauncherVisible;
    107    }
    108  }
    109 
    110  /**
    111   * Get the sidebar launcher.
    112   *
    113   * @returns {HTMLElement}
    114   */
    115  get #launcherEl() {
    116    return this.#controller.sidebarMain;
    117  }
    118 
    119  /**
    120   * Get parent element of the sidebar launcher.
    121   *
    122   * @returns {XULElement}
    123   */
    124  get #launcherContainerEl() {
    125    return this.#controller.sidebarContainer;
    126  }
    127 
    128  /**
    129   * Get the sidebar panel element.
    130   *
    131   * @returns {XULElement}
    132   */
    133  get #sidebarBoxEl() {
    134    return this.#controller._box;
    135  }
    136 
    137  /**
    138   * Get the sidebar panel.
    139   *
    140   * @returns {XULElement}
    141   */
    142  get #panelEl() {
    143    return this.#controller._box;
    144  }
    145 
    146  /**
    147   * Get the pinned tabs container element.
    148   *
    149   * @returns {XULElement}
    150   */
    151  get #pinnedTabsContainerEl() {
    152    return this.#controller._pinnedTabsContainer;
    153  }
    154 
    155  /**
    156   * Get the items-wrapper part of the pinned tabs container element.
    157   *
    158   * @returns {XULElement}
    159   */
    160  get #pinnedTabsItemsWrapper() {
    161    return this.#pinnedTabsContainerEl.shadowRoot.querySelector(
    162      "[part=items-wrapper]"
    163    );
    164  }
    165 
    166  /**
    167   * Get the tools container element.
    168   *
    169   * @returns {XULElement}
    170   */
    171  get #toolsContainer() {
    172    return this.#controller.sidebarMain?.buttonsWrapper;
    173  }
    174 
    175  /**
    176   * Get the tools button-group element.
    177   *
    178   * @returns {XULElement}
    179   */
    180  get #toolsButtonGroup() {
    181    return this.#controller.sidebarMain?.buttonGroup;
    182  }
    183 
    184  /**
    185   * Get window object from the controller.
    186   */
    187  get #controllerGlobal() {
    188    return this.#launcherContainerEl.ownerGlobal;
    189  }
    190 
    191  /**
    192   * Update the starting properties according to external factors such as
    193   * window type and user preferences.
    194   */
    195  initializeState(showLauncher = this.defaultLauncherVisible) {
    196    const isPopup = !this.#controllerGlobal.toolbar.visible;
    197    if (isPopup) {
    198      // Don't show launcher if we're in a popup window.
    199      this.launcherVisible = false;
    200    } else {
    201      if (lazy.verticalTabsEnabled) {
    202        this.#props.launcherExpanded = true;
    203      }
    204      this.launcherVisible = showLauncher;
    205    }
    206 
    207    // Explicitly trigger effects to ensure that the UI is kept up to date.
    208    this.launcherExpanded = this.#props.launcherExpanded;
    209  }
    210 
    211  /**
    212   * Load the state information given by session store, backup state, or
    213   * adopted window.
    214   *
    215   * @param {SidebarStateProps} props
    216   *   New properties to overwrite the default state with.
    217   */
    218  loadInitialState(props) {
    219    // Override any initial launcher visible state when the new sidebar has not been
    220    // made visible yet
    221    let hasPreviousVisibleState = false;
    222    if (props.hasOwnProperty("hidden")) {
    223      props.launcherVisible = !props.hidden;
    224      hasPreviousVisibleState = true;
    225      delete props.hidden;
    226    }
    227    if (props.hasOwnProperty("launcherVisible")) {
    228      hasPreviousVisibleState = true;
    229    }
    230 
    231    const hasSidebarLauncherBeenVisible =
    232      this.#controller.SidebarManager.hasSidebarLauncherBeenVisible;
    233 
    234    // We override a falsey launcherVisible property with the default value if
    235    // there's no explicitly visible/hidden state and its not been visible before
    236    if (
    237      !props.launcherVisible &&
    238      !hasPreviousVisibleState &&
    239      !hasSidebarLauncherBeenVisible
    240    ) {
    241      props.launcherVisible = this.defaultLauncherVisible;
    242    }
    243    for (const [key, value] of Object.entries(props)) {
    244      if (value === undefined) {
    245        // `undefined` means we should use the default value.
    246        continue;
    247      }
    248      switch (key) {
    249        case "command":
    250          this.command = value;
    251          break;
    252        case "panelWidth":
    253          this.#panelEl.style.width = `${value}px`;
    254          break;
    255        case "width":
    256          this.#panelEl.style.width = value;
    257          break;
    258        case "expanded":
    259          this.launcherExpanded = value;
    260          break;
    261        case "panelOpen":
    262          // we need to know if we have a command value before finalizing panelOpen
    263          break;
    264        case "expandedPinnedTabsHeight":
    265        case "collapsedPinnedTabsHeight":
    266          this.updatePinnedTabsHeight();
    267          break;
    268        case "expandedToolsHeight":
    269        case "collapsedToolsHeight":
    270          this.updateToolsHeight();
    271          break;
    272        default:
    273          this[key] = value;
    274      }
    275    }
    276 
    277    if (this.command && !props.hasOwnProperty("panelOpen")) {
    278      // legacy state saved before panelOpen was a thing
    279      props.panelOpen = true;
    280    }
    281    if (!this.command) {
    282      props.panelOpen = false;
    283    }
    284    this.panelOpen = !!props.panelOpen;
    285    if (this.command && this.panelOpen) {
    286      this.launcherVisible = true;
    287      // show() is async, so make sure we return its promise here
    288      return this.#controller.showInitially(this.command);
    289    }
    290    return this.#controller.hide();
    291  }
    292 
    293  /**
    294   * Toggle the value of a boolean property.
    295   *
    296   * @param {string} key
    297   *   The property to toggle.
    298   */
    299  toggle(key) {
    300    if (Object.hasOwn(this.#props, key)) {
    301      this[key] = !this[key];
    302    }
    303  }
    304 
    305  /**
    306   * Serialize the state properties for persistence in session store or prefs.
    307   *
    308   * @returns {SidebarStateProps}
    309   */
    310  getProperties() {
    311    const props = {
    312      command: this.command,
    313      panelOpen: this.panelOpen,
    314      panelWidth: this.panelWidth,
    315      launcherWidth: convertToInt(this.launcherWidth),
    316      expandedLauncherWidth: convertToInt(this.expandedLauncherWidth),
    317      launcherExpanded: this.launcherExpanded,
    318      launcherVisible: this.launcherVisible,
    319      pinnedTabsHeight: this.pinnedTabsHeight,
    320      expandedPinnedTabsHeight: this.expandedPinnedTabsHeight,
    321      collapsedPinnedTabsHeight: this.collapsedPinnedTabsHeight,
    322      toolsHeight: this.toolsHeight,
    323      expandedToolsHeight: this.expandedToolsHeight,
    324      collapsedToolsHeight: this.collapsedToolsHeight,
    325    };
    326    // omit any properties with undefined values'
    327    for (let [key, value] of Object.entries(props)) {
    328      if (value === undefined) {
    329        delete props[key];
    330      }
    331    }
    332    return props;
    333  }
    334 
    335  get panelOpen() {
    336    return this.#props.panelOpen;
    337  }
    338 
    339  set panelOpen(open) {
    340    if (this.#props.panelOpen == open) {
    341      return;
    342    }
    343    this.#props.panelOpen = !!open;
    344    if (open) {
    345      // Launcher must be visible to open a panel.
    346      this.launcherVisible = true;
    347 
    348      Services.prefs.setBoolPref(
    349        this.revampEnabled ? REVAMP_USED_PREF : LEGACY_USED_PREF,
    350        true
    351      );
    352    }
    353 
    354    const mainEl = this.#controller.sidebarContainer;
    355    const boxEl = this.#controller._box;
    356    const contentAreaEl =
    357      this.#controllerGlobal.document.getElementById("tabbrowser-tabbox");
    358    if (mainEl?.toggleAttribute) {
    359      mainEl.toggleAttribute("sidebar-panel-open", open);
    360    }
    361    boxEl.toggleAttribute("sidebar-panel-open", open);
    362    contentAreaEl.toggleAttribute("sidebar-panel-open", open);
    363  }
    364 
    365  get panelWidth() {
    366    // Use the value from `style`. This is a more accurate user preference, as
    367    // opposed to what the resize observer gives us.
    368    return convertToInt(this.#panelEl?.style.width);
    369  }
    370 
    371  set panelWidth(width) {
    372    this.#launcherContainerEl.style.maxWidth = `calc(${SIDEBAR_MAXIMUM_WIDTH} - ${width}px)`;
    373  }
    374 
    375  get expandedPinnedTabsHeight() {
    376    return this.#props.expandedPinnedTabsHeight;
    377  }
    378 
    379  set expandedPinnedTabsHeight(height) {
    380    this.#props.expandedPinnedTabsHeight = height;
    381    this.updatePinnedTabsHeight();
    382  }
    383 
    384  get collapsedPinnedTabsHeight() {
    385    return this.#props.collapsedPinnedTabsHeight;
    386  }
    387 
    388  set collapsedPinnedTabsHeight(height) {
    389    this.#props.collapsedPinnedTabsHeight = height;
    390    this.updatePinnedTabsHeight();
    391  }
    392 
    393  get expandedToolsHeight() {
    394    return this.#props.expandedToolsHeight;
    395  }
    396 
    397  set expandedToolsHeight(height) {
    398    this.#props.expandedToolsHeight = height;
    399    this.updateToolsHeight();
    400  }
    401 
    402  get collapsedToolsHeight() {
    403    return this.#props.collapsedToolsHeight;
    404  }
    405 
    406  set collapsedToolsHeight(height) {
    407    this.#props.collapsedToolsHeight = height;
    408    this.updateToolsHeight();
    409  }
    410 
    411  get defaultLauncherVisible() {
    412    if (!this.revampEnabled) {
    413      return false;
    414    }
    415 
    416    // default/fallback value for vertical tabs is to always be visible initially
    417    if (lazy.verticalTabsEnabled) {
    418      return true;
    419    }
    420    return DEFAULT_LAUNCHER_VISIBLE;
    421  }
    422 
    423  get launcherVisible() {
    424    return this.#props.launcherVisible;
    425  }
    426 
    427  get launcherEverVisible() {
    428    return this.#launcherEverVisible;
    429  }
    430 
    431  /**
    432   * Update the launcher `visible` and `expanded` states
    433   *
    434   * @param {boolean} visible
    435   *                  Show or hide the launcher. Defaults to the value returned by the defaultLauncherVisible getter
    436   * @param {boolean} forceExpandValue
    437   */
    438  updateVisibility(
    439    visible = this.defaultLauncherVisible,
    440    forceExpandValue = null
    441  ) {
    442    switch (this.revampVisibility) {
    443      case "hide-sidebar":
    444        if (lazy.verticalTabsEnabled) {
    445          forceExpandValue = visible;
    446        }
    447        this.launcherVisible = visible;
    448        break;
    449      case "always-show":
    450      case "expand-on-hover":
    451        this.launcherVisible = true;
    452        break;
    453    }
    454    if (forceExpandValue !== null) {
    455      this.launcherExpanded = forceExpandValue;
    456    }
    457  }
    458 
    459  set launcherVisible(visible) {
    460    if (!this.revampEnabled) {
    461      // Launcher not supported in legacy sidebar.
    462      this.#props.launcherVisible = false;
    463      this.#launcherContainerEl.hidden = true;
    464      return;
    465    }
    466    this.#props.launcherVisible = visible;
    467    if (visible) {
    468      this.#launcherEverVisible = true;
    469    }
    470    this.#launcherContainerEl.hidden = !visible;
    471    this.#launcherEl.requestUpdate();
    472    this.#updateTabbrowser(visible);
    473    this.#sidebarBoxEl.style.paddingInlineStart =
    474      this.panelOpen && !visible ? "var(--space-small)" : "unset";
    475  }
    476 
    477  get launcherExpanded() {
    478    return this.#props.launcherExpanded;
    479  }
    480 
    481  set launcherExpanded(expanded) {
    482    if (!this.revampEnabled) {
    483      // Launcher not supported in legacy sidebar.
    484      this.#props.launcherExpanded = false;
    485      return;
    486    }
    487    const previousExpanded = this.#props.launcherExpanded;
    488    this.#props.launcherExpanded = expanded;
    489    this.#launcherEl.expanded = expanded;
    490    if (expanded && !previousExpanded) {
    491      Glean.sidebar.expand.record();
    492    }
    493    // Marking the tab container element as expanded or not simplifies the CSS logic
    494    // and selectors considerably.
    495    const { tabContainer } = this.#controllerGlobal.gBrowser;
    496    const mainEl = this.#controller.sidebarContainer;
    497    const splitterEl = this.#controller._launcherSplitter;
    498    const boxEl = this.#controller._box;
    499    const contentAreaEl =
    500      this.#controllerGlobal.document.getElementById("tabbrowser-tabbox");
    501    tabContainer.toggleAttribute("expanded", expanded);
    502    if (mainEl?.toggleAttribute) {
    503      mainEl.toggleAttribute("sidebar-launcher-expanded", expanded);
    504    }
    505    splitterEl?.toggleAttribute("sidebar-launcher-expanded", expanded);
    506    boxEl?.toggleAttribute("sidebar-launcher-expanded", expanded);
    507    contentAreaEl.toggleAttribute("sidebar-launcher-expanded", expanded);
    508    this.#controller.updateToolbarButton();
    509    if (!this.launcherDragActive) {
    510      this.#updateLauncherWidth();
    511    }
    512    if (
    513      !this.pinnedTabsDragActive &&
    514      this.#controller.sidebarRevampVisibility !== "expand-on-hover"
    515    ) {
    516      this.updatePinnedTabsHeight();
    517    }
    518    this.handleUpdateToolsHeightOnLauncherExpanded();
    519  }
    520 
    521  get launcherDragActive() {
    522    return this.#props.launcherDragActive;
    523  }
    524 
    525  set launcherDragActive(active) {
    526    this.#props.launcherDragActive = active;
    527    if (active) {
    528      // Temporarily disable expand on hover functionality while dragging
    529      if (this.#controller.sidebarRevampVisibility === "expand-on-hover") {
    530        this.#controller.toggleExpandOnHover(false);
    531      }
    532 
    533      this.#launcherEl.toggleAttribute("customWidth", true);
    534    } else if (this.launcherWidth < LAUNCHER_MINIMUM_WIDTH) {
    535      // Re-enable expand on hover if necessary
    536      if (this.#controller.sidebarRevampVisibility === "expand-on-hover") {
    537        this.#controller.toggleExpandOnHover(true, true);
    538      }
    539 
    540      // Snap back to collapsed state when the new width is too narrow.
    541      this.launcherExpanded = false;
    542      if (this.revampVisibility === "hide-sidebar") {
    543        this.launcherVisible = false;
    544      }
    545    } else {
    546      // Re-enable expand on hover if necessary
    547      if (this.#controller.sidebarRevampVisibility === "expand-on-hover") {
    548        this.#controller.toggleExpandOnHover(true, true);
    549      }
    550 
    551      // Store the user-preferred launcher width.
    552      this.expandedLauncherWidth = this.launcherWidth;
    553    }
    554    const rootEl = this.#controllerGlobal.document.documentElement;
    555    rootEl.toggleAttribute("sidebar-launcher-drag-active", active);
    556  }
    557 
    558  get pinnedTabsDragActive() {
    559    return this.#props.pinnedTabsDragActive;
    560  }
    561 
    562  set pinnedTabsDragActive(active) {
    563    this.#props.pinnedDragActive = active;
    564 
    565    let itemsWrapperHeight =
    566      this.#controllerGlobal.windowUtils.getBoundsWithoutFlushing(
    567        this.#pinnedTabsItemsWrapper
    568      ).height;
    569    let pinnedTabsContainerHeight =
    570      this.#controllerGlobal.windowUtils.getBoundsWithoutFlushing(
    571        this.#pinnedTabsContainerEl
    572      ).height;
    573    if (!active) {
    574      this.pinnedTabsHeight = Math.min(
    575        pinnedTabsContainerHeight,
    576        itemsWrapperHeight
    577      );
    578      // Store the user-preferred pinned tabs height.
    579      if (this.#props.launcherExpanded) {
    580        this.expandedPinnedTabsHeight = this.pinnedTabsHeight;
    581      } else {
    582        this.collapsedPinnedTabsHeight = this.pinnedTabsHeight;
    583      }
    584    }
    585  }
    586 
    587  get toolsDragActive() {
    588    return this.#props.toolsDragActive;
    589  }
    590 
    591  set toolsDragActive(active) {
    592    this.#props.toolsDragActive = active;
    593    let maxToolsHeight = this.maxToolsHeight;
    594    if (!active && this.#toolsContainer) {
    595      let buttonGroupHeight =
    596        this.#controllerGlobal.windowUtils.getBoundsWithoutFlushing(
    597          this.#toolsContainer
    598        ).height;
    599      this.toolsHeight =
    600        buttonGroupHeight > maxToolsHeight ? maxToolsHeight : buttonGroupHeight;
    601      if (
    602        buttonGroupHeight > maxToolsHeight &&
    603        this.#controller.sidebarRevampVisibility !== "expand-on-hover"
    604      ) {
    605        this.#launcherEl.shouldShowOverflowButton = false;
    606      }
    607      // Store the user-preferred tools height.
    608      if (this.#props.launcherExpanded) {
    609        this.expandedToolsHeight = this.toolsHeight;
    610      } else {
    611        this.collapsedToolsHeight = this.toolsHeight;
    612      }
    613    }
    614  }
    615 
    616  get maxToolsHeight() {
    617    const FIRST_LAST_TAB_PADDING = 5.8833;
    618    if (!this.#toolsButtonGroup) {
    619      return null;
    620    }
    621    let referenceToolButton;
    622    if (this.#toolsButtonGroup.children.length > 1) {
    623      referenceToolButton = this.#toolsButtonGroup.children[1];
    624    } else {
    625      referenceToolButton = this.#toolsButtonGroup.children[0];
    626    }
    627    let toolRect =
    628      this.#controllerGlobal.windowUtils.getBoundsWithoutFlushing(
    629        referenceToolButton
    630      );
    631    let extraPadding = 0;
    632    if (this.#toolsButtonGroup.children.length >= 3) {
    633      extraPadding = FIRST_LAST_TAB_PADDING * 2;
    634    } else if (this.#toolsButtonGroup.children.length === 2) {
    635      extraPadding = FIRST_LAST_TAB_PADDING;
    636    }
    637    return this.#props.launcherExpanded
    638      ? "unset"
    639      : toolRect.height * this.#toolsButtonGroup.children.length + extraPadding;
    640  }
    641 
    642  get launcherHoverActive() {
    643    return this.#props.launcherHoverActive;
    644  }
    645 
    646  set launcherHoverActive(active) {
    647    this.#props.launcherHoverActive = active;
    648  }
    649 
    650  get launcherWidth() {
    651    return this.#props.launcherWidth;
    652  }
    653 
    654  set launcherWidth(width) {
    655    this.#props.launcherWidth = width;
    656    const { document } = this.#controllerGlobal;
    657    if (!document.documentElement.hasAttribute("inDOMFullscreen")) {
    658      this.#panelEl.style.maxWidth = `calc(${SIDEBAR_MAXIMUM_WIDTH} - ${width}px)`;
    659      // Expand the launcher when it gets wide enough.
    660      if (this.launcherDragActive) {
    661        this.launcherExpanded = width >= LAUNCHER_MINIMUM_WIDTH;
    662      }
    663    }
    664  }
    665 
    666  get expandedLauncherWidth() {
    667    return this.#props.expandedLauncherWidth;
    668  }
    669 
    670  set expandedLauncherWidth(width) {
    671    this.#props.expandedLauncherWidth = width;
    672    this.#updateLauncherWidth();
    673  }
    674 
    675  /**
    676   * If the sidebar is expanded, resize the launcher to the user-preferred
    677   * width (if available). If it is collapsed, reset the launcher width.
    678   */
    679  #updateLauncherWidth() {
    680    if (this.launcherExpanded && this.expandedLauncherWidth) {
    681      this.#launcherContainerEl.style.width = `${this.expandedLauncherWidth}px`;
    682    } else if (!this.launcherExpanded) {
    683      this.#launcherContainerEl.style.width = "";
    684    }
    685    this.#launcherEl.toggleAttribute(
    686      "customWidth",
    687      !!this.expandedLauncherWidth
    688    );
    689  }
    690 
    691  get pinnedTabsHeight() {
    692    return this.#props.pinnedTabsHeight;
    693  }
    694 
    695  set pinnedTabsHeight(height) {
    696    this.#props.pinnedTabsHeight = height;
    697    if (this.launcherExpanded && lazy.verticalTabsEnabled) {
    698      this.expandedPinnedTabsHeight = height;
    699    } else if (lazy.verticalTabsEnabled) {
    700      this.collapsedPinnedTabsHeight = height;
    701    }
    702  }
    703 
    704  get toolsHeight() {
    705    return this.#props.toolsHeight;
    706  }
    707 
    708  set toolsHeight(height) {
    709    this.#props.toolsHeight = height;
    710    if (this.launcherExpanded) {
    711      this.expandedToolsHeight = height;
    712    } else {
    713      this.collapsedToolsHeight = height;
    714    }
    715  }
    716 
    717  /**
    718   * When the sidebar is expanded/collapsed, resize the pinned tabs container to the user-preferred
    719   * height (if available).
    720   */
    721  updatePinnedTabsHeight() {
    722    if (!lazy.verticalTabsEnabled) {
    723      if (this.#pinnedTabsContainerEl) {
    724        this.#pinnedTabsContainerEl.style.height = "";
    725      }
    726      return;
    727    }
    728    if (this.launcherExpanded && this.expandedPinnedTabsHeight) {
    729      this.#pinnedTabsContainerEl.style.height = `${this.expandedPinnedTabsHeight}px`;
    730    } else if (!this.launcherExpanded && this.collapsedPinnedTabsHeight) {
    731      this.#pinnedTabsContainerEl.style.height = `${this.collapsedPinnedTabsHeight}px`;
    732    }
    733  }
    734 
    735  /**
    736   * When the sidebar is expanded or collapsed, resize the tools container to the expected height.
    737   */
    738  handleUpdateToolsHeightOnLauncherExpanded() {
    739    if (!this.toolsDragActive) {
    740      if (this.#controller.sidebarRevampVisibility !== "expand-on-hover") {
    741        this.updateToolsHeight();
    742      } else if (this.#toolsContainer) {
    743        this.#toolsContainer.style.height = this.#props.launcherExpanded
    744          ? ""
    745          : "0";
    746      }
    747    }
    748  }
    749 
    750  /**
    751   * Resize the tools container to the user-preferred height (if available).
    752   */
    753  updateToolsHeight() {
    754    if (this.#toolsContainer) {
    755      if (!lazy.verticalTabsEnabled) {
    756        this.#toolsContainer.style.height = "";
    757        return;
    758      }
    759 
    760      if (
    761        this.launcherExpanded &&
    762        this.#props.expandedToolsHeight !== undefined
    763      ) {
    764        this.#toolsContainer.style.height = `${this.#props.expandedToolsHeight}px`;
    765      } else if (
    766        !this.launcherExpanded &&
    767        this.#props.collapsedToolsHeight !== undefined
    768      ) {
    769        this.#toolsContainer.style.height = `${this.#props.collapsedToolsHeight}px`;
    770      } else if (
    771        (this.launcherExpanded &&
    772          this.#props.expandedToolsHeight === undefined) ||
    773        (!this.launcherExpanded &&
    774          this.#props.collapsedToolsHeight === undefined)
    775      ) {
    776        this.#toolsContainer.style.height = "";
    777      }
    778    }
    779  }
    780 
    781  #updateTabbrowser(isSidebarShown) {
    782    this.#controllerGlobal.document
    783      .getElementById("tabbrowser-tabbox")
    784      .toggleAttribute("sidebar-shown", isSidebarShown);
    785  }
    786 
    787  get command() {
    788    return this.#props.command || "";
    789  }
    790 
    791  set command(id) {
    792    if (id && !this.#controller.sidebars.has(id)) {
    793      throw new Error("Setting command to an invalid value");
    794    }
    795    if (id && id !== this.#props.command) {
    796      this.#props.command = id;
    797      // We need the attribute to mirror the command property as its used as a CSS hook
    798      this.#controller._box.setAttribute("sidebarcommand", id);
    799    } else if (!id) {
    800      delete this.#props.command;
    801      this.#controller._box.setAttribute("sidebarcommand", "");
    802    }
    803  }
    804 }
    805 
    806 /**
    807 * Convert a value to an integer.
    808 *
    809 * @param {string} value
    810 *   The value to convert.
    811 * @returns {number}
    812 *   The resulting integer, or `undefined` if it's not a number.
    813 */
    814 function convertToInt(value) {
    815  const intValue = parseInt(value);
    816  if (isNaN(intValue)) {
    817    return undefined;
    818  }
    819  return intValue;
    820 }