/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

// This is loaded into all browser windows. Wrap in a block to prevent
// leaking to window scope.
{
  const DIRECTION_BACKWARD = -1;
  const DIRECTION_FORWARD = 1;

  const isTab = element => gBrowser.isTab(element);
  const isTabGroup = element => gBrowser.isTabGroup(element);
  const isTabGroupLabel = element => gBrowser.isTabGroupLabel(element);
  const isSplitViewWrapper = element => gBrowser.isSplitViewWrapper(element);

  class MozTabbrowserTabs extends MozElements.TabsBase {
    static observedAttributes = ["orient"];

    #mustUpdateTabMinHeight = false;
    #tabMinHeight = 36;
    #animatingGroups = new Set();

    constructor() {
      super();

      this.addEventListener("TabSelect", this);
      this.addEventListener("TabClose", this);
      this.addEventListener("TabAttrModified", this);
      this.addEventListener("TabHide", this);
      this.addEventListener("TabShow", this);
      this.addEventListener("TabHoverStart", this);
      this.addEventListener("TabHoverEnd", this);
      this.addEventListener("TabNoteIconHoverStart", this);
      this.addEventListener("TabNoteIconHoverEnd", this);
      this.addEventListener("TabGroupLabelHoverStart", this);
      this.addEventListener("TabGroupLabelHoverEnd", this);
      // Capture collapse/expand early so we mark animating groups before
      // overflow/underflow handlers run.
      this.addEventListener("TabGroupExpand", this, true);
      this.addEventListener("TabGroupCollapse", this, true);
      this.addEventListener("TabGroupAnimationComplete", this);
      this.addEventListener("TabGroupCreate", this);
      this.addEventListener("TabGroupRemoved", this);
      this.addEventListener("SplitViewCreated", this);
      this.addEventListener("SplitViewRemoved", this);
      this.addEventListener("transitionend", this);
      this.addEventListener("dblclick", this);
      this.addEventListener("click", this);
      this.addEventListener("click", this, true);
      this.addEventListener("keydown", this, { mozSystemGroup: true });
      this.addEventListener("mouseleave", this);
      this.addEventListener("focusin", this);
      this.addEventListener("focusout", this);
      this.addEventListener("contextmenu", this);
      this.addEventListener("dragstart", this);
      this.addEventListener("dragover", this);
      this.addEventListener("drop", this);
      this.addEventListener("dragend", this);
      this.addEventListener("dragleave", this);
    }

    init() {
      this.startupTime = Services.startup.getStartupInfo().start.getTime();

      this.arrowScrollbox = document.getElementById(
        "tabbrowser-arrowscrollbox"
      );
      this.arrowScrollbox.addEventListener("wheel", this, true);
      this.arrowScrollbox.addEventListener("underflow", this);
      this.arrowScrollbox.addEventListener("overflow", this);
      this.pinnedTabsContainer = document.getElementById(
        "pinned-tabs-container"
      );
      this.pinnedTabsContainer.setAttribute(
        "orient",
        this.getAttribute("orient")
      );

      // Override arrowscrollbox.js method, since our scrollbox's children are
      // inherited from the scrollbox binding parent (this).
      this.arrowScrollbox._getScrollableElements = () => {
        return this.ariaFocusableItems.reduce((elements, item) => {
          if (this.arrowScrollbox._canScrollToElement(item)) {
            elements.push(item);
            if (
              isTab(item) &&
              item.group &&
              item.group.collapsed &&
              item.selected
            ) {
              // overflow container is scrollable, but not in focus order
              elements.push(item.group.overflowContainer);
            }
          }
          return elements;
        }, []);
      };
      this.arrowScrollbox._canScrollToElement = element => {
        if (isTab(element)) {
          return !element.pinned;
        }
        return true;
      };

      // Override for performance reasons. This is the size of a single element
      // that can be scrolled when using mouse wheel scrolling. If we don't do
      // this then arrowscrollbox computes this value by calling
      // _getScrollableElements and dividing the box size by that number.
      // However in the tabstrip case we already know the answer to this as,
      // when we're overflowing, it is always the same as the tab min width or
      // height. For tab group labels, the number won't exactly match, but
      // that shouldn't be a problem in practice since the arrowscrollbox
      // stops at element bounds when finishing scrolling.
      Object.defineProperty(this.arrowScrollbox, "lineScrollAmount", {
        get: () =>
          this.verticalMode ? this.#tabMinHeight : this._tabMinWidthPref,
      });

      this.baseConnect();

      this._blockDblClick = false;
      this._closeButtonsUpdatePending = false;
      this._closingTabsSpacer = this.querySelector(".closing-tabs-spacer");
      this._tabDefaultMaxWidth = NaN;
      this._lastTabClosedByMouse = false;
      this._hasTabTempMaxWidth = false;
      this._scrollButtonWidth = 0;
      this._animateElement = this.arrowScrollbox;
      this._tabClipWidth = Services.prefs.getIntPref(
        "browser.tabs.tabClipWidth"
      );
      this._hiddenSoundPlayingTabs = new Set();
      this.previewPanel = null;

      // When a new application window is spawned, this will set the initial
      // tab's displayed title. If `shouldShowTorConnect` is true, we expect
      // this first page to be `about:torconnect`, so we show the corresponding
      // title that will be used once the page is shown. See tor-browser#44781.
      this.allTabs[0].label = TorConnect.shouldShowTorConnect
        ? this.torconnectTitle
        : this.emptyTabTitle;
      // Mark this as the first tab that is still loading. This can be cleared
      // once we have a definite title or URI.
      this.allTabs[0]._isFirstTabLoading = true;

      this.newTabButton.setAttribute(
        "aria-label",
        DynamicShortcutTooltip.getText("tabs-newtab-button")
      );

      let handleResize = () => {
        this._updateCloseButtons();
        this._handleTabSelect(true);
      };
      window.addEventListener("resize", handleResize);
      this._fullscreenMutationObserver = new MutationObserver(handleResize);
      this._fullscreenMutationObserver.observe(document.documentElement, {
        attributeFilter: ["inFullscreen", "inDOMFullscreen"],
      });
      window.addEventListener("uidensitychanged", this);

      this.boundObserve = (...args) => this.observe(...args);
      Services.prefs.addObserver("privacy.userContext", this.boundObserve);
      this.observe(null, "nsPref:changed", "privacy.userContext.enabled");

      document
        .getElementById("vertical-tabs-newtab-button")
        .addEventListener("keypress", this);
      document
        .getElementById("tabs-newtab-button")
        .addEventListener("keypress", this);

      XPCOMUtils.defineLazyPreferenceGetter(
        this,
        "_tabMinWidthPref",
        "browser.tabs.tabMinWidth",
        null,
        (pref, prevValue, newValue) => this.#updateTabMinWidth(newValue),
        newValue => {
          const LIMIT = 50;
          return Math.max(newValue, LIMIT);
        }
      );
      this.#updateTabMinWidth(this._tabMinWidthPref);
      this.#updateTabMinHeight();

      CustomizableUI.addListener(this);
      this._updateNewTabVisibility();

      XPCOMUtils.defineLazyPreferenceGetter(
        this,
        "_closeTabByDblclick",
        "browser.tabs.closeTabByDblclick",
        false
      );

      XPCOMUtils.defineLazyPreferenceGetter(
        this,
        "_sidebarVisibility",
        "sidebar.visibility",
        "always-show"
      );

      XPCOMUtils.defineLazyPreferenceGetter(
        this,
        "_sidebarPositionStart",
        "sidebar.position_start",
        true
      );

      if (gMultiProcessBrowser) {
        this.tabbox.tabpanels.setAttribute("async", "true");
      }

      XPCOMUtils.defineLazyPreferenceGetter(
        this,
        "_showTabHoverPreview",
        "browser.tabs.hoverPreview.enabled",
        false
      );
      XPCOMUtils.defineLazyPreferenceGetter(
        this,
        "_showTabGroupHoverPreview",
        "browser.tabs.groups.hoverPreview.enabled",
        false
      );

      this.tooltip = "tabbrowser-tab-tooltip";

      this.tabDragAndDrop = new window.TabDragAndDrop(this);
      this.tabDragAndDrop.init();
    }

    attributeChangedCallback(name, oldValue, newValue) {
      if (name == "orient") {
        // reset this attribute so we don't have incorrect styling for vertical tabs
        this.removeAttribute("overflow");
        this.#updateTabMinWidth();
        this.#updateTabMinHeight();
        this.pinnedTabsContainer?.setAttribute("orient", newValue);
      }
      super.attributeChangedCallback(name, oldValue, newValue);
    }

    // Event handlers

    handleEvent(aEvent) {
      switch (aEvent.type) {
        case "mouseout": {
          // If the "related target" (the node to which the pointer went) is not
          // a child of the current document, the mouse just left the window.
          let relatedTarget = aEvent.relatedTarget;
          if (relatedTarget && relatedTarget.ownerDocument == document) {
            break;
          }
        }
        // fall through
        case "mousemove":
          if (
            document.getElementById("tabContextMenu").state != "open" &&
            !this.#isMovingTab()
          ) {
            this._unlockTabSizing();
          }
          break;
        case "mouseleave":
          this.previewPanel?.deactivate();
          break;
        default: {
          let methodName = `on_${aEvent.type}`;
          if (methodName in this) {
            this[methodName](aEvent);
          } else {
            throw new Error(`Unexpected event ${aEvent.type}`);
          }
        }
      }
    }

    /**
     * @param {CustomEvent} event
     */
    on_TabSelect(event) {
      const {
        target: newTab,
        detail: { previousTab },
      } = event;

      // In some cases (e.g. by selecting a tab in a collapsed tab group),
      // changing the selected tab may cause a tab to appear/disappear.
      if (previousTab.group?.collapsed || newTab.group?.collapsed) {
        this._invalidateCachedVisibleTabs();
      }
      this._handleTabSelect();
    }

    on_TabClose(event) {
      this._hiddenSoundPlayingStatusChanged(event.target, { closed: true });
    }

    on_TabAttrModified(event) {
      if (
        event.detail.changed.includes("soundplaying") &&
        !event.target.visible
      ) {
        this._hiddenSoundPlayingStatusChanged(event.target);
      }
      if (
        event.detail.changed.includes("soundplaying") ||
        event.detail.changed.includes("muted") ||
        event.detail.changed.includes("activemedia-blocked")
      ) {
        this.updateTabSoundLabel(event.target);
      }
    }

    on_TabHide(event) {
      if (event.target.soundPlaying) {
        this._hiddenSoundPlayingStatusChanged(event.target);
      }
    }

    on_TabShow(event) {
      if (event.target.soundPlaying) {
        this._hiddenSoundPlayingStatusChanged(event.target);
      }
    }

    on_TabHoverStart(event) {
      if (!this._showTabHoverPreview) {
        return;
      }
      this.ensureTabPreviewPanelLoaded();
      this.previewPanel.activate(event.target);
    }

    on_TabHoverEnd(event) {
      this.previewPanel?.deactivate(event.target);
    }

    on_TabNoteIconHoverStart(event) {
      if (!this._showTabHoverPreview) {
        return;
      }
      this.ensureTabPreviewPanelLoaded();
      this.previewPanel.activateNotePanel(
        event.target,
        event.detail.noteIconElement
      );
    }

    on_TabNoteIconHoverEnd(event) {
      this.previewPanel?.deactivateNotePanel(event.target);
      if (event.detail.returningToTab) {
        this.previewPanel?.activate(event.target);
      }
    }

    cancelTabGroupPreview() {
      this.previewPanel?.panelOpener.clear();
    }

    showTabGroupPreview(group) {
      if (!this._showTabGroupHoverPreview) {
        return;
      }
      this.ensureTabPreviewPanelLoaded();
      this.previewPanel.activate(group);
    }

    on_TabGroupLabelHoverStart(event) {
      this.showTabGroupPreview(event.target.group);
    }

    on_TabGroupLabelHoverEnd(event) {
      this.previewPanel?.deactivate(event.target.group);
    }

    on_TabGroupExpand(event) {
      this._invalidateCachedVisibleTabs();
      this.#animatingGroups.add(event.target.id);
    }

    on_TabGroupCollapse(event) {
      this._invalidateCachedVisibleTabs();
      this._unlockTabSizing();
      this.#animatingGroups.add(event.target.id);
    }

    on_TabGroupAnimationComplete(event) {
      // Delay clearing the animating flag so overflow/underflow handlers
      // triggered by the size change can observe it and skip auto-scroll.
      window.requestAnimationFrame(() => {
        this.#animatingGroups.delete(event.target.id);
      });
    }

    on_TabGroupCreate() {
      this._invalidateCachedTabs();
    }

    on_TabGroupRemoved() {
      this._invalidateCachedTabs();
    }

    on_SplitViewCreated() {
      this._invalidateCachedTabs();
    }

    on_SplitViewRemoved() {
      this._invalidateCachedTabs();
    }

    /**
     * @param {TransitionEvent} event
     */
    on_transitionend(event) {
      if (event.propertyName != "max-width") {
        return;
      }

      let tab = event.target?.closest("tab");

      if (!tab) {
        return;
      }

      if (tab.hasAttribute("fadein")) {
        if (tab._fullyOpen) {
          this._updateCloseButtons();
        } else {
          this._handleNewTab(tab);
        }
      } else if (tab.closing) {
        gBrowser._endRemoveTab(tab);
      }

      let evt = new CustomEvent("TabAnimationEnd", { bubbles: true });
      tab.dispatchEvent(evt);
    }

    on_dblclick(event) {
      // When the tabbar has an unified appearance with the titlebar
      // and menubar, a double-click in it should have the same behavior
      // as double-clicking the titlebar
      if (CustomTitlebar.enabled && !this.verticalMode) {
        return;
      }

      // Make sure it is the primary button, we are hitting our arrowscrollbox,
      // and we're not hitting the scroll buttons.
      if (
        event.button != 0 ||
        event.target != this.arrowScrollbox ||
        event.composedTarget.localName == "toolbarbutton"
      ) {
        return;
      }

      if (!this._blockDblClick) {
        BrowserCommands.openTab();
      }

      event.preventDefault();
    }

    on_click(event) {
      if (event.eventPhase == Event.CAPTURING_PHASE && event.button == 0) {
        /* Catches extra clicks meant for the in-tab close button.
         * Placed here to avoid leaking (a temporary handler added from the
         * in-tab close button binding would close over the tab and leak it
         * until the handler itself was removed). (bug 897751)
         *
         * The only sequence in which a second click event (i.e. dblclik)
         * can be dispatched on an in-tab close button is when it is shown
         * after the first click (i.e. the first click event was dispatched
         * on the tab). This happens when we show the close button only on
         * the active tab. (bug 352021)
         * The only sequence in which a third click event can be dispatched
         * on an in-tab close button is when the tab was opened with a
         * double click on the tabbar. (bug 378344)
         * In both cases, it is most likely that the close button area has
         * been accidentally clicked, therefore we do not close the tab.
         *
         * We don't want to ignore processing of more than one click event,
         * though, since the user might actually be repeatedly clicking to
         * close many tabs at once.
         */
        let target = event.originalTarget;
        if (target.classList.contains("tab-close-button")) {
          // We preemptively set this to allow the closing-multiple-tabs-
          // in-a-row case.
          if (this._blockDblClick) {
            target._ignoredCloseButtonClicks = true;
          } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
            target._ignoredCloseButtonClicks = true;
            event.stopPropagation();
            return;
          } else {
            // Reset the "ignored click" flag
            target._ignoredCloseButtonClicks = false;
          }
        }

        /* Protects from close-tab-button errant doubleclick:
         * Since we're removing the event target, if the user
         * double-clicks the button, the dblclick event will be dispatched
         * with the tabbar as its event target (and explicit/originalTarget),
         * which treats that as a mouse gesture for opening a new tab.
         * In this context, we're manually blocking the dblclick event.
         */
        if (this._blockDblClick) {
          if (!("_clickedTabBarOnce" in this)) {
            this._clickedTabBarOnce = true;
            return;
          }
          delete this._clickedTabBarOnce;
          this._blockDblClick = false;
        }
      } else if (
        event.eventPhase == Event.BUBBLING_PHASE &&
        event.button == 1
      ) {
        let tab = event.target?.closest("tab");
        if (tab) {
          if (tab.multiselected) {
            gBrowser.removeMultiSelectedTabs();
          } else {
            gBrowser.removeTab(tab, {
              animate: true,
              triggeringEvent: event,
            });
          }
        } else if (isTabGroupLabel(event.target)) {
          event.target.group.saveAndClose();
        } else if (
          event.originalTarget.closest("scrollbox") &&
          !Services.prefs.getBoolPref(
            "widget.gtk.titlebar-action-middle-click-enabled"
          )
        ) {
          // Check whether the click
          // was dispatched on the open space of it.
          let visibleTabs = this.visibleTabs;
          let lastTab = visibleTabs.at(-1);
          let winUtils = window.windowUtils;
          let endOfTab =
            winUtils.getBoundsWithoutFlushing(lastTab)[
              (this.verticalMode && "bottom") ||
                (this.#rtlMode ? "left" : "right")
            ];
          if (
            (this.verticalMode && event.clientY > endOfTab) ||
            (!this.verticalMode &&
              (this.#rtlMode
                ? event.clientX < endOfTab
                : event.clientX > endOfTab))
          ) {
            BrowserCommands.openTab();
          }
        } else {
          return;
        }

        event.preventDefault();
        event.stopPropagation();
      }
    }

    on_keydown(event) {
      let { altKey, shiftKey } = event;
      let [accel, nonAccel] =
        AppConstants.platform == "macosx"
          ? [event.metaKey, event.ctrlKey]
          : [event.ctrlKey, event.metaKey];

      let keyComboForFocusedElement =
        !accel && !shiftKey && !altKey && !nonAccel;
      let keyComboForMove = accel && shiftKey && !altKey && !nonAccel;
      let keyComboForFocus = accel && !shiftKey && !altKey && !nonAccel;

      if (!keyComboForFocusedElement && !keyComboForMove && !keyComboForFocus) {
        return;
      }

      if (keyComboForFocusedElement) {
        let ariaFocusedItem = this.ariaFocusedItem;
        if (isTabGroupLabel(ariaFocusedItem)) {
          switch (event.keyCode) {
            case KeyEvent.DOM_VK_SPACE:
            case KeyEvent.DOM_VK_RETURN: {
              ariaFocusedItem.click();
              event.preventDefault();
            }
          }
        }
      } else if (keyComboForMove) {
        switch (event.keyCode) {
          case KeyEvent.DOM_VK_UP:
            gBrowser.moveTabBackward();
            break;
          case KeyEvent.DOM_VK_DOWN:
            gBrowser.moveTabForward();
            break;
          case KeyEvent.DOM_VK_RIGHT:
            if (RTL_UI) {
              gBrowser.moveTabBackward();
            } else {
              gBrowser.moveTabForward();
            }
            break;
          case KeyEvent.DOM_VK_LEFT:
            if (RTL_UI) {
              gBrowser.moveTabForward();
            } else {
              gBrowser.moveTabBackward();
            }
            break;
          case KeyEvent.DOM_VK_HOME:
            gBrowser.moveTabToStart();
            break;
          case KeyEvent.DOM_VK_END:
            gBrowser.moveTabToEnd();
            break;
          default:
            // Consume the keydown event for the above keyboard
            // shortcuts only.
            return;
        }

        event.preventDefault();
      } else if (keyComboForFocus) {
        switch (event.keyCode) {
          case KeyEvent.DOM_VK_UP:
            this.#advanceFocus(DIRECTION_BACKWARD);
            break;
          case KeyEvent.DOM_VK_DOWN:
            this.#advanceFocus(DIRECTION_FORWARD);
            break;
          case KeyEvent.DOM_VK_RIGHT:
            if (RTL_UI) {
              this.#advanceFocus(DIRECTION_BACKWARD);
            } else {
              this.#advanceFocus(DIRECTION_FORWARD);
            }
            break;
          case KeyEvent.DOM_VK_LEFT:
            if (RTL_UI) {
              this.#advanceFocus(DIRECTION_FORWARD);
            } else {
              this.#advanceFocus(DIRECTION_BACKWARD);
            }
            break;
          case KeyEvent.DOM_VK_HOME:
            this.ariaFocusedItem = this.ariaFocusableItems.at(0);
            break;
          case KeyEvent.DOM_VK_END:
            this.ariaFocusedItem = this.ariaFocusableItems.at(-1);
            break;
          case KeyEvent.DOM_VK_SPACE: {
            let ariaFocusedItem = this.ariaFocusedItem;
            if (isTab(ariaFocusedItem)) {
              if (ariaFocusedItem.multiselected) {
                gBrowser.removeFromMultiSelectedTabs(ariaFocusedItem);
              } else {
                gBrowser.addToMultiSelectedTabs(ariaFocusedItem);
              }
            }
            break;
          }
          default:
            // Consume the keydown event for the above keyboard
            // shortcuts only.
            return;
        }

        event.preventDefault();
      }
    }

    /**
     * @param {FocusEvent} event
     */
    on_focusin(event) {
      if (event.target == this.selectedItem) {
        this.tablistHasFocus = true;
        if (!this.ariaFocusedItem) {
          // If the active tab is receiving focus and there isn't a keyboard
          // focus target yet, set the keyboard focus target to the active
          // tab. Do not override the keyboard-focused item if the user
          // already set a keyboard focus.
          this.ariaFocusedItem = this.selectedItem;
        }
      }
      let focusReturnedFromGroupPanel = event.relatedTarget?.classList.contains(
        "group-preview-button"
      );
      if (
        !focusReturnedFromGroupPanel &&
        this.tablistHasFocus &&
        isTabGroupLabel(this.ariaFocusedItem)
      ) {
        this.showTabGroupPreview(this.ariaFocusedItem.group);
      }
    }

    /**
     * @param {FocusEvent} event
     */
    on_focusout(event) {
      this.cancelTabGroupPreview();
      if (event.target == this.selectedItem) {
        this.tablistHasFocus = false;
      }
    }

    on_keypress(event) {
      if (event.defaultPrevented) {
        return;
      }
      if (event.key == " " || event.key == "Enter") {
        event.preventDefault();
        event.target.click();
      }
    }

    on_dragstart(event) {
      this.tabDragAndDrop.handle_dragstart(event);
    }

    on_dragover(event) {
      this.tabDragAndDrop.handle_dragover(event);
    }

    on_drop(event) {
      this.tabDragAndDrop.handle_drop(event);
    }

    on_dragend(event) {
      this.tabDragAndDrop.handle_dragend(event);
    }

    on_dragleave(event) {
      this.tabDragAndDrop.handle_dragleave(event);
    }

    on_wheel(event) {
      if (
        Services.prefs.getBoolPref("toolkit.tabbox.switchByScrolling", false)
      ) {
        event.stopImmediatePropagation();
      }
    }

    on_overflow(event) {
      // Ignore overflow events from nested scrollable elements
      if (event.target != this.arrowScrollbox) {
        return;
      }

      this.toggleAttribute("overflow", true);
      this._updateCloseButtons();

      if (!this.#animatingGroups.size) {
        this._handleTabSelect(true);
      }

      document
        .getElementById("tab-preview-panel")
        ?.setAttribute("rolluponmousewheel", true);
    }

    on_underflow(event) {
      // Ignore underflow events:
      // - from nested scrollable elements
      // - corresponding to an overflow event that we ignored
      if (event.target != this.arrowScrollbox || !this.overflowing) {
        return;
      }

      this.removeAttribute("overflow");

      if (this._lastTabClosedByMouse) {
        this._expandSpacerBy(this._scrollButtonWidth);
      }

      for (let tab of gBrowser._removingTabs) {
        gBrowser.removeTab(tab);
      }

      this._updateCloseButtons();

      document
        .getElementById("tab-preview-panel")
        ?.removeAttribute("rolluponmousewheel");
    }

    on_contextmenu(event) {
      // When pressing the context menu key (as opposed to right-clicking)
      // while a tab group label has aria focus (as opposed to DOM focus),
      // open the tab group context menu as if the label had DOM focus.
      // The button property is used to differentiate between key and mouse.
      if (event.button == 0 && isTabGroupLabel(this.ariaFocusedItem)) {
        gBrowser.tabGroupMenu.openEditModal(this.ariaFocusedItem.group);
        event.preventDefault();
      }
    }

    on_uidensitychanged() {
      this._updateCloseButtons();
      this.#updateTabMinHeight();
      this._handleTabSelect(true);
    }

    // Utilities

    get emptyTabTitle() {
      // Normal tab title is used also in the permanent private browsing mode.
      const l10nId =
        PrivateBrowsingUtils.isWindowPrivate(window) &&
        !Services.prefs.getBoolPref("browser.privatebrowsing.autostart")
          ? "tabbrowser-empty-private-tab-title"
          : "tabbrowser-empty-tab-title";
      return gBrowser.tabLocalization.formatValueSync(l10nId);
    }

    /**
     * The about:torconnect page <title>, to be fetched when about:torconnect
     * has not yet been loaded.
     *
     * @type {string}
     */
    get torconnectTitle() {
      return gBrowser.torconnectLocalization.formatValueSync(
        "tor-connect-page-title"
      );
    }

    get tabbox() {
      return document.getElementById("tabbrowser-tabbox");
    }

    get newTabButton() {
      return this.querySelector("#tabs-newtab-button");
    }

    get verticalMode() {
      return this.getAttribute("orient") == "vertical";
    }

    get expandOnHover() {
      return this._sidebarVisibility == "expand-on-hover";
    }

    get #rtlMode() {
      return !this.verticalMode && RTL_UI;
    }

    get overflowing() {
      return this.hasAttribute("overflow");
    }

    #allTabs;
    get allTabs() {
      if (this.#allTabs) {
        return this.#allTabs;
      }
      // Remove temporary periphery element added at drag start.
      let pinnedChildren = Array.from(this.pinnedTabsContainer.children);
      if (pinnedChildren?.at(-1)?.id == "pinned-tabs-container-periphery") {
        pinnedChildren.pop();
      }
      let unpinnedChildren = Array.from(this.arrowScrollbox.children);
      // remove arrowScrollbox periphery element.
      unpinnedChildren.pop();

      // explode tab groups and split view wrappers
      // Iterate backwards over the array to preserve indices while we modify
      // things in place
      for (let i = unpinnedChildren.length - 1; i >= 0; i--) {
        if (
          unpinnedChildren[i].tagName == "tab-group" ||
          unpinnedChildren[i].tagName == "tab-split-view-wrapper"
        ) {
          unpinnedChildren.splice(i, 1, ...unpinnedChildren[i].tabs);
        }
      }

      this.#allTabs = [...pinnedChildren, ...unpinnedChildren];
      return this.#allTabs;
    }

    get allGroups() {
      let children = Array.from(this.arrowScrollbox.children);
      return children.filter(node => node.tagName == "tab-group");
    }

    get allSplitViews() {
      let children = Array.from(this.arrowScrollbox.children);
      let splitViews = [];
      for (let node of children) {
        if (node.tagName == "tab-split-view-wrapper") {
          splitViews.push(node);
        } else if (node.tagName == "tab-group") {
          splitViews.push(
            ...Array.from(node.children).filter(
              child => child.tagName == "tab-split-view-wrapper"
            )
          );
        }
      }
      return splitViews;
    }

    /**
     * Returns all tabs in the current window, including hidden tabs and tabs
     * in collapsed groups, but excluding closing tabs and the Firefox View tab.
     */
    get openTabs() {
      if (!this.#openTabs) {
        this.#openTabs = this.allTabs.filter(tab => tab.isOpen);
      }
      return this.#openTabs;
    }
    #openTabs;

    /**
     * Same as `openTabs` but excluding hidden tabs.
     */
    get nonHiddenTabs() {
      if (!this.#nonHiddenTabs) {
        this.#nonHiddenTabs = this.openTabs.filter(tab => !tab.hidden);
      }
      return this.#nonHiddenTabs;
    }
    #nonHiddenTabs;

    /**
     * Same as `openTabs` but excluding hidden tabs and tabs in collapsed groups.
     */
    get visibleTabs() {
      if (!this.#visibleTabs) {
        this.#visibleTabs = this.openTabs.filter(tab => tab.visible);
      }
      return this.#visibleTabs;
    }
    #visibleTabs;

    /**
     * @returns {boolean} true if the keyboard focus is on the active tab
     */
    get tablistHasFocus() {
      return this.hasAttribute("tablist-has-focus");
    }

    /**
     * @param {boolean} hasFocus true if the keyboard focus is on the active tab
     */
    set tablistHasFocus(hasFocus) {
      this.toggleAttribute("tablist-has-focus", hasFocus);
    }

    /** @typedef {MozTabbrowserTab|MozTextLabel} FocusableItem */

    /** @type {FocusableItem[]} */
    #focusableItems;

    /** @type {dragAndDropElements[]} */
    #dragAndDropElements;

    /**
     * @returns {FocusableItem[]}
     * @override
     */
    get ariaFocusableItems() {
      if (this.#focusableItems) {
        return this.#focusableItems;
      }

      let unpinnedChildren = Array.from(this.arrowScrollbox.children);
      let pinnedChildren = Array.from(this.pinnedTabsContainer.children);

      let focusableItems = [];
      for (let child of pinnedChildren) {
        if (isTab(child)) {
          focusableItems.push(child);
        }
      }
      for (let child of unpinnedChildren) {
        if (isTab(child) && child.visible) {
          focusableItems.push(child);
        } else if (isTabGroup(child)) {
          focusableItems.push(child.labelElement);

          let visibleTabsInGroup = child.tabs.filter(tab => tab.visible);
          focusableItems.push(...visibleTabsInGroup);
        } else if (child.tagName == "tab-split-view-wrapper") {
          let visibleTabsInSplitView = child.tabs.filter(tab => tab.visible);
          focusableItems.push(...visibleTabsInSplitView);
        }
      }

      this.#focusableItems = focusableItems;

      return this.#focusableItems;
    }

    /**
     * @returns {dragAndDropElements[]}
     * Representation of every drag and drop element including tabs, tab group labels and split view wrapper.
     * We keep this separate from ariaFocusableItems because not every element for drag n'drop also needs to be
     * focusable (ex, we don't want the splitview container to be focusable, only its children).
     */
    get dragAndDropElements() {
      if (this.#dragAndDropElements) {
        return this.#dragAndDropElements;
      }

      let elementIndex = 0;
      let dragAndDropElements = [];
      let unpinnedChildren = Array.from(this.arrowScrollbox.children);
      let pinnedChildren = Array.from(this.pinnedTabsContainer.children);

      for (let child of [...pinnedChildren, ...unpinnedChildren]) {
        if (
          !(
            (isTab(child) && child.visible) ||
            isTabGroup(child) ||
            isSplitViewWrapper(child)
          )
        ) {
          continue;
        }

        if (isTabGroup(child)) {
          child.labelElement.elementIndex = elementIndex++;
          dragAndDropElements.push(child.labelElement);

          let tabsAndSplitViews = child.tabsAndSplitViews.filter(
            node => node.visible
          );
          tabsAndSplitViews.forEach(ele => {
            ele.elementIndex = elementIndex++;
          });
          dragAndDropElements.push(...tabsAndSplitViews);
        } else {
          child.elementIndex = elementIndex++;
          dragAndDropElements.push(child);
        }
      }

      this.#dragAndDropElements = dragAndDropElements;
      return this.#dragAndDropElements;
    }

    /**
     * Moves the ARIA focus in the tab strip left or right, as appropriate, to
     * the next tab or tab group label.
     *
     * @param {-1|1} direction
     */
    #advanceFocus(direction) {
      let currentIndex = this.ariaFocusableItems.indexOf(this.ariaFocusedItem);
      let newIndex = currentIndex + direction;

      // Clamp the index so that the focus stops at the edges of the tab strip
      newIndex = Math.min(
        this.ariaFocusableItems.length - 1,
        Math.max(0, newIndex)
      );

      let itemToFocus = this.ariaFocusableItems[newIndex];
      this.ariaFocusedItem = itemToFocus;

      // If the newly-focused item is a tab group label and the group is collapsed,
      // proactively show the tab group preview
      if (isTabGroupLabel(this.ariaFocusedItem)) {
        this.showTabGroupPreview(this.ariaFocusedItem.group);
      }
    }

    _invalidateCachedTabs() {
      this.#allTabs = null;
      this._invalidateCachedVisibleTabs();
    }

    _invalidateCachedVisibleTabs() {
      this.#openTabs = null;
      this.#nonHiddenTabs = null;
      this.#visibleTabs = null;
      // Focusable items must also be visible, but they do not depend on
      // this.#visibleTabs, so changes to visible tabs need to also invalidate
      // the focusable items and dragAndDropElements cache.
      this.#focusableItems = null;
      this.#dragAndDropElements = null;
    }

    #isMovingTab() {
      return this.hasAttribute("movingtab");
    }

    isContainerVerticalPinnedGrid(tab) {
      return (
        tab.pinned &&
        this.verticalMode &&
        this.hasAttribute("expanded") &&
        !this.expandOnHover
      );
    }

    /**
     * Changes the selected tab or tab group label on the tab strip
     * relative to the ARIA-focused tab strip element or the active tab. This
     * is intended for traversing the tab strip visually, e.g by using keyboard
     * arrows. For cases where keyboard shortcuts or other logic should only
     * select tabs (and never tab group labels), see `advanceSelectedTab`.
     *
     * @override
     * @param {-1|1} direction
     * @param {boolean} shouldWrap
     */
    advanceSelectedItem(aDir, aWrap) {
      let groupPanel = this.previewPanel?.tabGroupPanel;
      if (groupPanel && groupPanel.isActive) {
        // if the group panel is open, it should receive keyboard focus here
        // instead of moving to the next item in the tabstrip.
        groupPanel.focusPanel(aDir);
        return;
      }

      // cancel any pending group popup since we expect to deselect the label
      this.cancelTabGroupPreview();

      let { ariaFocusableItems, ariaFocusedIndex } = this;

      // Advance relative to the ARIA-focused item if set, otherwise advance
      // relative to the active tab.
      let currentItemIndex =
        ariaFocusedIndex >= 0
          ? ariaFocusedIndex
          : ariaFocusableItems.indexOf(this.selectedItem);

      let newItemIndex = currentItemIndex + aDir;

      if (aWrap) {
        if (newItemIndex >= ariaFocusableItems.length) {
          newItemIndex = 0;
        } else if (newItemIndex < 0) {
          newItemIndex = ariaFocusableItems.length - 1;
        }
      } else {
        newItemIndex = Math.min(
          ariaFocusableItems.length - 1,
          Math.max(0, newItemIndex)
        );
      }

      if (currentItemIndex == newItemIndex) {
        return;
      }

      // If the next item is a tab, select it. If the next item is a tab group
      // label, keep the active tab selected and just set ARIA focus on the tab
      // group label.
      let newItem = ariaFocusableItems[newItemIndex];
      if (isTab(newItem)) {
        this._selectNewTab(newItem, aDir, aWrap);
      }
      this.ariaFocusedItem = newItem;

      // If the newly-focused item is a tab group label and the group is collapsed,
      // proactively show the tab group preview
      if (isTabGroupLabel(this.ariaFocusedItem)) {
        this.showTabGroupPreview(this.ariaFocusedItem.group);
      }
    }

    ensureTabPreviewPanelLoaded() {
      if (!this.previewPanel) {
        const TabHoverPanelSet = ChromeUtils.importESModule(
          "chrome://browser/content/tabbrowser/tab-hover-preview.mjs"
        ).default;
        this.previewPanel = new TabHoverPanelSet(window);
      }
    }

    appendChild(tab) {
      return this.insertBefore(tab, null);
    }

    insertBefore(tab, node) {
      if (!this.arrowScrollbox) {
        throw new Error("Shouldn't call this without arrowscrollbox");
      }

      if (node == null) {
        // We have a container for non-tab elements at the end of the scrollbox.
        node = this.arrowScrollbox.lastChild;
      }

      node.before(tab);

      if (this.#mustUpdateTabMinHeight) {
        this.#updateTabMinHeight();
      }
    }

    #updateTabMinWidth(val) {
      this.style.setProperty(
        "--tab-min-width-pref",
        (val ?? this._tabMinWidthPref) + "px"
      );
    }

    #updateTabMinHeight() {
      if (!this.verticalMode || !window.toolbar.visible) {
        this.#mustUpdateTabMinHeight = false;
        return;
      }

      // Find at least one tab we can scroll to.
      let firstScrollableTab = this.visibleTabs.find(
        this.arrowScrollbox._canScrollToElement
      );

      if (!firstScrollableTab) {
        // If not, we're in a pickle. We should never get here except if we
        // also don't use the outcome of this work (because there's nothing to
        // scroll so we don't care about the scrollbox size).
        // So just set a flag so we re-run once we do have a new tab.
        this.#mustUpdateTabMinHeight = true;
        return;
      }

      let { height } =
        window.windowUtils.getBoundsWithoutFlushing(firstScrollableTab);

      // Use the current known height or a sane default.
      this.#tabMinHeight = height || 36;

      // The height we got may be incorrect if a flush is pending so re-check it after
      // a flush completes.
      window
        .promiseDocumentFlushed(() => {})
        .then(
          () => {
            height =
              window.windowUtils.getBoundsWithoutFlushing(
                firstScrollableTab
              ).height;

            if (height) {
              this.#tabMinHeight = height;
            }
          },
          () => {
            /* ignore errors */
          }
        );
    }

    get _isCustomizing() {
      return document.documentElement.hasAttribute("customizing");
    }

    // This overrides the TabsBase _selectNewTab method so that we can
    // potentially interrupt keyboard tab switching when sharing the
    // window or screen.
    _selectNewTab(aNewTab, aFallbackDir, aWrap) {
      if (!gSharedTabWarning.willShowSharedTabWarning(aNewTab)) {
        super._selectNewTab(aNewTab, aFallbackDir, aWrap);
      }
    }

    observe(aSubject, aTopic) {
      switch (aTopic) {
        case "nsPref:changed": {
          // This is has to deal with changes in
          // privacy.userContext.enabled and
          // privacy.userContext.newTabContainerOnLeftClick.enabled.
          let containersEnabled =
            Services.prefs.getBoolPref("privacy.userContext.enabled") &&
            !PrivateBrowsingUtils.isWindowPrivate(window);

          // This pref won't change so often, so just recreate the menu.
          const newTabLeftClickOpensContainersMenu = Services.prefs.getBoolPref(
            "privacy.userContext.newTabContainerOnLeftClick.enabled"
          );

          // There are separate "new tab" buttons for horizontal tabs toolbar, vertical tabs and
          // for when the tab strip is overflowed (which is shared by vertical and horizontal tabs);
          // Attach the long click popup to all of them.
          const newTab = document.getElementById("new-tab-button");
          const newTab2 = this.newTabButton;
          const newTabVertical = document.getElementById(
            "vertical-tabs-newtab-button"
          );

          for (let parent of [newTab, newTab2, newTabVertical]) {
            if (!parent) {
              continue;
            }

            parent.removeAttribute("type");
            if (parent.menupopup) {
              parent.menupopup.remove();
            }

            if (containersEnabled) {
              parent.setAttribute("context", "new-tab-button-popup");

              let popup = document
                .getElementById("new-tab-button-popup")
                .cloneNode(true);
              popup.removeAttribute("id");
              popup.className = "new-tab-popup";
              popup.setAttribute("position", "after_end");
              popup.addEventListener("popupshowing", CreateContainerTabMenu);
              parent.prepend(popup);
              parent.setAttribute("type", "menu");
              // Update tooltip text
              DynamicShortcutTooltip.nodeToTooltipMap[parent.id] =
                newTabLeftClickOpensContainersMenu
                  ? "newTabAlwaysContainer.tooltip"
                  : "newTabContainer.tooltip";
            } else {
              DynamicShortcutTooltip.nodeToTooltipMap[parent.id] =
                "newTabButton.tooltip";
              parent.removeAttribute("context", "new-tab-button-popup");
            }
            // evict from tooltip cache
            DynamicShortcutTooltip.cache.delete(parent.id);

            // If containers and press-hold container menu are both used,
            // add to gClickAndHoldListenersOnElement; otherwise, remove.
            if (containersEnabled && !newTabLeftClickOpensContainersMenu) {
              gClickAndHoldListenersOnElement.add(parent);
            } else {
              gClickAndHoldListenersOnElement.remove(parent);
            }
          }

          break;
        }
      }
    }

    _updateCloseButtons() {
      if (this.overflowing) {
        // Tabs are at their minimum widths.
        this.setAttribute("closebuttons", "activetab");
        return;
      }

      if (this._closeButtonsUpdatePending) {
        return;
      }
      this._closeButtonsUpdatePending = true;

      // Wait until after the next paint to get current layout data from
      // getBoundsWithoutFlushing.
      window.requestAnimationFrame(() => {
        window.requestAnimationFrame(() => {
          this._closeButtonsUpdatePending = false;

          // The scrollbox may have started overflowing since we checked
          // overflow earlier, so check again.
          if (this.overflowing) {
            this.setAttribute("closebuttons", "activetab");
            return;
          }

          // Check if tab widths are below the threshold where we want to
          // remove close buttons from background tabs so that people don't
          // accidentally close tabs by selecting them.
          let rect = ele => {
            return window.windowUtils.getBoundsWithoutFlushing(ele);
          };
          // See bug 2007766, we need to find the first tab that isn't
          // inside a split view, because those can be narrower than the threshold.
          let tab = this.visibleTabs
            .slice(gBrowser.pinnedTabCount)
            .find(t => !t.splitview);
          if (tab && rect(tab).width <= this._tabClipWidth) {
            this.setAttribute("closebuttons", "activetab");
          } else {
            this.removeAttribute("closebuttons");
          }
        });
      });
    }

    /**
     * @param {boolean} [aInstant]
     */
    _handleTabSelect(aInstant) {
      let selectedTab = this.selectedItem;
      this.#ensureTabIsVisible(selectedTab, aInstant);

      selectedTab._notselectedsinceload = false;
    }

    /**
     * @param {MozTabbrowserTab} tab
     * @param {boolean} [shouldScrollInstantly=false]
     */
    #ensureTabIsVisible(tab, shouldScrollInstantly = false) {
      let arrowScrollbox = tab.closest("arrowscrollbox");
      if (arrowScrollbox?.overflowing) {
        arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly);
      }
    }

    /**
     * Try to keep the active tab's close button under the mouse cursor
     */
    _lockTabSizing(aClosingTab, aTabWidth) {
      if (this.verticalMode) {
        return;
      }

      let tabs = this.visibleTabs;
      let numPinned = gBrowser.pinnedTabCount;

      if (tabs.length <= numPinned) {
        // There are no unpinned tabs left.
        return;
      }

      let isEndTab = aClosingTab && aClosingTab._tPos > tabs.at(-1)._tPos;

      if (!this._tabDefaultMaxWidth) {
        this._tabDefaultMaxWidth = parseFloat(
          window.getComputedStyle(tabs[numPinned]).maxWidth
        );
      }
      this._lastTabClosedByMouse = true;
      this._scrollButtonWidth = window.windowUtils.getBoundsWithoutFlushing(
        this.arrowScrollbox._scrollButtonDown
      ).width;
      if (aTabWidth === undefined) {
        aTabWidth = window.windowUtils.getBoundsWithoutFlushing(
          tabs[numPinned]
        ).width;
      }

      if (this.overflowing) {
        // Don't need to do anything if we're in overflow mode and aren't scrolled
        // all the way to the right, or if we're closing the last tab.
        if (isEndTab || !this.arrowScrollbox.hasAttribute("scrolledtoend")) {
          return;
        }
        // If the tab has an owner that will become the active tab, the owner will
        // be to the left of it, so we actually want the left tab to slide over.
        // This can't be done as easily in non-overflow mode, so we don't bother.
        if (aClosingTab?.owner) {
          return;
        }
        this._expandSpacerBy(aTabWidth);
      } /* non-overflow mode */ else {
        if (isEndTab && !this._hasTabTempMaxWidth) {
          // Locking is neither in effect nor needed, so let tabs expand normally.
          return;
        }
        // Force tabs to stay the same width, unless we're closing the last tab,
        // which case we need to let them expand just enough so that the overall
        // tabbar width is the same.
        if (isEndTab) {
          let numNormalTabs = tabs.length - numPinned;
          aTabWidth = (aTabWidth * (numNormalTabs + 1)) / numNormalTabs;
          if (aTabWidth > this._tabDefaultMaxWidth) {
            aTabWidth = this._tabDefaultMaxWidth;
          }
        }
        aTabWidth += "px";
        let tabsToReset = [];
        for (let i = numPinned; i < tabs.length; i++) {
          let tab = tabs[i];
          tab.style.setProperty("max-width", aTabWidth, "important");
          if (!isEndTab) {
            // keep tabs the same width
            tab.animationsEnabled = false;
            tabsToReset.push(tab);
          }
        }

        if (tabsToReset.length) {
          window
            .promiseDocumentFlushed(() => {})
            .then(() => {
              window.requestAnimationFrame(() => {
                for (let tab of tabsToReset) {
                  tab.animationsEnabled = true;
                }
              });
            });
        }

        this._hasTabTempMaxWidth = true;
        gBrowser.addEventListener("mousemove", this);
        window.addEventListener("mouseout", this);
      }
    }

    _expandSpacerBy(pixels) {
      let spacer = this._closingTabsSpacer;
      spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
      this.toggleAttribute("using-closing-tabs-spacer", true);
      gBrowser.addEventListener("mousemove", this);
      window.addEventListener("mouseout", this);
    }

    _unlockTabSizing() {
      gBrowser.removeEventListener("mousemove", this);
      window.removeEventListener("mouseout", this);

      if (this._hasTabTempMaxWidth) {
        this._hasTabTempMaxWidth = false;
        // Only visible tabs have their sizes locked, but those visible tabs
        // could become invisible before being unlocked (e.g. by being inside
        // of a collapsing tab group), so it's better to reset all tabs.
        let tabs = this.allTabs;
        for (let i = 0; i < tabs.length; i++) {
          tabs[i].style.maxWidth = "";
        }
      }

      if (this.hasAttribute("using-closing-tabs-spacer")) {
        this.removeAttribute("using-closing-tabs-spacer");
        this._closingTabsSpacer.style.width = 0;
      }
    }

    _notifyBackgroundTab(aTab) {
      if (aTab.pinned || !aTab.visible || !this.overflowing) {
        return;
      }

      this._lastTabToScrollIntoView = aTab;
      if (!this._backgroundTabScrollPromise) {
        this._backgroundTabScrollPromise = window
          .promiseDocumentFlushed(() => {
            let lastTabRect =
              this._lastTabToScrollIntoView.getBoundingClientRect();
            let selectedTab = this.selectedItem;
            if (selectedTab.pinned) {
              selectedTab = null;
            } else {
              selectedTab = selectedTab.getBoundingClientRect();
              selectedTab = {
                left: selectedTab.left,
                right: selectedTab.right,
                top: selectedTab.top,
                bottom: selectedTab.bottom,
              };
            }
            return [
              this._lastTabToScrollIntoView,
              this.arrowScrollbox.scrollClientRect,
              lastTabRect,
              selectedTab,
            ];
          })
          .then(([tabToScrollIntoView, scrollRect, tabRect, selectedRect]) => {
            // First off, remove the promise so we can re-enter if necessary.
            delete this._backgroundTabScrollPromise;
            // Then, if the layout info isn't for the last-scrolled-to-tab, re-run
            // the code above to get layout info for *that* tab, and don't do
            // anything here, as we really just want to run this for the last-opened tab.
            if (this._lastTabToScrollIntoView != tabToScrollIntoView) {
              this._notifyBackgroundTab(this._lastTabToScrollIntoView);
              return;
            }
            delete this._lastTabToScrollIntoView;
            // Is the new tab already completely visible?
            if (
              this.verticalMode
                ? scrollRect.top <= tabRect.top &&
                  tabRect.bottom <= scrollRect.bottom
                : scrollRect.left <= tabRect.left &&
                  tabRect.right <= scrollRect.right
            ) {
              return;
            }

            if (this.arrowScrollbox.smoothScroll) {
              // Can we make both the new tab and the selected tab completely visible?
              if (
                !selectedRect ||
                (this.verticalMode
                  ? Math.max(
                      tabRect.bottom - selectedRect.top,
                      selectedRect.bottom - tabRect.top
                    ) <= scrollRect.height
                  : Math.max(
                      tabRect.right - selectedRect.left,
                      selectedRect.right - tabRect.left
                    ) <= scrollRect.width)
              ) {
                this.#ensureTabIsVisible(tabToScrollIntoView);
                return;
              }

              let scrollPixels;
              if (this.verticalMode) {
                scrollPixels = tabRect.top - selectedRect.top;
              } else if (this.#rtlMode) {
                scrollPixels = selectedRect.right - scrollRect.right;
              } else {
                scrollPixels = selectedRect.left - scrollRect.left;
              }
              this.arrowScrollbox.scrollByPixels(scrollPixels);
            }

            if (!this._animateElement.hasAttribute("highlight")) {
              this._animateElement.toggleAttribute("highlight", true);
              setTimeout(
                function (ele) {
                  ele.removeAttribute("highlight");
                },
                150,
                this._animateElement
              );
            }
          });
      }
    }

    _handleNewTab(tab) {
      if (tab.container != this) {
        return;
      }
      tab._fullyOpen = true;
      gBrowser.tabAnimationsInProgress--;

      this._updateCloseButtons();

      if (tab.hasAttribute("selected")) {
        this._handleTabSelect();
      } else if (!tab.hasAttribute("skipbackgroundnotify")) {
        this._notifyBackgroundTab(tab);
      }

      // If this browser isn't lazy (indicating it's probably created by
      // session restore), preload the next about:newtab if we don't
      // already have a preloaded browser.
      if (tab.linkedPanel) {
        NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
      }

      if (UserInteraction.running("browser.tabs.opening", window)) {
        UserInteraction.finish("browser.tabs.opening", window);
      }
    }

    _canAdvanceToTab(aTab) {
      return !aTab.closing;
    }

    /**
     * Returns the panel associated with a tab if it has a connected browser
     * and/or it is the selected tab.
     * For background lazy browsers, this will return null.
     */
    getRelatedElement(aTab) {
      if (!aTab) {
        return null;
      }

      // Cannot access gBrowser before it's initialized.
      if (!gBrowser._initialized) {
        return this.tabbox.tabpanels.firstElementChild;
      }

      // If the tab's browser is lazy, we need to `_insertBrowser` in order
      // to have a linkedPanel.  This will also serve to bind the browser
      // and make it ready to use. We only do this if the tab is selected
      // because otherwise, callers might end up unintentionally binding the
      // browser for lazy background tabs.
      if (!aTab.linkedPanel) {
        if (!aTab.selected) {
          return null;
        }
        gBrowser._insertBrowser(aTab);
      }
      return document.getElementById(aTab.linkedPanel);
    }

    _updateNewTabVisibility() {
      // Helper functions to help deal with customize mode wrapping some items
      let wrap = n =>
        n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
      let unwrap = n =>
        n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;

      // Starting from the tabs element, find the next sibling that:
      // - isn't hidden; and
      // - isn't the all-tabs button.
      // If it's the new tab button, consider the new tab button adjacent to the tabs.
      // If the new tab button is marked as adjacent and the tabstrip doesn't
      // overflow, we'll display the 'new tab' button inline in the tabstrip.
      // In all other cases, the separate new tab button is displayed in its
      // customized location.
      let sib = this;
      do {
        sib = unwrap(wrap(sib).nextElementSibling);
      } while (sib && (sib.hidden || sib.id == "alltabs-button"));

      this.toggleAttribute(
        "hasadjacentnewtabbutton",
        sib && sib.id == "new-tab-button"
      );
    }

    onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
      if (
        aContainer.ownerDocument == document &&
        aContainer.id == "TabsToolbar-customization-target"
      ) {
        this._updateNewTabVisibility();
      }
    }

    onAreaNodeRegistered(aArea, aContainer) {
      if (aContainer.ownerDocument == document && aArea == "TabsToolbar") {
        this._updateNewTabVisibility();
      }
    }

    onAreaReset(aArea, aContainer) {
      this.onAreaNodeRegistered(aArea, aContainer);
    }

    _hiddenSoundPlayingStatusChanged(tab, opts) {
      let closed = opts && opts.closed;
      if (!closed && tab.soundPlaying && !tab.visible) {
        this._hiddenSoundPlayingTabs.add(tab);
        this.toggleAttribute("hiddensoundplaying", true);
      } else {
        this._hiddenSoundPlayingTabs.delete(tab);
        if (this._hiddenSoundPlayingTabs.size == 0) {
          this.removeAttribute("hiddensoundplaying");
        }
      }
    }

    destroy() {
      if (this.boundObserve) {
        Services.prefs.removeObserver("privacy.userContext", this.boundObserve);
      }
      CustomizableUI.removeListener(this);
    }

    updateTabSoundLabel(tab) {
      // Add aria-label for inline audio button
      const [unmute, mute, unblock] =
        gBrowser.tabLocalization.formatMessagesSync([
          "tabbrowser-unmute-tab-audio-aria-label",
          "tabbrowser-mute-tab-audio-aria-label",
          "tabbrowser-unblock-tab-audio-aria-label",
        ]);
      if (tab.audioButton) {
        if (tab.hasAttribute("muted") || tab.hasAttribute("soundplaying")) {
          let ariaLabel;
          tab.linkedBrowser.audioMuted
            ? (ariaLabel = unmute.attributes[0].value)
            : (ariaLabel = mute.attributes[0].value);
          tab.audioButton.setAttribute("aria-label", ariaLabel);
        } else if (tab.hasAttribute("activemedia-blocked")) {
          tab.audioButton.setAttribute(
            "aria-label",
            unblock.attributes[0].value
          );
        }
      }
    }
  }

  customElements.define("tabbrowser-tabs", MozTabbrowserTabs, {
    extends: "tabs",
  });
}
