tor-browser

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

tabs.js (57308B)


      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 "use strict";
      6 
      7 // This is loaded into all browser windows. Wrap in a block to prevent
      8 // leaking to window scope.
      9 {
     10  const DIRECTION_BACKWARD = -1;
     11  const DIRECTION_FORWARD = 1;
     12 
     13  const isTab = element => gBrowser.isTab(element);
     14  const isTabGroup = element => gBrowser.isTabGroup(element);
     15  const isTabGroupLabel = element => gBrowser.isTabGroupLabel(element);
     16  const isSplitViewWrapper = element => gBrowser.isSplitViewWrapper(element);
     17 
     18  class MozTabbrowserTabs extends MozElements.TabsBase {
     19    static observedAttributes = ["orient"];
     20 
     21    #mustUpdateTabMinHeight = false;
     22    #tabMinHeight = 36;
     23    #animatingGroups = new Set();
     24 
     25    constructor() {
     26      super();
     27 
     28      this.addEventListener("TabSelect", this);
     29      this.addEventListener("TabClose", this);
     30      this.addEventListener("TabAttrModified", this);
     31      this.addEventListener("TabHide", this);
     32      this.addEventListener("TabShow", this);
     33      this.addEventListener("TabHoverStart", this);
     34      this.addEventListener("TabHoverEnd", this);
     35      this.addEventListener("TabGroupLabelHoverStart", this);
     36      this.addEventListener("TabGroupLabelHoverEnd", this);
     37      // Capture collapse/expand early so we mark animating groups before
     38      // overflow/underflow handlers run.
     39      this.addEventListener("TabGroupExpand", this, true);
     40      this.addEventListener("TabGroupCollapse", this, true);
     41      this.addEventListener("TabGroupAnimationComplete", this);
     42      this.addEventListener("TabGroupCreate", this);
     43      this.addEventListener("TabGroupRemoved", this);
     44      this.addEventListener("SplitViewCreated", this);
     45      this.addEventListener("SplitViewRemoved", this);
     46      this.addEventListener("transitionend", this);
     47      this.addEventListener("dblclick", this);
     48      this.addEventListener("click", this);
     49      this.addEventListener("click", this, true);
     50      this.addEventListener("keydown", this, { mozSystemGroup: true });
     51      this.addEventListener("mouseleave", this);
     52      this.addEventListener("focusin", this);
     53      this.addEventListener("focusout", this);
     54      this.addEventListener("contextmenu", this);
     55      this.addEventListener("dragstart", this);
     56      this.addEventListener("dragover", this);
     57      this.addEventListener("drop", this);
     58      this.addEventListener("dragend", this);
     59      this.addEventListener("dragleave", this);
     60    }
     61 
     62    init() {
     63      this.startupTime = Services.startup.getStartupInfo().start.getTime();
     64 
     65      this.arrowScrollbox = document.getElementById(
     66        "tabbrowser-arrowscrollbox"
     67      );
     68      this.arrowScrollbox.addEventListener("wheel", this, true);
     69      this.arrowScrollbox.addEventListener("underflow", this);
     70      this.arrowScrollbox.addEventListener("overflow", this);
     71      this.pinnedTabsContainer = document.getElementById(
     72        "pinned-tabs-container"
     73      );
     74      this.pinnedTabsContainer.setAttribute(
     75        "orient",
     76        this.getAttribute("orient")
     77      );
     78 
     79      // Override arrowscrollbox.js method, since our scrollbox's children are
     80      // inherited from the scrollbox binding parent (this).
     81      this.arrowScrollbox._getScrollableElements = () => {
     82        return this.ariaFocusableItems.reduce((elements, item) => {
     83          if (this.arrowScrollbox._canScrollToElement(item)) {
     84            elements.push(item);
     85            if (
     86              isTab(item) &&
     87              item.group &&
     88              item.group.collapsed &&
     89              item.selected
     90            ) {
     91              // overflow container is scrollable, but not in focus order
     92              elements.push(item.group.overflowContainer);
     93            }
     94          }
     95          return elements;
     96        }, []);
     97      };
     98      this.arrowScrollbox._canScrollToElement = element => {
     99        if (isTab(element)) {
    100          return !element.pinned;
    101        }
    102        return true;
    103      };
    104 
    105      // Override for performance reasons. This is the size of a single element
    106      // that can be scrolled when using mouse wheel scrolling. If we don't do
    107      // this then arrowscrollbox computes this value by calling
    108      // _getScrollableElements and dividing the box size by that number.
    109      // However in the tabstrip case we already know the answer to this as,
    110      // when we're overflowing, it is always the same as the tab min width or
    111      // height. For tab group labels, the number won't exactly match, but
    112      // that shouldn't be a problem in practice since the arrowscrollbox
    113      // stops at element bounds when finishing scrolling.
    114      Object.defineProperty(this.arrowScrollbox, "lineScrollAmount", {
    115        get: () =>
    116          this.verticalMode ? this.#tabMinHeight : this._tabMinWidthPref,
    117      });
    118 
    119      this.baseConnect();
    120 
    121      this._blockDblClick = false;
    122      this._closeButtonsUpdatePending = false;
    123      this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
    124      this._tabDefaultMaxWidth = NaN;
    125      this._lastTabClosedByMouse = false;
    126      this._hasTabTempMaxWidth = false;
    127      this._scrollButtonWidth = 0;
    128      this._animateElement = this.arrowScrollbox;
    129      this._tabClipWidth = Services.prefs.getIntPref(
    130        "browser.tabs.tabClipWidth"
    131      );
    132      this._hiddenSoundPlayingTabs = new Set();
    133      this.previewPanel = null;
    134 
    135      this.allTabs[0].label = this.emptyTabTitle;
    136 
    137      // Hide the secondary text for locales where it is unsupported due to size constraints.
    138      const language = Services.locale.appLocaleAsBCP47;
    139      const unsupportedLocales = Services.prefs.getCharPref(
    140        "browser.tabs.secondaryTextUnsupportedLocales"
    141      );
    142      this.toggleAttribute(
    143        "secondarytext-unsupported",
    144        unsupportedLocales.split(",").includes(language.split("-")[0])
    145      );
    146 
    147      this.newTabButton.setAttribute(
    148        "aria-label",
    149        DynamicShortcutTooltip.getText("tabs-newtab-button")
    150      );
    151 
    152      let handleResize = () => {
    153        this._updateCloseButtons();
    154        this._handleTabSelect(true);
    155      };
    156      window.addEventListener("resize", handleResize);
    157      this._fullscreenMutationObserver = new MutationObserver(handleResize);
    158      this._fullscreenMutationObserver.observe(document.documentElement, {
    159        attributeFilter: ["inFullscreen", "inDOMFullscreen"],
    160      });
    161 
    162      this.boundObserve = (...args) => this.observe(...args);
    163      Services.prefs.addObserver("privacy.userContext", this.boundObserve);
    164      this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
    165 
    166      document
    167        .getElementById("vertical-tabs-newtab-button")
    168        .addEventListener("keypress", this);
    169      document
    170        .getElementById("tabs-newtab-button")
    171        .addEventListener("keypress", this);
    172 
    173      XPCOMUtils.defineLazyPreferenceGetter(
    174        this,
    175        "_tabMinWidthPref",
    176        "browser.tabs.tabMinWidth",
    177        null,
    178        (pref, prevValue, newValue) => this.#updateTabMinWidth(newValue),
    179        newValue => {
    180          const LIMIT = 50;
    181          return Math.max(newValue, LIMIT);
    182        }
    183      );
    184      this.#updateTabMinWidth(this._tabMinWidthPref);
    185      this.#updateTabMinHeight();
    186 
    187      CustomizableUI.addListener(this);
    188      this._updateNewTabVisibility();
    189 
    190      XPCOMUtils.defineLazyPreferenceGetter(
    191        this,
    192        "_closeTabByDblclick",
    193        "browser.tabs.closeTabByDblclick",
    194        false
    195      );
    196 
    197      XPCOMUtils.defineLazyPreferenceGetter(
    198        this,
    199        "_sidebarVisibility",
    200        "sidebar.visibility",
    201        "always-show"
    202      );
    203 
    204      XPCOMUtils.defineLazyPreferenceGetter(
    205        this,
    206        "_sidebarPositionStart",
    207        "sidebar.position_start",
    208        true
    209      );
    210 
    211      if (gMultiProcessBrowser) {
    212        this.tabbox.tabpanels.setAttribute("async", "true");
    213      }
    214 
    215      XPCOMUtils.defineLazyPreferenceGetter(
    216        this,
    217        "_showTabHoverPreview",
    218        "browser.tabs.hoverPreview.enabled",
    219        false
    220      );
    221      XPCOMUtils.defineLazyPreferenceGetter(
    222        this,
    223        "_showTabGroupHoverPreview",
    224        "browser.tabs.groups.hoverPreview.enabled",
    225        false
    226      );
    227 
    228      this.tooltip = "tabbrowser-tab-tooltip";
    229 
    230      Services.prefs.addObserver(
    231        "browser.tabs.dragDrop.multiselectStacking",
    232        this.boundObserve
    233      );
    234      this.observe(
    235        null,
    236        "nsPref:changed",
    237        "browser.tabs.dragDrop.multiselectStacking"
    238      );
    239    }
    240 
    241    #initializeDragAndDrop() {
    242      this.tabDragAndDrop = Services.prefs.getBoolPref(
    243        "browser.tabs.dragDrop.multiselectStacking",
    244        true
    245      )
    246        ? new window.TabStacking(this)
    247        : new window.TabDragAndDrop(this);
    248      this.tabDragAndDrop.init();
    249    }
    250 
    251    attributeChangedCallback(name, oldValue, newValue) {
    252      if (name == "orient") {
    253        // reset this attribute so we don't have incorrect styling for vertical tabs
    254        this.removeAttribute("overflow");
    255        this.#updateTabMinWidth();
    256        this.#updateTabMinHeight();
    257        this.pinnedTabsContainer?.setAttribute("orient", newValue);
    258      }
    259      super.attributeChangedCallback(name, oldValue, newValue);
    260    }
    261 
    262    // Event handlers
    263 
    264    handleEvent(aEvent) {
    265      switch (aEvent.type) {
    266        case "mouseout": {
    267          // If the "related target" (the node to which the pointer went) is not
    268          // a child of the current document, the mouse just left the window.
    269          let relatedTarget = aEvent.relatedTarget;
    270          if (relatedTarget && relatedTarget.ownerDocument == document) {
    271            break;
    272          }
    273        }
    274        // fall through
    275        case "mousemove":
    276          if (
    277            document.getElementById("tabContextMenu").state != "open" &&
    278            !this.#isMovingTab()
    279          ) {
    280            this._unlockTabSizing();
    281          }
    282          break;
    283        case "mouseleave":
    284          this.previewPanel?.deactivate();
    285          break;
    286        default: {
    287          let methodName = `on_${aEvent.type}`;
    288          if (methodName in this) {
    289            this[methodName](aEvent);
    290          } else {
    291            throw new Error(`Unexpected event ${aEvent.type}`);
    292          }
    293        }
    294      }
    295    }
    296 
    297    /**
    298     * @param {CustomEvent} event
    299     */
    300    on_TabSelect(event) {
    301      const {
    302        target: newTab,
    303        detail: { previousTab },
    304      } = event;
    305 
    306      // In some cases (e.g. by selecting a tab in a collapsed tab group),
    307      // changing the selected tab may cause a tab to appear/disappear.
    308      if (previousTab.group?.collapsed || newTab.group?.collapsed) {
    309        this._invalidateCachedVisibleTabs();
    310      }
    311      this._handleTabSelect();
    312    }
    313 
    314    on_TabClose(event) {
    315      this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
    316    }
    317 
    318    on_TabAttrModified(event) {
    319      if (
    320        event.detail.changed.includes("soundplaying") &&
    321        !event.target.visible
    322      ) {
    323        this._hiddenSoundPlayingStatusChanged(event.target);
    324      }
    325      if (
    326        event.detail.changed.includes("soundplaying") ||
    327        event.detail.changed.includes("muted") ||
    328        event.detail.changed.includes("activemedia-blocked")
    329      ) {
    330        this.updateTabSoundLabel(event.target);
    331      }
    332    }
    333 
    334    on_TabHide(event) {
    335      if (event.target.soundPlaying) {
    336        this._hiddenSoundPlayingStatusChanged(event.target);
    337      }
    338    }
    339 
    340    on_TabShow(event) {
    341      if (event.target.soundPlaying) {
    342        this._hiddenSoundPlayingStatusChanged(event.target);
    343      }
    344    }
    345 
    346    on_TabHoverStart(event) {
    347      if (!this._showTabHoverPreview) {
    348        return;
    349      }
    350      this.ensureTabPreviewPanelLoaded();
    351      this.previewPanel.activate(event.target);
    352    }
    353 
    354    on_TabHoverEnd(event) {
    355      this.previewPanel?.deactivate(event.target);
    356    }
    357 
    358    cancelTabGroupPreview() {
    359      this.previewPanel?.panelOpener.clear();
    360    }
    361 
    362    showTabGroupPreview(group) {
    363      if (!this._showTabGroupHoverPreview) {
    364        return;
    365      }
    366      this.ensureTabPreviewPanelLoaded();
    367      this.previewPanel.activate(group);
    368    }
    369 
    370    on_TabGroupLabelHoverStart(event) {
    371      this.showTabGroupPreview(event.target.group);
    372    }
    373 
    374    on_TabGroupLabelHoverEnd(event) {
    375      this.previewPanel?.deactivate(event.target.group);
    376    }
    377 
    378    on_TabGroupExpand(event) {
    379      this._invalidateCachedVisibleTabs();
    380      this.#animatingGroups.add(event.target.id);
    381    }
    382 
    383    on_TabGroupCollapse(event) {
    384      this._invalidateCachedVisibleTabs();
    385      this._unlockTabSizing();
    386      this.#animatingGroups.add(event.target.id);
    387    }
    388 
    389    on_TabGroupAnimationComplete(event) {
    390      // Delay clearing the animating flag so overflow/underflow handlers
    391      // triggered by the size change can observe it and skip auto-scroll.
    392      window.requestAnimationFrame(() => {
    393        this.#animatingGroups.delete(event.target.id);
    394      });
    395    }
    396 
    397    on_TabGroupCreate() {
    398      this._invalidateCachedTabs();
    399    }
    400 
    401    on_TabGroupRemoved() {
    402      this._invalidateCachedTabs();
    403    }
    404 
    405    on_SplitViewCreated() {
    406      this._invalidateCachedTabs();
    407    }
    408 
    409    on_SplitViewRemoved() {
    410      this._invalidateCachedTabs();
    411    }
    412 
    413    /**
    414     * @param {TransitionEvent} event
    415     */
    416    on_transitionend(event) {
    417      if (event.propertyName != "max-width") {
    418        return;
    419      }
    420 
    421      let tab = event.target?.closest("tab");
    422 
    423      if (!tab) {
    424        return;
    425      }
    426 
    427      if (tab.hasAttribute("fadein")) {
    428        if (tab._fullyOpen) {
    429          this._updateCloseButtons();
    430        } else {
    431          this._handleNewTab(tab);
    432        }
    433      } else if (tab.closing) {
    434        gBrowser._endRemoveTab(tab);
    435      }
    436 
    437      let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
    438      tab.dispatchEvent(evt);
    439    }
    440 
    441    on_dblclick(event) {
    442      // When the tabbar has an unified appearance with the titlebar
    443      // and menubar, a double-click in it should have the same behavior
    444      // as double-clicking the titlebar
    445      if (CustomTitlebar.enabled && !this.verticalMode) {
    446        return;
    447      }
    448 
    449      // Make sure it is the primary button, we are hitting our arrowscrollbox,
    450      // and we're not hitting the scroll buttons.
    451      if (
    452        event.button != 0 ||
    453        event.target != this.arrowScrollbox ||
    454        event.composedTarget.localName == "toolbarbutton"
    455      ) {
    456        return;
    457      }
    458 
    459      if (!this._blockDblClick) {
    460        BrowserCommands.openTab();
    461      }
    462 
    463      event.preventDefault();
    464    }
    465 
    466    on_click(event) {
    467      if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
    468        /* Catches extra clicks meant for the in-tab close button.
    469         * Placed here to avoid leaking (a temporary handler added from the
    470         * in-tab close button binding would close over the tab and leak it
    471         * until the handler itself was removed). (bug 897751)
    472         *
    473         * The only sequence in which a second click event (i.e. dblclik)
    474         * can be dispatched on an in-tab close button is when it is shown
    475         * after the first click (i.e. the first click event was dispatched
    476         * on the tab). This happens when we show the close button only on
    477         * the active tab. (bug 352021)
    478         * The only sequence in which a third click event can be dispatched
    479         * on an in-tab close button is when the tab was opened with a
    480         * double click on the tabbar. (bug 378344)
    481         * In both cases, it is most likely that the close button area has
    482         * been accidentally clicked, therefore we do not close the tab.
    483         *
    484         * We don't want to ignore processing of more than one click event,
    485         * though, since the user might actually be repeatedly clicking to
    486         * close many tabs at once.
    487         */
    488        let target = event.originalTarget;
    489        if (target.classList.contains("tab-close-button")) {
    490          // We preemptively set this to allow the closing-multiple-tabs-
    491          // in-a-row case.
    492          if (this._blockDblClick) {
    493            target._ignoredCloseButtonClicks = true;
    494          } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
    495            target._ignoredCloseButtonClicks = true;
    496            event.stopPropagation();
    497            return;
    498          } else {
    499            // Reset the "ignored click" flag
    500            target._ignoredCloseButtonClicks = false;
    501          }
    502        }
    503 
    504        /* Protects from close-tab-button errant doubleclick:
    505         * Since we're removing the event target, if the user
    506         * double-clicks the button, the dblclick event will be dispatched
    507         * with the tabbar as its event target (and explicit/originalTarget),
    508         * which treats that as a mouse gesture for opening a new tab.
    509         * In this context, we're manually blocking the dblclick event.
    510         */
    511        if (this._blockDblClick) {
    512          if (!("_clickedTabBarOnce" in this)) {
    513            this._clickedTabBarOnce = true;
    514            return;
    515          }
    516          delete this._clickedTabBarOnce;
    517          this._blockDblClick = false;
    518        }
    519      } else if (
    520        event.eventPhase == Event.BUBBLING_PHASE &&
    521        event.button == 1
    522      ) {
    523        let tab = event.target?.closest("tab");
    524        if (tab) {
    525          if (tab.multiselected) {
    526            gBrowser.removeMultiSelectedTabs();
    527          } else {
    528            gBrowser.removeTab(tab, {
    529              animate: true,
    530              triggeringEvent: event,
    531            });
    532          }
    533        } else if (isTabGroupLabel(event.target)) {
    534          event.target.group.saveAndClose();
    535        } else if (
    536          event.originalTarget.closest("scrollbox") &&
    537          !Services.prefs.getBoolPref(
    538            "widget.gtk.titlebar-action-middle-click-enabled"
    539          )
    540        ) {
    541          // Check whether the click
    542          // was dispatched on the open space of it.
    543          let visibleTabs = this.visibleTabs;
    544          let lastTab = visibleTabs.at(-1);
    545          let winUtils = window.windowUtils;
    546          let endOfTab =
    547            winUtils.getBoundsWithoutFlushing(lastTab)[
    548              (this.verticalMode && "bottom") ||
    549                (this.#rtlMode ? "left" : "right")
    550            ];
    551          if (
    552            (this.verticalMode && event.clientY > endOfTab) ||
    553            (!this.verticalMode &&
    554              (this.#rtlMode
    555                ? event.clientX < endOfTab
    556                : event.clientX > endOfTab))
    557          ) {
    558            BrowserCommands.openTab();
    559          }
    560        } else {
    561          return;
    562        }
    563 
    564        event.preventDefault();
    565        event.stopPropagation();
    566      }
    567    }
    568 
    569    on_keydown(event) {
    570      let { altKey, shiftKey } = event;
    571      let [accel, nonAccel] =
    572        AppConstants.platform == "macosx"
    573          ? [event.metaKey, event.ctrlKey]
    574          : [event.ctrlKey, event.metaKey];
    575 
    576      let keyComboForFocusedElement =
    577        !accel && !shiftKey && !altKey && !nonAccel;
    578      let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
    579      let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;
    580 
    581      if (!keyComboForFocusedElement && !keyComboForMove && !keyComboForFocus) {
    582        return;
    583      }
    584 
    585      if (keyComboForFocusedElement) {
    586        let ariaFocusedItem = this.ariaFocusedItem;
    587        if (isTabGroupLabel(ariaFocusedItem)) {
    588          switch (event.keyCode) {
    589            case KeyEvent.DOM_VK_SPACE:
    590            case KeyEvent.DOM_VK_RETURN: {
    591              ariaFocusedItem.click();
    592              event.preventDefault();
    593            }
    594          }
    595        }
    596      } else if (keyComboForMove) {
    597        switch (event.keyCode) {
    598          case KeyEvent.DOM_VK_UP:
    599            gBrowser.moveTabBackward();
    600            break;
    601          case KeyEvent.DOM_VK_DOWN:
    602            gBrowser.moveTabForward();
    603            break;
    604          case KeyEvent.DOM_VK_RIGHT:
    605            if (RTL_UI) {
    606              gBrowser.moveTabBackward();
    607            } else {
    608              gBrowser.moveTabForward();
    609            }
    610            break;
    611          case KeyEvent.DOM_VK_LEFT:
    612            if (RTL_UI) {
    613              gBrowser.moveTabForward();
    614            } else {
    615              gBrowser.moveTabBackward();
    616            }
    617            break;
    618          case KeyEvent.DOM_VK_HOME:
    619            gBrowser.moveTabToStart();
    620            break;
    621          case KeyEvent.DOM_VK_END:
    622            gBrowser.moveTabToEnd();
    623            break;
    624          default:
    625            // Consume the keydown event for the above keyboard
    626            // shortcuts only.
    627            return;
    628        }
    629 
    630        event.preventDefault();
    631      } else if (keyComboForFocus) {
    632        switch (event.keyCode) {
    633          case KeyEvent.DOM_VK_UP:
    634            this.#advanceFocus(DIRECTION_BACKWARD);
    635            break;
    636          case KeyEvent.DOM_VK_DOWN:
    637            this.#advanceFocus(DIRECTION_FORWARD);
    638            break;
    639          case KeyEvent.DOM_VK_RIGHT:
    640            if (RTL_UI) {
    641              this.#advanceFocus(DIRECTION_BACKWARD);
    642            } else {
    643              this.#advanceFocus(DIRECTION_FORWARD);
    644            }
    645            break;
    646          case KeyEvent.DOM_VK_LEFT:
    647            if (RTL_UI) {
    648              this.#advanceFocus(DIRECTION_FORWARD);
    649            } else {
    650              this.#advanceFocus(DIRECTION_BACKWARD);
    651            }
    652            break;
    653          case KeyEvent.DOM_VK_HOME:
    654            this.ariaFocusedItem = this.ariaFocusableItems.at(0);
    655            break;
    656          case KeyEvent.DOM_VK_END:
    657            this.ariaFocusedItem = this.ariaFocusableItems.at(-1);
    658            break;
    659          case KeyEvent.DOM_VK_SPACE: {
    660            let ariaFocusedItem = this.ariaFocusedItem;
    661            if (isTab(ariaFocusedItem)) {
    662              if (ariaFocusedItem.multiselected) {
    663                gBrowser.removeFromMultiSelectedTabs(ariaFocusedItem);
    664              } else {
    665                gBrowser.addToMultiSelectedTabs(ariaFocusedItem);
    666              }
    667            }
    668            break;
    669          }
    670          default:
    671            // Consume the keydown event for the above keyboard
    672            // shortcuts only.
    673            return;
    674        }
    675 
    676        event.preventDefault();
    677      }
    678    }
    679 
    680    /**
    681     * @param {FocusEvent} event
    682     */
    683    on_focusin(event) {
    684      if (event.target == this.selectedItem) {
    685        this.tablistHasFocus = true;
    686        if (!this.ariaFocusedItem) {
    687          // If the active tab is receiving focus and there isn't a keyboard
    688          // focus target yet, set the keyboard focus target to the active
    689          // tab. Do not override the keyboard-focused item if the user
    690          // already set a keyboard focus.
    691          this.ariaFocusedItem = this.selectedItem;
    692        }
    693      }
    694      let focusReturnedFromGroupPanel = event.relatedTarget?.classList.contains(
    695        "group-preview-button"
    696      );
    697      if (
    698        !focusReturnedFromGroupPanel &&
    699        this.tablistHasFocus &&
    700        isTabGroupLabel(this.ariaFocusedItem)
    701      ) {
    702        this.showTabGroupPreview(this.ariaFocusedItem.group);
    703      }
    704    }
    705 
    706    /**
    707     * @param {FocusEvent} event
    708     */
    709    on_focusout(event) {
    710      this.cancelTabGroupPreview();
    711      if (event.target == this.selectedItem) {
    712        this.tablistHasFocus = false;
    713      }
    714    }
    715 
    716    on_keypress(event) {
    717      if (event.defaultPrevented) {
    718        return;
    719      }
    720      if (event.key == " " || event.key == "Enter") {
    721        event.preventDefault();
    722        event.target.click();
    723      }
    724    }
    725 
    726    on_dragstart(event) {
    727      this.tabDragAndDrop.handle_dragstart(event);
    728    }
    729 
    730    on_dragover(event) {
    731      this.tabDragAndDrop.handle_dragover(event);
    732    }
    733 
    734    on_drop(event) {
    735      this.tabDragAndDrop.handle_drop(event);
    736    }
    737 
    738    on_dragend(event) {
    739      this.tabDragAndDrop.handle_dragend(event);
    740    }
    741 
    742    on_dragleave(event) {
    743      this.tabDragAndDrop.handle_dragleave(event);
    744    }
    745 
    746    on_wheel(event) {
    747      if (
    748        Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false)
    749      ) {
    750        event.stopImmediatePropagation();
    751      }
    752    }
    753 
    754    on_overflow(event) {
    755      // Ignore overflow events from nested scrollable elements
    756      if (event.target != this.arrowScrollbox) {
    757        return;
    758      }
    759 
    760      this.toggleAttribute("overflow", true);
    761      this._updateCloseButtons();
    762 
    763      if (!this.#animatingGroups.size) {
    764        this._handleTabSelect(true);
    765      }
    766 
    767      document
    768        .getElementById("tab-preview-panel")
    769        ?.setAttribute("rolluponmousewheel", true);
    770    }
    771 
    772    on_underflow(event) {
    773      // Ignore underflow events:
    774      // - from nested scrollable elements
    775      // - corresponding to an overflow event that we ignored
    776      if (event.target != this.arrowScrollbox || !this.overflowing) {
    777        return;
    778      }
    779 
    780      this.removeAttribute("overflow");
    781 
    782      if (this._lastTabClosedByMouse) {
    783        this._expandSpacerBy(this._scrollButtonWidth);
    784      }
    785 
    786      for (let tab of gBrowser._removingTabs) {
    787        gBrowser.removeTab(tab);
    788      }
    789 
    790      this._updateCloseButtons();
    791 
    792      document
    793        .getElementById("tab-preview-panel")
    794        ?.removeAttribute("rolluponmousewheel");
    795    }
    796 
    797    on_contextmenu(event) {
    798      // When pressing the context menu key (as opposed to right-clicking)
    799      // while a tab group label has aria focus (as opposed to DOM focus),
    800      // open the tab group context menu as if the label had DOM focus.
    801      // The button property is used to differentiate between key and mouse.
    802      if (event.button == 0 && isTabGroupLabel(this.ariaFocusedItem)) {
    803        gBrowser.tabGroupMenu.openEditModal(this.ariaFocusedItem.group);
    804        event.preventDefault();
    805      }
    806    }
    807 
    808    // Utilities
    809 
    810    get emptyTabTitle() {
    811      // Normal tab title is used also in the permanent private browsing mode.
    812      const l10nId =
    813        PrivateBrowsingUtils.isWindowPrivate(window) &&
    814        !Services.prefs.getBoolPref("browser.privatebrowsing.autostart")
    815          ? "tabbrowser-empty-private-tab-title"
    816          : "tabbrowser-empty-tab-title";
    817      return gBrowser.tabLocalization.formatValueSync(l10nId);
    818    }
    819 
    820    get tabbox() {
    821      return document.getElementById("tabbrowser-tabbox");
    822    }
    823 
    824    get newTabButton() {
    825      return this.querySelector("#tabs-newtab-button");
    826    }
    827 
    828    get verticalMode() {
    829      return this.getAttribute("orient") == "vertical";
    830    }
    831 
    832    get expandOnHover() {
    833      return this._sidebarVisibility == "expand-on-hover";
    834    }
    835 
    836    get #rtlMode() {
    837      return !this.verticalMode && RTL_UI;
    838    }
    839 
    840    get overflowing() {
    841      return this.hasAttribute("overflow");
    842    }
    843 
    844    #allTabs;
    845    get allTabs() {
    846      if (this.#allTabs) {
    847        return this.#allTabs;
    848      }
    849      // Remove temporary periphery element added at drag start.
    850      let pinnedChildren = Array.from(this.pinnedTabsContainer.children);
    851      if (pinnedChildren?.at(-1)?.id == "pinned-tabs-container-periphery") {
    852        pinnedChildren.pop();
    853      }
    854      let unpinnedChildren = Array.from(this.arrowScrollbox.children);
    855      // remove arrowScrollbox periphery element.
    856      unpinnedChildren.pop();
    857 
    858      // explode tab groups and split view wrappers
    859      // Iterate backwards over the array to preserve indices while we modify
    860      // things in place
    861      for (let i = unpinnedChildren.length - 1; i >= 0; i--) {
    862        if (
    863          unpinnedChildren[i].tagName == "tab-group" ||
    864          unpinnedChildren[i].tagName == "tab-split-view-wrapper"
    865        ) {
    866          unpinnedChildren.splice(i, 1, ...unpinnedChildren[i].tabs);
    867        }
    868      }
    869 
    870      this.#allTabs = [...pinnedChildren, ...unpinnedChildren];
    871      return this.#allTabs;
    872    }
    873 
    874    get allGroups() {
    875      let children = Array.from(this.arrowScrollbox.children);
    876      return children.filter(node => node.tagName == "tab-group");
    877    }
    878 
    879    /**
    880     * Returns all tabs in the current window, including hidden tabs and tabs
    881     * in collapsed groups, but excluding closing tabs and the Firefox View tab.
    882     */
    883    get openTabs() {
    884      if (!this.#openTabs) {
    885        this.#openTabs = this.allTabs.filter(tab => tab.isOpen);
    886      }
    887      return this.#openTabs;
    888    }
    889    #openTabs;
    890 
    891    /**
    892     * Same as `openTabs` but excluding hidden tabs.
    893     */
    894    get nonHiddenTabs() {
    895      if (!this.#nonHiddenTabs) {
    896        this.#nonHiddenTabs = this.openTabs.filter(tab => !tab.hidden);
    897      }
    898      return this.#nonHiddenTabs;
    899    }
    900    #nonHiddenTabs;
    901 
    902    /**
    903     * Same as `openTabs` but excluding hidden tabs and tabs in collapsed groups.
    904     */
    905    get visibleTabs() {
    906      if (!this.#visibleTabs) {
    907        this.#visibleTabs = this.openTabs.filter(tab => tab.visible);
    908      }
    909      return this.#visibleTabs;
    910    }
    911    #visibleTabs;
    912 
    913    /**
    914     * @returns {boolean} true if the keyboard focus is on the active tab
    915     */
    916    get tablistHasFocus() {
    917      return this.hasAttribute("tablist-has-focus");
    918    }
    919 
    920    /**
    921     * @param {boolean} hasFocus true if the keyboard focus is on the active tab
    922     */
    923    set tablistHasFocus(hasFocus) {
    924      this.toggleAttribute("tablist-has-focus", hasFocus);
    925    }
    926 
    927    /** @typedef {MozTabbrowserTab|MozTextLabel} FocusableItem */
    928 
    929    /** @type {FocusableItem[]} */
    930    #focusableItems;
    931 
    932    /** @type {dragAndDropElements[]} */
    933    #dragAndDropElements;
    934 
    935    /**
    936     * @returns {FocusableItem[]}
    937     * @override
    938     */
    939    get ariaFocusableItems() {
    940      if (this.#focusableItems) {
    941        return this.#focusableItems;
    942      }
    943 
    944      let unpinnedChildren = Array.from(this.arrowScrollbox.children);
    945      let pinnedChildren = Array.from(this.pinnedTabsContainer.children);
    946 
    947      let focusableItems = [];
    948      for (let child of pinnedChildren) {
    949        if (isTab(child)) {
    950          focusableItems.push(child);
    951        }
    952      }
    953      for (let child of unpinnedChildren) {
    954        if (isTab(child) && child.visible) {
    955          focusableItems.push(child);
    956        } else if (isTabGroup(child)) {
    957          focusableItems.push(child.labelElement);
    958 
    959          let visibleTabsInGroup = child.tabs.filter(tab => tab.visible);
    960          focusableItems.push(...visibleTabsInGroup);
    961        } else if (child.tagName == "tab-split-view-wrapper") {
    962          let visibleTabsInSplitView = child.tabs.filter(tab => tab.visible);
    963          focusableItems.push(...visibleTabsInSplitView);
    964        }
    965      }
    966 
    967      this.#focusableItems = focusableItems;
    968 
    969      return this.#focusableItems;
    970    }
    971 
    972    /**
    973     * @returns {dragAndDropElements[]}
    974     * Representation of every drag and drop element including tabs, tab group labels and split view wrapper.
    975     * We keep this separate from ariaFocusableItems because not every element for drag n'drop also needs to be
    976     * focusable (ex, we don't want the splitview container to be focusable, only its children).
    977     */
    978    get dragAndDropElements() {
    979      if (this.#dragAndDropElements) {
    980        return this.#dragAndDropElements;
    981      }
    982 
    983      let elementIndex = 0;
    984      let dragAndDropElements = [];
    985      let unpinnedChildren = Array.from(this.arrowScrollbox.children);
    986      let pinnedChildren = Array.from(this.pinnedTabsContainer.children);
    987 
    988      for (let child of [...pinnedChildren, ...unpinnedChildren]) {
    989        if (
    990          !(
    991            (isTab(child) && child.visible) ||
    992            isTabGroup(child) ||
    993            isSplitViewWrapper(child)
    994          )
    995        ) {
    996          continue;
    997        }
    998 
    999        if (isTabGroup(child)) {
   1000          child.labelElement.elementIndex = elementIndex++;
   1001          dragAndDropElements.push(child.labelElement);
   1002 
   1003          let tabsAndSplitViews = child.tabsAndSplitViews.filter(
   1004            node => node.visible
   1005          );
   1006          tabsAndSplitViews.forEach(ele => {
   1007            ele.elementIndex = elementIndex++;
   1008          });
   1009          dragAndDropElements.push(...tabsAndSplitViews);
   1010        } else {
   1011          child.elementIndex = elementIndex++;
   1012          dragAndDropElements.push(child);
   1013        }
   1014      }
   1015 
   1016      this.#dragAndDropElements = dragAndDropElements;
   1017      return this.#dragAndDropElements;
   1018    }
   1019 
   1020    /**
   1021     * Moves the ARIA focus in the tab strip left or right, as appropriate, to
   1022     * the next tab or tab group label.
   1023     *
   1024     * @param {-1|1} direction
   1025     */
   1026    #advanceFocus(direction) {
   1027      let currentIndex = this.ariaFocusableItems.indexOf(this.ariaFocusedItem);
   1028      let newIndex = currentIndex + direction;
   1029 
   1030      // Clamp the index so that the focus stops at the edges of the tab strip
   1031      newIndex = Math.min(
   1032        this.ariaFocusableItems.length - 1,
   1033        Math.max(0, newIndex)
   1034      );
   1035 
   1036      let itemToFocus = this.ariaFocusableItems[newIndex];
   1037      this.ariaFocusedItem = itemToFocus;
   1038 
   1039      // If the newly-focused item is a tab group label and the group is collapsed,
   1040      // proactively show the tab group preview
   1041      if (isTabGroupLabel(this.ariaFocusedItem)) {
   1042        this.showTabGroupPreview(this.ariaFocusedItem.group);
   1043      }
   1044    }
   1045 
   1046    _invalidateCachedTabs() {
   1047      this.#allTabs = null;
   1048      this._invalidateCachedVisibleTabs();
   1049    }
   1050 
   1051    _invalidateCachedVisibleTabs() {
   1052      this.#openTabs = null;
   1053      this.#nonHiddenTabs = null;
   1054      this.#visibleTabs = null;
   1055      // Focusable items must also be visible, but they do not depend on
   1056      // this.#visibleTabs, so changes to visible tabs need to also invalidate
   1057      // the focusable items and dragAndDropElements cache.
   1058      this.#focusableItems = null;
   1059      this.#dragAndDropElements = null;
   1060    }
   1061 
   1062    #isMovingTab() {
   1063      return this.hasAttribute("movingtab");
   1064    }
   1065 
   1066    isContainerVerticalPinnedGrid(tab) {
   1067      return (
   1068        tab.pinned &&
   1069        this.verticalMode &&
   1070        this.hasAttribute("expanded") &&
   1071        !this.expandOnHover
   1072      );
   1073    }
   1074 
   1075    /**
   1076     * Changes the selected tab or tab group label on the tab strip
   1077     * relative to the ARIA-focused tab strip element or the active tab. This
   1078     * is intended for traversing the tab strip visually, e.g by using keyboard
   1079     * arrows. For cases where keyboard shortcuts or other logic should only
   1080     * select tabs (and never tab group labels), see `advanceSelectedTab`.
   1081     *
   1082     * @override
   1083     * @param {-1|1} direction
   1084     * @param {boolean} shouldWrap
   1085     */
   1086    advanceSelectedItem(aDir, aWrap) {
   1087      let groupPanel = this.previewPanel?.tabGroupPanel;
   1088      if (groupPanel && groupPanel.isActive) {
   1089        // if the group panel is open, it should receive keyboard focus here
   1090        // instead of moving to the next item in the tabstrip.
   1091        groupPanel.focusPanel(aDir);
   1092        return;
   1093      }
   1094 
   1095      // cancel any pending group popup since we expect to deselect the label
   1096      this.cancelTabGroupPreview();
   1097 
   1098      let { ariaFocusableItems, ariaFocusedIndex } = this;
   1099 
   1100      // Advance relative to the ARIA-focused item if set, otherwise advance
   1101      // relative to the active tab.
   1102      let currentItemIndex =
   1103        ariaFocusedIndex >= 0
   1104          ? ariaFocusedIndex
   1105          : ariaFocusableItems.indexOf(this.selectedItem);
   1106 
   1107      let newItemIndex = currentItemIndex + aDir;
   1108 
   1109      if (aWrap) {
   1110        if (newItemIndex >= ariaFocusableItems.length) {
   1111          newItemIndex = 0;
   1112        } else if (newItemIndex < 0) {
   1113          newItemIndex = ariaFocusableItems.length - 1;
   1114        }
   1115      } else {
   1116        newItemIndex = Math.min(
   1117          ariaFocusableItems.length - 1,
   1118          Math.max(0, newItemIndex)
   1119        );
   1120      }
   1121 
   1122      if (currentItemIndex == newItemIndex) {
   1123        return;
   1124      }
   1125 
   1126      // If the next item is a tab, select it. If the next item is a tab group
   1127      // label, keep the active tab selected and just set ARIA focus on the tab
   1128      // group label.
   1129      let newItem = ariaFocusableItems[newItemIndex];
   1130      if (isTab(newItem)) {
   1131        this._selectNewTab(newItem, aDir, aWrap);
   1132      }
   1133      this.ariaFocusedItem = newItem;
   1134 
   1135      // If the newly-focused item is a tab group label and the group is collapsed,
   1136      // proactively show the tab group preview
   1137      if (isTabGroupLabel(this.ariaFocusedItem)) {
   1138        this.showTabGroupPreview(this.ariaFocusedItem.group);
   1139      }
   1140    }
   1141 
   1142    ensureTabPreviewPanelLoaded() {
   1143      if (!this.previewPanel) {
   1144        const TabHoverPanelSet = ChromeUtils.importESModule(
   1145          "chrome://browser/content/tabbrowser/tab-hover-preview.mjs"
   1146        ).default;
   1147        this.previewPanel = new TabHoverPanelSet(window);
   1148      }
   1149    }
   1150 
   1151    appendChild(tab) {
   1152      return this.insertBefore(tab, null);
   1153    }
   1154 
   1155    insertBefore(tab, node) {
   1156      if (!this.arrowScrollbox) {
   1157        throw new Error("Shouldn't call this without arrowscrollbox");
   1158      }
   1159 
   1160      if (node == null) {
   1161        // We have a container for non-tab elements at the end of the scrollbox.
   1162        node = this.arrowScrollbox.lastChild;
   1163      }
   1164 
   1165      node.before(tab);
   1166 
   1167      if (this.#mustUpdateTabMinHeight) {
   1168        this.#updateTabMinHeight();
   1169      }
   1170    }
   1171 
   1172    #updateTabMinWidth(val) {
   1173      this.style.setProperty(
   1174        "--tab-min-width-pref",
   1175        (val ?? this._tabMinWidthPref) + "px"
   1176      );
   1177    }
   1178 
   1179    #updateTabMinHeight() {
   1180      if (!this.verticalMode || !window.toolbar.visible) {
   1181        this.#mustUpdateTabMinHeight = false;
   1182        return;
   1183      }
   1184 
   1185      // Find at least one tab we can scroll to.
   1186      let firstScrollableTab = this.visibleTabs.find(
   1187        this.arrowScrollbox._canScrollToElement
   1188      );
   1189 
   1190      if (!firstScrollableTab) {
   1191        // If not, we're in a pickle. We should never get here except if we
   1192        // also don't use the outcome of this work (because there's nothing to
   1193        // scroll so we don't care about the scrollbox size).
   1194        // So just set a flag so we re-run once we do have a new tab.
   1195        this.#mustUpdateTabMinHeight = true;
   1196        return;
   1197      }
   1198 
   1199      let { height } =
   1200        window.windowUtils.getBoundsWithoutFlushing(firstScrollableTab);
   1201 
   1202      // Use the current known height or a sane default.
   1203      this.#tabMinHeight = height || 36;
   1204 
   1205      // The height we got may be incorrect if a flush is pending so re-check it after
   1206      // a flush completes.
   1207      window
   1208        .promiseDocumentFlushed(() => {})
   1209        .then(
   1210          () => {
   1211            height =
   1212              window.windowUtils.getBoundsWithoutFlushing(
   1213                firstScrollableTab
   1214              ).height;
   1215 
   1216            if (height) {
   1217              this.#tabMinHeight = height;
   1218            }
   1219          },
   1220          () => {
   1221            /* ignore errors */
   1222          }
   1223        );
   1224    }
   1225 
   1226    get _isCustomizing() {
   1227      return document.documentElement.hasAttribute("customizing");
   1228    }
   1229 
   1230    // This overrides the TabsBase _selectNewTab method so that we can
   1231    // potentially interrupt keyboard tab switching when sharing the
   1232    // window or screen.
   1233    _selectNewTab(aNewTab, aFallbackDir, aWrap) {
   1234      if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
   1235        super._selectNewTab(aNewTab, aFallbackDir, aWrap);
   1236      }
   1237    }
   1238 
   1239    observe(aSubject, aTopic, aData) {
   1240      switch (aTopic) {
   1241        case "nsPref:changed": {
   1242          if (aData == "browser.tabs.dragDrop.multiselectStacking") {
   1243            this.#initializeDragAndDrop();
   1244          }
   1245          // This is has to deal with changes in
   1246          // privacy.userContext.enabled and
   1247          // privacy.userContext.newTabContainerOnLeftClick.enabled.
   1248          let containersEnabled =
   1249            Services.prefs.getBoolPref("privacy.userContext.enabled") &&
   1250            !PrivateBrowsingUtils.isWindowPrivate(window);
   1251 
   1252          // This pref won't change so often, so just recreate the menu.
   1253          const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
   1254            "privacy.userContext.newTabContainerOnLeftClick.enabled"
   1255          );
   1256 
   1257          // There are separate "new tab" buttons for horizontal tabs toolbar, vertical tabs and
   1258          // for when the tab strip is overflowed (which is shared by vertical and horizontal tabs);
   1259          // Attach the long click popup to all of them.
   1260          const newTab = document.getElementById("new-tab-button");
   1261          const newTab2 = this.newTabButton;
   1262          const newTabVertical = document.getElementById(
   1263            "vertical-tabs-newtab-button"
   1264          );
   1265 
   1266          for (let parent of [newTab, newTab2, newTabVertical]) {
   1267            if (!parent) {
   1268              continue;
   1269            }
   1270 
   1271            parent.removeAttribute("type");
   1272            if (parent.menupopup) {
   1273              parent.menupopup.remove();
   1274            }
   1275 
   1276            if (containersEnabled) {
   1277              parent.setAttribute("context", "new-tab-button-popup");
   1278 
   1279              let popup = document
   1280                .getElementById("new-tab-button-popup")
   1281                .cloneNode(true);
   1282              popup.removeAttribute("id");
   1283              popup.className = "new-tab-popup";
   1284              popup.setAttribute("position", "after_end");
   1285              popup.addEventListener("popupshowing", CreateContainerTabMenu);
   1286              parent.prepend(popup);
   1287              parent.setAttribute("type", "menu");
   1288              // Update tooltip text
   1289              DynamicShortcutTooltip.nodeToTooltipMap[parent.id] =
   1290                newTabLeftClickOpensContainersMenu
   1291                  ? "newTabAlwaysContainer.tooltip"
   1292                  : "newTabContainer.tooltip";
   1293            } else {
   1294              DynamicShortcutTooltip.nodeToTooltipMap[parent.id] =
   1295                "newTabButton.tooltip";
   1296              parent.removeAttribute("context", "new-tab-button-popup");
   1297            }
   1298            // evict from tooltip cache
   1299            DynamicShortcutTooltip.cache.delete(parent.id);
   1300 
   1301            // If containers and press-hold container menu are both used,
   1302            // add to gClickAndHoldListenersOnElement; otherwise, remove.
   1303            if (containersEnabled && !newTabLeftClickOpensContainersMenu) {
   1304              gClickAndHoldListenersOnElement.add(parent);
   1305            } else {
   1306              gClickAndHoldListenersOnElement.remove(parent);
   1307            }
   1308          }
   1309 
   1310          break;
   1311        }
   1312      }
   1313    }
   1314 
   1315    _updateCloseButtons() {
   1316      if (this.overflowing) {
   1317        // Tabs are at their minimum widths.
   1318        this.setAttribute("closebuttons", "activetab");
   1319        return;
   1320      }
   1321 
   1322      if (this._closeButtonsUpdatePending) {
   1323        return;
   1324      }
   1325      this._closeButtonsUpdatePending = true;
   1326 
   1327      // Wait until after the next paint to get current layout data from
   1328      // getBoundsWithoutFlushing.
   1329      window.requestAnimationFrame(() => {
   1330        window.requestAnimationFrame(() => {
   1331          this._closeButtonsUpdatePending = false;
   1332 
   1333          // The scrollbox may have started overflowing since we checked
   1334          // overflow earlier, so check again.
   1335          if (this.overflowing) {
   1336            this.setAttribute("closebuttons", "activetab");
   1337            return;
   1338          }
   1339 
   1340          // Check if tab widths are below the threshold where we want to
   1341          // remove close buttons from background tabs so that people don't
   1342          // accidentally close tabs by selecting them.
   1343          let rect = ele => {
   1344            return window.windowUtils.getBoundsWithoutFlushing(ele);
   1345          };
   1346          let tab = this.visibleTabs[gBrowser.pinnedTabCount];
   1347          if (tab && rect(tab).width <= this._tabClipWidth) {
   1348            this.setAttribute("closebuttons", "activetab");
   1349          } else {
   1350            this.removeAttribute("closebuttons");
   1351          }
   1352        });
   1353      });
   1354    }
   1355 
   1356    /**
   1357     * @param {boolean} [aInstant]
   1358     */
   1359    _handleTabSelect(aInstant) {
   1360      let selectedTab = this.selectedItem;
   1361      this.#ensureTabIsVisible(selectedTab, aInstant);
   1362 
   1363      selectedTab._notselectedsinceload = false;
   1364    }
   1365 
   1366    /**
   1367     * @param {MozTabbrowserTab} tab
   1368     * @param {boolean} [shouldScrollInstantly=false]
   1369     */
   1370    #ensureTabIsVisible(tab, shouldScrollInstantly = false) {
   1371      let arrowScrollbox = tab.closest("arrowscrollbox");
   1372      if (arrowScrollbox?.overflowing) {
   1373        arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly);
   1374      }
   1375    }
   1376 
   1377    /**
   1378     * Try to keep the active tab's close button under the mouse cursor
   1379     */
   1380    _lockTabSizing(aClosingTab, aTabWidth) {
   1381      if (this.verticalMode) {
   1382        return;
   1383      }
   1384 
   1385      let tabs = this.visibleTabs;
   1386      let numPinned = gBrowser.pinnedTabCount;
   1387 
   1388      if (tabs.length <= numPinned) {
   1389        // There are no unpinned tabs left.
   1390        return;
   1391      }
   1392 
   1393      let isEndTab = aClosingTab && aClosingTab._tPos > tabs.at(-1)._tPos;
   1394 
   1395      if (!this._tabDefaultMaxWidth) {
   1396        this._tabDefaultMaxWidth = parseFloat(
   1397          window.getComputedStyle(tabs[numPinned]).maxWidth
   1398        );
   1399      }
   1400      this._lastTabClosedByMouse = true;
   1401      this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
   1402        this.arrowScrollbox._scrollButtonDown
   1403      ).width;
   1404      if (aTabWidth === undefined) {
   1405        aTabWidth = window.windowUtils.getBoundsWithoutFlushing(
   1406          tabs[numPinned]
   1407        ).width;
   1408      }
   1409 
   1410      if (this.overflowing) {
   1411        // Don't need to do anything if we're in overflow mode and aren't scrolled
   1412        // all the way to the right, or if we're closing the last tab.
   1413        if (isEndTab || !this.arrowScrollbox.hasAttribute("scrolledtoend")) {
   1414          return;
   1415        }
   1416        // If the tab has an owner that will become the active tab, the owner will
   1417        // be to the left of it, so we actually want the left tab to slide over.
   1418        // This can't be done as easily in non-overflow mode, so we don't bother.
   1419        if (aClosingTab?.owner) {
   1420          return;
   1421        }
   1422        this._expandSpacerBy(aTabWidth);
   1423      } /* non-overflow mode */ else {
   1424        if (isEndTab && !this._hasTabTempMaxWidth) {
   1425          // Locking is neither in effect nor needed, so let tabs expand normally.
   1426          return;
   1427        }
   1428        // Force tabs to stay the same width, unless we're closing the last tab,
   1429        // which case we need to let them expand just enough so that the overall
   1430        // tabbar width is the same.
   1431        if (isEndTab) {
   1432          let numNormalTabs = tabs.length - numPinned;
   1433          aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
   1434          if (aTabWidth > this._tabDefaultMaxWidth) {
   1435            aTabWidth = this._tabDefaultMaxWidth;
   1436          }
   1437        }
   1438        aTabWidth += "px";
   1439        let tabsToReset = [];
   1440        for (let i = numPinned; i < tabs.length; i++) {
   1441          let tab = tabs[i];
   1442          tab.style.setProperty("max-width", aTabWidth, "important");
   1443          if (!isEndTab) {
   1444            // keep tabs the same width
   1445            tab.animationsEnabled = false;
   1446            tabsToReset.push(tab);
   1447          }
   1448        }
   1449 
   1450        if (tabsToReset.length) {
   1451          window
   1452            .promiseDocumentFlushed(() => {})
   1453            .then(() => {
   1454              window.requestAnimationFrame(() => {
   1455                for (let tab of tabsToReset) {
   1456                  tab.animationsEnabled = true;
   1457                }
   1458              });
   1459            });
   1460        }
   1461 
   1462        this._hasTabTempMaxWidth = true;
   1463        gBrowser.addEventListener("mousemove", this);
   1464        window.addEventListener("mouseout", this);
   1465      }
   1466    }
   1467 
   1468    _expandSpacerBy(pixels) {
   1469      let spacer = this._closingTabsSpacer;
   1470      spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
   1471      this.toggleAttribute("using-closing-tabs-spacer", true);
   1472      gBrowser.addEventListener("mousemove", this);
   1473      window.addEventListener("mouseout", this);
   1474    }
   1475 
   1476    _unlockTabSizing() {
   1477      gBrowser.removeEventListener("mousemove", this);
   1478      window.removeEventListener("mouseout", this);
   1479 
   1480      if (this._hasTabTempMaxWidth) {
   1481        this._hasTabTempMaxWidth = false;
   1482        // Only visible tabs have their sizes locked, but those visible tabs
   1483        // could become invisible before being unlocked (e.g. by being inside
   1484        // of a collapsing tab group), so it's better to reset all tabs.
   1485        let tabs = this.allTabs;
   1486        for (let i = 0; i < tabs.length; i++) {
   1487          tabs[i].style.maxWidth = "";
   1488        }
   1489      }
   1490 
   1491      if (this.hasAttribute("using-closing-tabs-spacer")) {
   1492        this.removeAttribute("using-closing-tabs-spacer");
   1493        this._closingTabsSpacer.style.width = 0;
   1494      }
   1495    }
   1496 
   1497    uiDensityChanged() {
   1498      this._updateCloseButtons();
   1499      this.#updateTabMinHeight();
   1500      this._handleTabSelect(true);
   1501    }
   1502 
   1503    _notifyBackgroundTab(aTab) {
   1504      if (aTab.pinned || !aTab.visible || !this.overflowing) {
   1505        return;
   1506      }
   1507 
   1508      this._lastTabToScrollIntoView = aTab;
   1509      if (!this._backgroundTabScrollPromise) {
   1510        this._backgroundTabScrollPromise = window
   1511          .promiseDocumentFlushed(() => {
   1512            let lastTabRect =
   1513              this._lastTabToScrollIntoView.getBoundingClientRect();
   1514            let selectedTab = this.selectedItem;
   1515            if (selectedTab.pinned) {
   1516              selectedTab = null;
   1517            } else {
   1518              selectedTab = selectedTab.getBoundingClientRect();
   1519              selectedTab = {
   1520                left: selectedTab.left,
   1521                right: selectedTab.right,
   1522                top: selectedTab.top,
   1523                bottom: selectedTab.bottom,
   1524              };
   1525            }
   1526            return [
   1527              this._lastTabToScrollIntoView,
   1528              this.arrowScrollbox.scrollClientRect,
   1529              lastTabRect,
   1530              selectedTab,
   1531            ];
   1532          })
   1533          .then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
   1534            // First off, remove the promise so we can re-enter if necessary.
   1535            delete this._backgroundTabScrollPromise;
   1536            // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
   1537            // the code above to get layout info for *that* tab, and don't do
   1538            // anything here, as we really just want to run this for the last-opened tab.
   1539            if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
   1540              this._notifyBackgroundTab(this._lastTabToScrollIntoView);
   1541              return;
   1542            }
   1543            delete this._lastTabToScrollIntoView;
   1544            // Is the new tab already completely visible?
   1545            if (
   1546              this.verticalMode
   1547                ? scrollRect.top <= tabRect.top &&
   1548                  tabRect.bottom <= scrollRect.bottom
   1549                : scrollRect.left <= tabRect.left &&
   1550                  tabRect.right <= scrollRect.right
   1551            ) {
   1552              return;
   1553            }
   1554 
   1555            if (this.arrowScrollbox.smoothScroll) {
   1556              // Can we make both the new tab and the selected tab completely visible?
   1557              if (
   1558                !selectedRect ||
   1559                (this.verticalMode
   1560                  ? Math.max(
   1561                      tabRect.bottom - selectedRect.top,
   1562                      selectedRect.bottom - tabRect.top
   1563                    ) <= scrollRect.height
   1564                  : Math.max(
   1565                      tabRect.right - selectedRect.left,
   1566                      selectedRect.right - tabRect.left
   1567                    ) <= scrollRect.width)
   1568              ) {
   1569                this.#ensureTabIsVisible(tabToScrollIntoView);
   1570                return;
   1571              }
   1572 
   1573              let scrollPixels;
   1574              if (this.verticalMode) {
   1575                scrollPixels = tabRect.top - selectedRect.top;
   1576              } else if (this.#rtlMode) {
   1577                scrollPixels = selectedRect.right - scrollRect.right;
   1578              } else {
   1579                scrollPixels = selectedRect.left - scrollRect.left;
   1580              }
   1581              this.arrowScrollbox.scrollByPixels(scrollPixels);
   1582            }
   1583 
   1584            if (!this._animateElement.hasAttribute("highlight")) {
   1585              this._animateElement.toggleAttribute("highlight", true);
   1586              setTimeout(
   1587                function (ele) {
   1588                  ele.removeAttribute("highlight");
   1589                },
   1590                150,
   1591                this._animateElement
   1592              );
   1593            }
   1594          });
   1595      }
   1596    }
   1597 
   1598    _handleNewTab(tab) {
   1599      if (tab.container != this) {
   1600        return;
   1601      }
   1602      tab._fullyOpen = true;
   1603      gBrowser.tabAnimationsInProgress--;
   1604 
   1605      this._updateCloseButtons();
   1606 
   1607      if (tab.hasAttribute("selected")) {
   1608        this._handleTabSelect();
   1609      } else if (!tab.hasAttribute("skipbackgroundnotify")) {
   1610        this._notifyBackgroundTab(tab);
   1611      }
   1612 
   1613      // If this browser isn't lazy (indicating it's probably created by
   1614      // session restore), preload the next about:newtab if we don't
   1615      // already have a preloaded browser.
   1616      if (tab.linkedPanel) {
   1617        NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
   1618      }
   1619 
   1620      if (UserInteraction.running("browser.tabs.opening", window)) {
   1621        UserInteraction.finish("browser.tabs.opening", window);
   1622      }
   1623    }
   1624 
   1625    _canAdvanceToTab(aTab) {
   1626      return !aTab.closing;
   1627    }
   1628 
   1629    /**
   1630     * Returns the panel associated with a tab if it has a connected browser
   1631     * and/or it is the selected tab.
   1632     * For background lazy browsers, this will return null.
   1633     */
   1634    getRelatedElement(aTab) {
   1635      if (!aTab) {
   1636        return null;
   1637      }
   1638 
   1639      // Cannot access gBrowser before it's initialized.
   1640      if (!gBrowser._initialized) {
   1641        return this.tabbox.tabpanels.firstElementChild;
   1642      }
   1643 
   1644      // If the tab's browser is lazy, we need to `_insertBrowser` in order
   1645      // to have a linkedPanel.  This will also serve to bind the browser
   1646      // and make it ready to use. We only do this if the tab is selected
   1647      // because otherwise, callers might end up unintentionally binding the
   1648      // browser for lazy background tabs.
   1649      if (!aTab.linkedPanel) {
   1650        if (!aTab.selected) {
   1651          return null;
   1652        }
   1653        gBrowser._insertBrowser(aTab);
   1654      }
   1655      return document.getElementById(aTab.linkedPanel);
   1656    }
   1657 
   1658    _updateNewTabVisibility() {
   1659      // Helper functions to help deal with customize mode wrapping some items
   1660      let wrap = n =>
   1661        n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
   1662      let unwrap = n =>
   1663        n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
   1664 
   1665      // Starting from the tabs element, find the next sibling that:
   1666      // - isn't hidden; and
   1667      // - isn't the all-tabs button.
   1668      // If it's the new tab button, consider the new tab button adjacent to the tabs.
   1669      // If the new tab button is marked as adjacent and the tabstrip doesn't
   1670      // overflow, we'll display the 'new tab' button inline in the tabstrip.
   1671      // In all other cases, the separate new tab button is displayed in its
   1672      // customized location.
   1673      let sib = this;
   1674      do {
   1675        sib = unwrap(wrap(sib).nextElementSibling);
   1676      } while (sib && (sib.hidden || sib.id == "alltabs-button"));
   1677 
   1678      this.toggleAttribute(
   1679        "hasadjacentnewtabbutton",
   1680        sib && sib.id == "new-tab-button"
   1681      );
   1682    }
   1683 
   1684    onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
   1685      if (
   1686        aContainer.ownerDocument == document &&
   1687        aContainer.id == "TabsToolbar-customization-target"
   1688      ) {
   1689        this._updateNewTabVisibility();
   1690      }
   1691    }
   1692 
   1693    onAreaNodeRegistered(aArea, aContainer) {
   1694      if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
   1695        this._updateNewTabVisibility();
   1696      }
   1697    }
   1698 
   1699    onAreaReset(aArea, aContainer) {
   1700      this.onAreaNodeRegistered(aArea, aContainer);
   1701    }
   1702 
   1703    _hiddenSoundPlayingStatusChanged(tab, opts) {
   1704      let closed = opts && opts.closed;
   1705      if (!closed && tab.soundPlaying && !tab.visible) {
   1706        this._hiddenSoundPlayingTabs.add(tab);
   1707        this.toggleAttribute("hiddensoundplaying", true);
   1708      } else {
   1709        this._hiddenSoundPlayingTabs.delete(tab);
   1710        if (this._hiddenSoundPlayingTabs.size == 0) {
   1711          this.removeAttribute("hiddensoundplaying");
   1712        }
   1713      }
   1714    }
   1715 
   1716    destroy() {
   1717      if (this.boundObserve) {
   1718        Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
   1719        Services.prefs.removeObserver(
   1720          "browser.tabs.dragDrop.multiselectStacking",
   1721          this.boundObserve
   1722        );
   1723      }
   1724      CustomizableUI.removeListener(this);
   1725    }
   1726 
   1727    updateTabSoundLabel(tab) {
   1728      // Add aria-label for inline audio button
   1729      const [unmute, mute, unblock] =
   1730        gBrowser.tabLocalization.formatMessagesSync([
   1731          "tabbrowser-unmute-tab-audio-aria-label",
   1732          "tabbrowser-mute-tab-audio-aria-label",
   1733          "tabbrowser-unblock-tab-audio-aria-label",
   1734        ]);
   1735      if (tab.audioButton) {
   1736        if (tab.hasAttribute("muted") || tab.hasAttribute("soundplaying")) {
   1737          let ariaLabel;
   1738          tab.linkedBrowser.audioMuted
   1739            ? (ariaLabel = unmute.attributes[0].value)
   1740            : (ariaLabel = mute.attributes[0].value);
   1741          tab.audioButton.setAttribute("aria-label", ariaLabel);
   1742        } else if (tab.hasAttribute("activemedia-blocked")) {
   1743          tab.audioButton.setAttribute(
   1744            "aria-label",
   1745            unblock.attributes[0].value
   1746          );
   1747        }
   1748      }
   1749    }
   1750  }
   1751 
   1752  customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {
   1753    extends: "tabs",
   1754  });
   1755 }