tor-browser

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

tab.js (25968B)


      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 chrome windows with the subscript loader. Wrap in
      8 // a block to prevent accidentally leaking globals onto `window`.
      9 {
     10  const lazy = {};
     11  ChromeUtils.defineESModuleGetters(lazy, {
     12    TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs",
     13  });
     14 
     15  class MozTabbrowserTab extends MozElements.MozTab {
     16    static markup = `
     17      <stack class="tab-stack" flex="1">
     18        <vbox class="tab-background">
     19          <hbox class="tab-context-line"/>
     20          <hbox class="tab-loading-burst" flex="1"/>
     21          <hbox class="tab-group-line"/>
     22        </vbox>
     23        <hbox class="tab-content" align="center">
     24          <stack class="tab-icon-stack">
     25            <hbox class="tab-throbber"/>
     26            <hbox class="tab-icon-pending"/>
     27            <html:img class="tab-icon-image" role="presentation" decoding="sync" />
     28            <image class="tab-sharing-icon-overlay" role="presentation"/>
     29            <image class="tab-icon-overlay" role="presentation"/>
     30            <image class="tab-note-icon-overlay" role="presentation"/>
     31          </stack>
     32          <html:moz-button type="icon ghost" size="small" class="tab-audio-button" tabindex="-1"></html:moz-button>
     33          <vbox class="tab-label-container"
     34                align="start"
     35                pack="center"
     36                flex="1">
     37            <label class="tab-text tab-label" role="presentation"/>
     38            <hbox class="tab-secondary-label">
     39              <label class="tab-icon-sound-label tab-icon-sound-pip-label" data-l10n-id="browser-tab-audio-pip" role="presentation"/>
     40            </hbox>
     41          </vbox>
     42          <image class="tab-note-icon" role="presentation"/>
     43          <image class="tab-close-button close-icon" role="button" data-l10n-id="tabbrowser-close-tabs-button" data-l10n-args='{"tabCount": 1}' keyNav="false"/>
     44        </hbox>
     45      </stack>
     46      `;
     47 
     48    constructor() {
     49      super();
     50 
     51      this.addEventListener("mouseover", this);
     52      this.addEventListener("mouseout", this);
     53      this.addEventListener("dragstart", this, true);
     54      this.addEventListener("dragstart", this);
     55      this.addEventListener("mousedown", this);
     56      this.addEventListener("mouseup", this);
     57      this.addEventListener("click", this);
     58      this.addEventListener("dblclick", this, true);
     59      this.addEventListener("animationstart", this);
     60      this.addEventListener("animationend", this);
     61      this.addEventListener("focus", this);
     62      this.addEventListener("AriaFocus", this);
     63 
     64      this._hover = false;
     65      this._selectedOnFirstMouseDown = false;
     66 
     67      /**
     68       * Describes how the tab ended up in this mute state. May be any of:
     69       *
     70       * - undefined: The tabs mute state has never changed.
     71       * - null: The mute state was last changed through the UI.
     72       * - Any string: The ID was changed through an extension API. The string
     73       * must be the ID of the extension which changed it.
     74       */
     75      this.muteReason = undefined;
     76 
     77      this.closing = false;
     78    }
     79 
     80    static get inheritedAttributes() {
     81      return {
     82        ".tab-background":
     83          "selected=visuallyselected,fadein,multiselected,dragover-groupTarget",
     84        ".tab-group-line": "selected=visuallyselected,multiselected",
     85        ".tab-loading-burst": "pinned,bursting,notselectedsinceload",
     86        ".tab-content":
     87          "pinned,selected=visuallyselected,multiselected,titlechanged,attention",
     88        ".tab-icon-stack":
     89          "sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked",
     90        ".tab-throbber":
     91          "fadein,pinned,busy,progress,selected=visuallyselected",
     92        ".tab-icon-pending":
     93          "fadein,pinned,busy,progress,selected=visuallyselected,pendingicon",
     94        ".tab-icon-image":
     95          "src=image,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing,pictureinpicture,pending,discarded",
     96        ".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned",
     97        ".tab-icon-overlay":
     98          "sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked",
     99        ".tab-audio-button":
    100          "crashed,soundplaying,soundplaying-scheduledremoval,pinned,muted,activemedia-blocked",
    101        ".tab-label-container":
    102          "pinned,selected=visuallyselected,labeldirection",
    103        ".tab-label":
    104          "text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
    105        ".tab-label-container .tab-secondary-label":
    106          "pinned,blocked,selected=visuallyselected,pictureinpicture",
    107        ".tab-close-button": "fadein,pinned,selected=visuallyselected",
    108      };
    109    }
    110 
    111    #lastGroup;
    112    connectedCallback() {
    113      this.#updateOnTabGrouped();
    114      this.#updateOnTabSplit();
    115      this.#lastGroup = this.group;
    116 
    117      this.initialize();
    118    }
    119 
    120    disconnectedCallback() {
    121      this.#updateOnTabUngrouped();
    122      this.#updateOnTabUnsplit();
    123    }
    124 
    125    initialize() {
    126      if (this._initialized) {
    127        return;
    128      }
    129 
    130      this.textContent = "";
    131      this.appendChild(this.constructor.fragment);
    132      this.initializeAttributeInheritance();
    133      this.setAttribute("context", "tabContextMenu");
    134      this._initialized = true;
    135 
    136      if (!("_lastAccessed" in this)) {
    137        this.updateLastAccessed();
    138      }
    139 
    140      let labelContainer = this.querySelector(".tab-label-container");
    141      labelContainer.addEventListener("overflow", this);
    142      labelContainer.addEventListener("underflow", this);
    143 
    144      // Tabs in the tab strip default to being at the top level (level 1)
    145      // Tabs in tab groups are one level down (level 2); this tab will
    146      // update its value when it moves in and out of tab groups.
    147      this.setAttribute("aria-level", 1);
    148    }
    149 
    150    #elementIndex;
    151    get elementIndex() {
    152      if (!this.visible) {
    153        throw new Error("Tab is not visible, so does not have an elementIndex");
    154      }
    155      // Make sure the index is up to date.
    156      this.container.dragAndDropElements;
    157      return this.#elementIndex;
    158    }
    159 
    160    set elementIndex(index) {
    161      this.#elementIndex = index;
    162    }
    163 
    164    #owner;
    165    get owner() {
    166      let owner = this.#owner?.deref();
    167      if (owner && !owner.closing) {
    168        return owner;
    169      }
    170      return null;
    171    }
    172 
    173    set owner(owner) {
    174      this.#owner = owner ? new WeakRef(owner) : null;
    175    }
    176 
    177    get container() {
    178      return gBrowser.tabContainer;
    179    }
    180 
    181    set attention(val) {
    182      if (val == this.hasAttribute("attention")) {
    183        return;
    184      }
    185 
    186      this.toggleAttribute("attention", val);
    187      gBrowser._tabAttrModified(this, ["attention"]);
    188    }
    189 
    190    set _visuallySelected(val) {
    191      if (val == this.hasAttribute("visuallyselected")) {
    192        return;
    193      }
    194 
    195      this.toggleAttribute("visuallyselected", val);
    196      gBrowser._tabAttrModified(this, ["visuallyselected"]);
    197    }
    198 
    199    set _selected(val) {
    200      // in e10s we want to only pseudo-select a tab before its rendering is done, so that
    201      // the rest of the system knows that the tab is selected, but we don't want to update its
    202      // visual status to selected until after we receive confirmation that its content has painted.
    203      if (val) {
    204        this.setAttribute("selected", "true");
    205      } else {
    206        this.removeAttribute("selected");
    207      }
    208 
    209      // If we're non-e10s we need to update the visual selection at the same
    210      // time, otherwise AsyncTabSwitcher will take care of this.
    211      if (!gMultiProcessBrowser) {
    212        this._visuallySelected = val;
    213      }
    214    }
    215 
    216    get pinned() {
    217      return this.hasAttribute("pinned");
    218    }
    219 
    220    get isOpen() {
    221      return (
    222        this.isConnected && !this.closing && this != FirefoxViewHandler.tab
    223      );
    224    }
    225 
    226    get visible() {
    227      return (
    228        this.isOpen &&
    229        !this.hidden &&
    230        (!this.group || this.group.isTabVisibleInGroup(this))
    231      );
    232    }
    233 
    234    get hidden() {
    235      // This getter makes `hidden` read-only
    236      return super.hidden;
    237    }
    238 
    239    get muted() {
    240      return this.hasAttribute("muted");
    241    }
    242 
    243    get multiselected() {
    244      return this.hasAttribute("multiselected");
    245    }
    246 
    247    get userContextId() {
    248      return this.hasAttribute("usercontextid")
    249        ? parseInt(this.getAttribute("usercontextid"))
    250        : 0;
    251    }
    252 
    253    get soundPlaying() {
    254      return this.hasAttribute("soundplaying");
    255    }
    256 
    257    get pictureinpicture() {
    258      return this.hasAttribute("pictureinpicture");
    259    }
    260 
    261    get activeMediaBlocked() {
    262      return this.hasAttribute("activemedia-blocked");
    263    }
    264 
    265    get undiscardable() {
    266      return this.hasAttribute("undiscardable");
    267    }
    268 
    269    set undiscardable(val) {
    270      if (val == this.hasAttribute("undiscardable")) {
    271        return;
    272      }
    273 
    274      this.toggleAttribute("undiscardable", val);
    275      gBrowser._tabAttrModified(this, ["undiscardable"]);
    276    }
    277 
    278    get animationsEnabled() {
    279      return this.style.transition == "";
    280    }
    281 
    282    set animationsEnabled(val) {
    283      this.style.transition = val ? "" : "none";
    284    }
    285 
    286    get isEmpty() {
    287      // Determines if a tab is "empty", usually used in the context of determining
    288      // if it's ok to close the tab.
    289      if (this.hasAttribute("busy")) {
    290        return false;
    291      }
    292 
    293      if (this.hasAttribute("customizemode")) {
    294        return false;
    295      }
    296 
    297      let browser = this.linkedBrowser;
    298      if (!isBlankPageURL(browser.currentURI.spec)) {
    299        return false;
    300      }
    301 
    302      if (!BrowserUIUtils.checkEmptyPageOrigin(browser)) {
    303        return false;
    304      }
    305 
    306      if (browser.canGoForward || browser.canGoBack) {
    307        return false;
    308      }
    309 
    310      return true;
    311    }
    312 
    313    get lastAccessed() {
    314      return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
    315    }
    316 
    317    /**
    318     * Returns a timestamp which attempts to represent the last time the user saw this tab.
    319     * If the tab has not been active in this session, any lastAccessed is used. We
    320     * differentiate between selected and explicitly visible; a selected tab in a hidden
    321     * window is last seen when that window and tab were last visible.
    322     * We use the application start time as a fallback value when no other suitable value
    323     * is available.
    324     */
    325    get lastSeenActive() {
    326      const isForegroundWindow =
    327        this.ownerGlobal ==
    328        BrowserWindowTracker.getTopWindow({ allowPopups: true });
    329      // the timestamp for the selected tab in the active window is always now
    330      if (isForegroundWindow && this.selected) {
    331        return Date.now();
    332      }
    333      if (this._lastSeenActive) {
    334        return this._lastSeenActive;
    335      }
    336 
    337      if (
    338        !this._lastAccessed ||
    339        this._lastAccessed >= this.container.startupTime
    340      ) {
    341        // When the tab was created this session but hasn't been seen by the user,
    342        // default to the application start time.
    343        return this.container.startupTime;
    344      }
    345      // The tab was restored from a previous session but never seen.
    346      // Use the lastAccessed as the best proxy for when the user might have seen it.
    347      return this._lastAccessed;
    348    }
    349 
    350    get _overPlayingIcon() {
    351      return this.overlayIcon?.matches(":hover");
    352    }
    353 
    354    get _overAudioButton() {
    355      return this.audioButton?.matches(":hover");
    356    }
    357 
    358    get overlayIcon() {
    359      return this.querySelector(".tab-icon-overlay");
    360    }
    361 
    362    get audioButton() {
    363      return this.querySelector(".tab-audio-button");
    364    }
    365 
    366    get throbber() {
    367      return this.querySelector(".tab-throbber");
    368    }
    369 
    370    get iconImage() {
    371      return this.querySelector(".tab-icon-image");
    372    }
    373 
    374    get sharingIcon() {
    375      return this.querySelector(".tab-sharing-icon-overlay");
    376    }
    377 
    378    get textLabel() {
    379      return this.querySelector(".tab-label");
    380    }
    381 
    382    get closeButton() {
    383      return this.querySelector(".tab-close-button");
    384    }
    385 
    386    get group() {
    387      return this.closest("tab-group");
    388    }
    389 
    390    get splitview() {
    391      if (this.parentElement?.tagName == "tab-split-view-wrapper") {
    392        return this.parentElement;
    393      }
    394      return null;
    395    }
    396 
    397    /**
    398     * @returns {boolean}
    399     */
    400    get hasTabNote() {
    401      return this.hasAttribute("tab-note");
    402    }
    403 
    404    /**
    405     * @param {boolean} val
    406     */
    407    set hasTabNote(val) {
    408      this.toggleAttribute("tab-note", val);
    409    }
    410 
    411    updateLastAccessed(aDate) {
    412      this._lastAccessed = this.selected ? Infinity : aDate || Date.now();
    413    }
    414 
    415    updateLastSeenActive() {
    416      this._lastSeenActive = Date.now();
    417    }
    418 
    419    updateLastUnloadedByTabUnloader() {
    420      this._lastUnloaded = Date.now();
    421      Glean.browserEngagement.tabUnloadCount.add(1);
    422    }
    423 
    424    recordTimeFromUnloadToReload() {
    425      if (!this._lastUnloaded) {
    426        return;
    427      }
    428 
    429      const diff_in_msec = Date.now() - this._lastUnloaded;
    430      Glean.browserEngagement.tabUnloadToReload.accumulateSingleSample(
    431        diff_in_msec / 1000
    432      );
    433      Glean.browserEngagement.tabReloadCount.add(1);
    434      delete this._lastUnloaded;
    435    }
    436 
    437    on_mouseover(event) {
    438      if (!this.visible) {
    439        return;
    440      }
    441 
    442      let tabToWarm = event.target.classList.contains("tab-close-button")
    443        ? gBrowser._findTabToBlurTo(this)
    444        : this;
    445      gBrowser.warmupTab(tabToWarm);
    446 
    447      // If the previous target wasn't part of this tab then this is a mouseenter event.
    448      if (!this.contains(event.relatedTarget)) {
    449        this._mouseenter();
    450      }
    451    }
    452 
    453    on_mouseout(event) {
    454      // If the new target is not part of this tab then this is a mouseleave event.
    455      if (!this.contains(event.relatedTarget)) {
    456        this._mouseleave();
    457      }
    458    }
    459 
    460    on_dragstart(event) {
    461      // We use "failed" drag end events that weren't cancelled by the user
    462      // to detach tabs. Ensure that we do not show the drag image returning
    463      // to its point of origin when this happens, as it makes the drag
    464      // finishing feel very slow.
    465      event.dataTransfer.mozShowFailAnimation = false;
    466      if (event.eventPhase == Event.CAPTURING_PHASE) {
    467        this.style.MozUserFocus = "";
    468      } else if (
    469        event.target.classList?.contains("tab-close-button") ||
    470        gSharedTabWarning.willShowSharedTabWarning(this)
    471      ) {
    472        event.stopPropagation();
    473      }
    474    }
    475 
    476    on_mousedown(event) {
    477      let eventMaySelectTab = true;
    478      let tabContainer = this.container;
    479 
    480      if (
    481        tabContainer._closeTabByDblclick &&
    482        event.button == 0 &&
    483        event.detail == 1
    484      ) {
    485        this._selectedOnFirstMouseDown = this.selected;
    486      }
    487 
    488      if (this.selected) {
    489        this.style.MozUserFocus = "ignore";
    490      } else if (
    491        event.target.classList.contains("tab-close-button") ||
    492        event.target.classList.contains("tab-icon-overlay") ||
    493        event.target.classList.contains("tab-audio-button")
    494      ) {
    495        eventMaySelectTab = false;
    496      }
    497 
    498      if (event.button == 1) {
    499        gBrowser.warmupTab(gBrowser._findTabToBlurTo(this));
    500      }
    501 
    502      if (event.button == 0) {
    503        let shiftKey = event.shiftKey;
    504        let accelKey = event.getModifierState("Accel");
    505        if (shiftKey) {
    506          eventMaySelectTab = false;
    507          const lastSelectedTab = gBrowser.lastMultiSelectedTab;
    508          if (!accelKey) {
    509            gBrowser.selectedTab = lastSelectedTab;
    510 
    511            // Make sure selection is cleared when tab-switch doesn't happen.
    512            gBrowser.clearMultiSelectedTabs();
    513          }
    514          gBrowser.addRangeToMultiSelectedTabs(lastSelectedTab, this);
    515        } else if (accelKey) {
    516          // Ctrl (Cmd for mac) key is pressed
    517          eventMaySelectTab = false;
    518          if (this.multiselected) {
    519            gBrowser.removeFromMultiSelectedTabs(this);
    520          } else if (this != gBrowser.selectedTab) {
    521            gBrowser.addToMultiSelectedTabs(this);
    522            gBrowser.lastMultiSelectedTab = this;
    523          }
    524        } else if (!this.selected && this.multiselected) {
    525          gBrowser.lockClearMultiSelectionOnce();
    526        }
    527      }
    528 
    529      if (gSharedTabWarning.willShowSharedTabWarning(this)) {
    530        eventMaySelectTab = false;
    531      }
    532 
    533      if (eventMaySelectTab) {
    534        super.on_mousedown(event);
    535      }
    536    }
    537 
    538    on_mouseup() {
    539      // Make sure that clear-selection is released.
    540      // Otherwise selection using Shift key may be broken.
    541      gBrowser.unlockClearMultiSelection();
    542 
    543      this.style.MozUserFocus = "";
    544    }
    545 
    546    on_click(event) {
    547      if (event.button != 0) {
    548        return;
    549      }
    550 
    551      if (event.getModifierState("Accel") || event.shiftKey) {
    552        return;
    553      }
    554 
    555      if (
    556        gBrowser.multiSelectedTabsCount > 0 &&
    557        !event.target.classList.contains("tab-close-button") &&
    558        !event.target.classList.contains("tab-icon-overlay") &&
    559        !event.target.classList.contains("tab-audio-button")
    560      ) {
    561        // Tabs were previously multi-selected and user clicks on a tab
    562        // without holding Ctrl/Cmd Key
    563        gBrowser.clearMultiSelectedTabs();
    564      }
    565 
    566      if (
    567        event.target.classList.contains("tab-icon-overlay") ||
    568        event.target.classList.contains("tab-audio-button")
    569      ) {
    570        if (this.activeMediaBlocked) {
    571          if (this.multiselected) {
    572            gBrowser.resumeDelayedMediaOnMultiSelectedTabs(this);
    573          } else {
    574            this.resumeDelayedMedia();
    575          }
    576        } else if (this.soundPlaying || this.muted) {
    577          if (this.multiselected) {
    578            gBrowser.toggleMuteAudioOnMultiSelectedTabs(this);
    579          } else {
    580            this.toggleMuteAudio();
    581          }
    582        }
    583        return;
    584      }
    585 
    586      if (event.target.classList.contains("tab-close-button")) {
    587        if (this.multiselected) {
    588          gBrowser.removeMultiSelectedTabs(
    589            lazy.TabMetrics.userTriggeredContext(
    590              lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP
    591            )
    592          );
    593        } else {
    594          gBrowser.removeTab(this, {
    595            animate: true,
    596            triggeringEvent: event,
    597            ...lazy.TabMetrics.userTriggeredContext(
    598              lazy.TabMetrics.METRIC_SOURCE.TAB_STRIP
    599            ),
    600          });
    601        }
    602        // This enables double-click protection for the tab container
    603        // (see tabbrowser-tabs 'click' handler).
    604        gBrowser.tabContainer._blockDblClick = true;
    605      }
    606    }
    607 
    608    on_dblclick(event) {
    609      if (event.button != 0) {
    610        return;
    611      }
    612 
    613      // for the one-close-button case
    614      if (event.target.classList.contains("tab-close-button")) {
    615        event.stopPropagation();
    616      }
    617 
    618      let tabContainer = this.container;
    619      if (
    620        tabContainer._closeTabByDblclick &&
    621        this._selectedOnFirstMouseDown &&
    622        this.selected &&
    623        !event.target.classList.contains("tab-icon-overlay")
    624      ) {
    625        gBrowser.removeTab(this, {
    626          animate: true,
    627          triggeringEvent: event,
    628        });
    629      }
    630    }
    631 
    632    on_animationstart(event) {
    633      if (!event.animationName.startsWith("tab-throbber-animation")) {
    634        return;
    635      }
    636      // The animation is on a pseudo-element so we need to use `subtree: true`
    637      // to get our hands on it.
    638      for (let animation of event.target.getAnimations({ subtree: true })) {
    639        if (animation.animationName === event.animationName) {
    640          // Ensure all tab throbber animations are synchronized by sharing an
    641          // start time.
    642          animation.startTime = 0;
    643        }
    644      }
    645    }
    646 
    647    on_animationend(event) {
    648      if (event.target.classList.contains("tab-loading-burst")) {
    649        this.removeAttribute("bursting");
    650      }
    651    }
    652 
    653    _mouseenter() {
    654      this._hover = true;
    655 
    656      if (this.selected) {
    657        this.container._handleTabSelect();
    658      } else if (this.linkedPanel) {
    659        this.linkedBrowser.unselectedTabHover(true);
    660      }
    661 
    662      // Prepare connection to host beforehand.
    663      SessionStore.speculativeConnectOnTabHover(this);
    664 
    665      this.dispatchEvent(new CustomEvent("TabHoverStart", { bubbles: true }));
    666    }
    667 
    668    _mouseleave() {
    669      if (!this._hover) {
    670        return;
    671      }
    672      this._hover = false;
    673      if (this.linkedPanel && !this.selected) {
    674        this.linkedBrowser.unselectedTabHover(false);
    675      }
    676      this.dispatchEvent(new CustomEvent("TabHoverEnd", { bubbles: true }));
    677    }
    678 
    679    resumeDelayedMedia() {
    680      if (this.activeMediaBlocked) {
    681        this.removeAttribute("activemedia-blocked");
    682        this.linkedBrowser.resumeMedia();
    683        gBrowser._tabAttrModified(this, ["activemedia-blocked"]);
    684      }
    685    }
    686 
    687    toggleMuteAudio(aMuteReason) {
    688      let browser = this.linkedBrowser;
    689      if (browser.audioMuted) {
    690        if (this.linkedPanel) {
    691          // "Lazy Browser" should not invoke its unmute method
    692          browser.unmute();
    693        }
    694        this.removeAttribute("muted");
    695      } else {
    696        if (this.linkedPanel) {
    697          // "Lazy Browser" should not invoke its mute method
    698          browser.mute();
    699        }
    700        this.toggleAttribute("muted", true);
    701      }
    702      this.muteReason = aMuteReason || null;
    703 
    704      gBrowser._tabAttrModified(this, ["muted"]);
    705    }
    706 
    707    setUserContextId(aUserContextId) {
    708      if (aUserContextId) {
    709        if (this.linkedBrowser) {
    710          this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
    711        }
    712        this.setAttribute("usercontextid", aUserContextId);
    713      } else {
    714        if (this.linkedBrowser) {
    715          this.linkedBrowser.removeAttribute("usercontextid");
    716        }
    717        this.removeAttribute("usercontextid");
    718      }
    719 
    720      ContextualIdentityService.setTabStyle(this);
    721    }
    722 
    723    updateA11yDescription() {
    724      let prevDescTab = gBrowser.tabContainer.querySelector(
    725        "tab[aria-describedby]"
    726      );
    727      if (prevDescTab) {
    728        // We can only have a description for the focused tab.
    729        prevDescTab.removeAttribute("aria-describedby");
    730      }
    731      let desc = document.getElementById("tabbrowser-tab-a11y-desc");
    732      desc.textContent = gBrowser.getTabTooltip(this, false);
    733      this.setAttribute("aria-describedby", "tabbrowser-tab-a11y-desc");
    734    }
    735 
    736    on_focus() {
    737      this.updateA11yDescription();
    738    }
    739 
    740    on_AriaFocus() {
    741      this.updateA11yDescription();
    742    }
    743 
    744    on_overflow(event) {
    745      event.currentTarget.toggleAttribute("textoverflow", true);
    746    }
    747 
    748    on_underflow(event) {
    749      event.currentTarget.removeAttribute("textoverflow");
    750    }
    751 
    752    #updateOnTabGrouped() {
    753      if (this.group && this.#lastGroup != this.group) {
    754        // Trigger TabGrouped on the tab group, not the tab itself. This is a
    755        // bit unorthodox, but fixes bug1964152 where tab group events are not
    756        // fired correctly when tabs change windows (because the tab is
    757        // detached from the DOM at time of the event).
    758        this.group.dispatchEvent(
    759          new CustomEvent("TabGrouped", {
    760            bubbles: true,
    761            detail: this,
    762          })
    763        );
    764        this.setAttribute("aria-level", 2);
    765      }
    766    }
    767 
    768    #updateOnTabUngrouped() {
    769      if (this.#lastGroup && this.#lastGroup != this.group) {
    770        // Trigger TabUngrouped on the tab group, not the tab itself. This is a
    771        // bit unorthodox, but fixes bug1964152 where tab group events are not
    772        // fired correctly when tabs change windows (because the tab is
    773        // detached from the DOM at time of the event).
    774        this.#lastGroup.dispatchEvent(
    775          new CustomEvent("TabUngrouped", {
    776            bubbles: true,
    777            detail: this,
    778          })
    779        );
    780        // Tab could have moved to be ungrouped (level 1)
    781        // or to a different group (level 2).
    782        this.setAttribute("aria-level", this.group ? 2 : 1);
    783        // `posinset` and `setsize` only need to be set explicitly
    784        // on grouped tabs so that a11y tools can tell users that a
    785        // given tab is "2 of 7" in the group, for example.
    786        this.removeAttribute("aria-posinset");
    787        this.removeAttribute("aria-setsize");
    788      }
    789    }
    790 
    791    #updateOnTabSplit() {
    792      if (this.splitview) {
    793        this.setAttribute("aria-level", 2);
    794      }
    795    }
    796 
    797    #updateOnTabUnsplit() {
    798      if (!this.splitview) {
    799        this.setAttribute("aria-level", 1);
    800        // `posinset` and `setsize` only need to be set explicitly
    801        // on split view tabs so that a11y tools can tell users that a
    802        // given tab is "1 of 2" in the split view, for example.
    803        this.removeAttribute("aria-posinset");
    804        this.removeAttribute("aria-setsize");
    805        this.removeAttribute("aria-label");
    806      }
    807    }
    808 
    809    /**
    810     * Set `aria-label` for this tab to indicate that it's in a Split View,
    811     * along with its position within the Split View.
    812     *
    813     * @param {number} index
    814     *   The index of this tab in the Split View.
    815     */
    816    updateSplitViewAriaLabel(index) {
    817      let l10nId = "";
    818      switch (index) {
    819        case 0:
    820          l10nId = window.RTL_UI
    821            ? "tabbrowser-tab-label-tab-split-view-right"
    822            : "tabbrowser-tab-label-tab-split-view-left";
    823          break;
    824        case 1:
    825          l10nId = window.RTL_UI
    826            ? "tabbrowser-tab-label-tab-split-view-left"
    827            : "tabbrowser-tab-label-tab-split-view-right";
    828          break;
    829      }
    830      if (l10nId) {
    831        const ariaLabel = gBrowser.tabLocalization.formatValueSync(l10nId, {
    832          label: this.getAttribute("label"),
    833        });
    834        this.setAttribute("aria-label", ariaLabel);
    835      }
    836    }
    837  }
    838 
    839  customElements.define("tabbrowser-tab", MozTabbrowserTab, {
    840    extends: "tab",
    841  });
    842 }