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 }