mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-05 10:48:15 +02:00
The [tab-close-button](https://searchfox.org/mozilla-central/rev/f9157a03835653cd3ece8d2dc713a782b7e4374e/browser/base/content/tabbrowser-tab.js#40) is not labeled and is missing an interactive role of button, while it is functioning as one. Note: we do not want this control to be keyboard focusable, because keyboard-only user could close the tab via the context menu and we don't want to create an additional tab stop for the navigation as well, but making sure the control is marked up as a button with an accessible name would allow it to be actionable with speech-to-text software, with touch devices, with switch controls in scan mode, and for screen readers via their navigation shortcuts as well. Differential Revision: https://phabricator.services.mozilla.com/D204413
742 lines
23 KiB
JavaScript
742 lines
23 KiB
JavaScript
/* 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 chrome windows with the subscript loader. Wrap in
|
|
// a block to prevent accidentally leaking globals onto `window`.
|
|
{
|
|
class MozTabbrowserTab extends MozElements.MozTab {
|
|
static markup = `
|
|
<stack class="tab-stack" flex="1">
|
|
<vbox class="tab-background">
|
|
<hbox class="tab-context-line"/>
|
|
<hbox class="tab-loading-burst" flex="1"/>
|
|
</vbox>
|
|
<hbox class="tab-content" align="center">
|
|
<stack class="tab-icon-stack">
|
|
<hbox class="tab-throbber"/>
|
|
<hbox class="tab-icon-pending"/>
|
|
<html:img class="tab-icon-image" role="presentation" decoding="sync" />
|
|
<image class="tab-sharing-icon-overlay" role="presentation"/>
|
|
<image class="tab-icon-overlay" role="presentation"/>
|
|
</stack>
|
|
<vbox class="tab-label-container"
|
|
onoverflow="this.setAttribute('textoverflow', 'true');"
|
|
onunderflow="this.removeAttribute('textoverflow');"
|
|
align="start"
|
|
pack="center"
|
|
flex="1">
|
|
<label class="tab-text tab-label" role="presentation"/>
|
|
<hbox class="tab-secondary-label">
|
|
<label class="tab-icon-sound-label tab-icon-sound-playing-label" data-l10n-id="browser-tab-audio-playing2" role="presentation"/>
|
|
<label class="tab-icon-sound-label tab-icon-sound-muted-label" data-l10n-id="browser-tab-audio-muted2" role="presentation"/>
|
|
<label class="tab-icon-sound-label tab-icon-sound-blocked-label" data-l10n-id="browser-tab-audio-blocked" role="presentation"/>
|
|
<label class="tab-icon-sound-label tab-icon-sound-pip-label" data-l10n-id="browser-tab-audio-pip" role="presentation"/>
|
|
<label class="tab-icon-sound-label tab-icon-sound-tooltip-label" role="presentation"/>
|
|
</hbox>
|
|
</vbox>
|
|
<image class="tab-close-button close-icon" role="button" data-l10n-id="tabbrowser-close-tabs-button" data-l10n-args='{"tabCount": 1}' keyNav="false"/>
|
|
</hbox>
|
|
</stack>
|
|
`;
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.addEventListener("mouseover", this);
|
|
this.addEventListener("mouseout", this);
|
|
this.addEventListener("dragstart", this, true);
|
|
this.addEventListener("dragstart", this);
|
|
this.addEventListener("mousedown", this);
|
|
this.addEventListener("mouseup", this);
|
|
this.addEventListener("click", this);
|
|
this.addEventListener("dblclick", this, true);
|
|
this.addEventListener("animationend", this);
|
|
this.addEventListener("focus", this);
|
|
this.addEventListener("AriaFocus", this);
|
|
|
|
this._hover = false;
|
|
this._selectedOnFirstMouseDown = false;
|
|
|
|
/**
|
|
* Describes how the tab ended up in this mute state. May be any of:
|
|
*
|
|
* - undefined: The tabs mute state has never changed.
|
|
* - null: The mute state was last changed through the UI.
|
|
* - Any string: The ID was changed through an extension API. The string
|
|
* must be the ID of the extension which changed it.
|
|
*/
|
|
this.muteReason = undefined;
|
|
|
|
this.mOverCloseButton = false;
|
|
|
|
this.mCorrespondingMenuitem = null;
|
|
|
|
this.closing = false;
|
|
}
|
|
|
|
static get inheritedAttributes() {
|
|
return {
|
|
".tab-background": "selected=visuallyselected,fadein,multiselected",
|
|
".tab-line": "selected=visuallyselected,multiselected",
|
|
".tab-loading-burst": "pinned,bursting,notselectedsinceload",
|
|
".tab-content":
|
|
"pinned,selected=visuallyselected,titlechanged,attention",
|
|
".tab-icon-stack":
|
|
"sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,indicator-replaces-favicon",
|
|
".tab-throbber":
|
|
"fadein,pinned,busy,progress,selected=visuallyselected",
|
|
".tab-icon-pending":
|
|
"fadein,pinned,busy,progress,selected=visuallyselected,pendingicon",
|
|
".tab-icon-image":
|
|
"src=image,triggeringprincipal=iconloadingprincipal,requestcontextid,fadein,pinned,selected=visuallyselected,busy,crashed,sharing,pictureinpicture",
|
|
".tab-sharing-icon-overlay": "sharing,selected=visuallyselected,pinned",
|
|
".tab-icon-overlay":
|
|
"sharing,pictureinpicture,crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,indicator-replaces-favicon",
|
|
".tab-label-container":
|
|
"pinned,selected=visuallyselected,labeldirection",
|
|
".tab-label":
|
|
"text=label,accesskey,fadein,pinned,selected=visuallyselected,attention",
|
|
".tab-label-container .tab-secondary-label":
|
|
"soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked,pictureinpicture",
|
|
".tab-close-button": "fadein,pinned,selected=visuallyselected",
|
|
};
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.initialize();
|
|
}
|
|
|
|
initialize() {
|
|
if (this._initialized) {
|
|
return;
|
|
}
|
|
|
|
this.textContent = "";
|
|
this.appendChild(this.constructor.fragment);
|
|
this.initializeAttributeInheritance();
|
|
this.setAttribute("context", "tabContextMenu");
|
|
this._initialized = true;
|
|
|
|
if (!("_lastAccessed" in this)) {
|
|
this.updateLastAccessed();
|
|
}
|
|
}
|
|
|
|
get owner() {
|
|
let owner = this._owner?.deref();
|
|
if (owner && !owner.closing) {
|
|
return owner;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
set owner(owner) {
|
|
if (owner) {
|
|
this._owner = new WeakRef(owner);
|
|
} else {
|
|
this._owner = null;
|
|
}
|
|
}
|
|
|
|
get container() {
|
|
return gBrowser.tabContainer;
|
|
}
|
|
|
|
set attention(val) {
|
|
if (val == this.hasAttribute("attention")) {
|
|
return;
|
|
}
|
|
|
|
this.toggleAttribute("attention", val);
|
|
gBrowser._tabAttrModified(this, ["attention"]);
|
|
}
|
|
|
|
set undiscardable(val) {
|
|
if (val == this.hasAttribute("undiscardable")) {
|
|
return;
|
|
}
|
|
|
|
this.toggleAttribute("undiscardable", val);
|
|
gBrowser._tabAttrModified(this, ["undiscardable"]);
|
|
}
|
|
|
|
set _visuallySelected(val) {
|
|
if (val == this.hasAttribute("visuallyselected")) {
|
|
return;
|
|
}
|
|
|
|
this.toggleAttribute("visuallyselected", val);
|
|
gBrowser._tabAttrModified(this, ["visuallyselected"]);
|
|
}
|
|
|
|
set _selected(val) {
|
|
// in e10s we want to only pseudo-select a tab before its rendering is done, so that
|
|
// the rest of the system knows that the tab is selected, but we don't want to update its
|
|
// visual status to selected until after we receive confirmation that its content has painted.
|
|
if (val) {
|
|
this.setAttribute("selected", "true");
|
|
} else {
|
|
this.removeAttribute("selected");
|
|
}
|
|
|
|
// If we're non-e10s we need to update the visual selection at the same
|
|
// time, otherwise AsyncTabSwitcher will take care of this.
|
|
if (!gMultiProcessBrowser) {
|
|
this._visuallySelected = val;
|
|
}
|
|
}
|
|
|
|
get pinned() {
|
|
return this.hasAttribute("pinned");
|
|
}
|
|
|
|
get hidden() {
|
|
// This getter makes `hidden` read-only
|
|
return super.hidden;
|
|
}
|
|
|
|
get muted() {
|
|
return this.hasAttribute("muted");
|
|
}
|
|
|
|
get multiselected() {
|
|
return this.hasAttribute("multiselected");
|
|
}
|
|
|
|
get userContextId() {
|
|
return this.hasAttribute("usercontextid")
|
|
? parseInt(this.getAttribute("usercontextid"))
|
|
: 0;
|
|
}
|
|
|
|
get soundPlaying() {
|
|
return this.hasAttribute("soundplaying");
|
|
}
|
|
|
|
get pictureinpicture() {
|
|
return this.hasAttribute("pictureinpicture");
|
|
}
|
|
|
|
get activeMediaBlocked() {
|
|
return this.hasAttribute("activemedia-blocked");
|
|
}
|
|
|
|
get undiscardable() {
|
|
return this.hasAttribute("undiscardable");
|
|
}
|
|
|
|
get isEmpty() {
|
|
// Determines if a tab is "empty", usually used in the context of determining
|
|
// if it's ok to close the tab.
|
|
if (this.hasAttribute("busy")) {
|
|
return false;
|
|
}
|
|
|
|
if (this.hasAttribute("customizemode")) {
|
|
return false;
|
|
}
|
|
|
|
let browser = this.linkedBrowser;
|
|
if (!isBlankPageURL(browser.currentURI.spec)) {
|
|
return false;
|
|
}
|
|
|
|
if (!BrowserUIUtils.checkEmptyPageOrigin(browser)) {
|
|
return false;
|
|
}
|
|
|
|
if (browser.canGoForward || browser.canGoBack) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
get lastAccessed() {
|
|
return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
|
|
}
|
|
|
|
/**
|
|
* Returns a timestamp which attempts to represent the last time the user saw this tab.
|
|
* If the tab has not been active in this session, any lastAccessed is used. We
|
|
* differentiate between selected and explicitly visible; a selected tab in a hidden
|
|
* window is last seen when that window and tab were last visible.
|
|
* We use the application start time as a fallback value when no other suitable value
|
|
* is available.
|
|
*/
|
|
get lastSeenActive() {
|
|
const isForegroundWindow =
|
|
this.ownerGlobal ==
|
|
BrowserWindowTracker.getTopWindow({ allowPopups: true });
|
|
// the timestamp for the selected tab in the active window is always now
|
|
if (isForegroundWindow && this.selected) {
|
|
return Date.now();
|
|
}
|
|
if (this._lastSeenActive) {
|
|
return this._lastSeenActive;
|
|
}
|
|
|
|
const appStartTime = Services.startup.getStartupInfo().start.getTime();
|
|
if (!this._lastAccessed || this._lastAccessed >= appStartTime) {
|
|
// When the tab was created this session but hasn't been seen by the user,
|
|
// default to the application start time.
|
|
return appStartTime;
|
|
}
|
|
// The tab was restored from a previous session but never seen.
|
|
// Use the lastAccessed as the best proxy for when the user might have seen it.
|
|
return this._lastAccessed;
|
|
}
|
|
|
|
get _overPlayingIcon() {
|
|
return this.overlayIcon?.matches(":hover");
|
|
}
|
|
|
|
get overlayIcon() {
|
|
return this.querySelector(".tab-icon-overlay");
|
|
}
|
|
|
|
get throbber() {
|
|
return this.querySelector(".tab-throbber");
|
|
}
|
|
|
|
get iconImage() {
|
|
return this.querySelector(".tab-icon-image");
|
|
}
|
|
|
|
get sharingIcon() {
|
|
return this.querySelector(".tab-sharing-icon-overlay");
|
|
}
|
|
|
|
get textLabel() {
|
|
return this.querySelector(".tab-label");
|
|
}
|
|
|
|
get closeButton() {
|
|
return this.querySelector(".tab-close-button");
|
|
}
|
|
|
|
updateLastAccessed(aDate) {
|
|
this._lastAccessed = this.selected ? Infinity : aDate || Date.now();
|
|
}
|
|
|
|
updateLastSeenActive() {
|
|
this._lastSeenActive = Date.now();
|
|
}
|
|
|
|
updateLastUnloadedByTabUnloader() {
|
|
this._lastUnloaded = Date.now();
|
|
Services.telemetry.scalarAdd("browser.engagement.tab_unload_count", 1);
|
|
}
|
|
|
|
recordTimeFromUnloadToReload() {
|
|
if (!this._lastUnloaded) {
|
|
return;
|
|
}
|
|
|
|
const diff_in_msec = Date.now() - this._lastUnloaded;
|
|
Services.telemetry
|
|
.getHistogramById("TAB_UNLOAD_TO_RELOAD")
|
|
.add(diff_in_msec / 1000);
|
|
Services.telemetry.scalarAdd("browser.engagement.tab_reload_count", 1);
|
|
delete this._lastUnloaded;
|
|
}
|
|
|
|
on_mouseover(event) {
|
|
if (event.target.classList.contains("tab-close-button")) {
|
|
this.mOverCloseButton = true;
|
|
}
|
|
if (this._overPlayingIcon) {
|
|
const selectedTabs = gBrowser.selectedTabs;
|
|
const contextTabInSelection = selectedTabs.includes(this);
|
|
const affectedTabsLength = contextTabInSelection
|
|
? selectedTabs.length
|
|
: 1;
|
|
let stringID;
|
|
if (this.hasAttribute("activemedia-blocked")) {
|
|
stringID = "browser-tab-unblock";
|
|
} else {
|
|
stringID = this.linkedBrowser.audioMuted
|
|
? "browser-tab-unmute"
|
|
: "browser-tab-mute";
|
|
}
|
|
this.setSecondaryTabTooltipLabel(stringID, {
|
|
count: affectedTabsLength,
|
|
});
|
|
}
|
|
|
|
if (this.hidden || this.closing) {
|
|
return;
|
|
}
|
|
|
|
let tabToWarm = this.mOverCloseButton
|
|
? gBrowser._findTabToBlurTo(this)
|
|
: this;
|
|
gBrowser.warmupTab(tabToWarm);
|
|
|
|
// If the previous target wasn't part of this tab then this is a mouseenter event.
|
|
if (!this.contains(event.relatedTarget)) {
|
|
this._mouseenter();
|
|
}
|
|
}
|
|
|
|
on_mouseout(event) {
|
|
if (event.target.classList.contains("tab-close-button")) {
|
|
this.mOverCloseButton = false;
|
|
}
|
|
if (event.target == this.overlayIcon) {
|
|
this.setSecondaryTabTooltipLabel(null);
|
|
}
|
|
|
|
// If the new target is not part of this tab then this is a mouseleave event.
|
|
if (!this.contains(event.relatedTarget)) {
|
|
this._mouseleave();
|
|
}
|
|
}
|
|
|
|
on_dragstart(event) {
|
|
// We use "failed" drag end events that weren't cancelled by the user
|
|
// to detach tabs. Ensure that we do not show the drag image returning
|
|
// to its point of origin when this happens, as it makes the drag
|
|
// finishing feel very slow.
|
|
event.dataTransfer.mozShowFailAnimation = false;
|
|
if (event.eventPhase == Event.CAPTURING_PHASE) {
|
|
this.style.MozUserFocus = "";
|
|
} else if (
|
|
this.mOverCloseButton ||
|
|
gSharedTabWarning.willShowSharedTabWarning(this)
|
|
) {
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
|
|
on_mousedown(event) {
|
|
let eventMaySelectTab = true;
|
|
let tabContainer = this.container;
|
|
|
|
if (
|
|
tabContainer._closeTabByDblclick &&
|
|
event.button == 0 &&
|
|
event.detail == 1
|
|
) {
|
|
this._selectedOnFirstMouseDown = this.selected;
|
|
}
|
|
|
|
if (this.selected) {
|
|
this.style.MozUserFocus = "ignore";
|
|
} else if (
|
|
event.target.classList.contains("tab-close-button") ||
|
|
event.target.classList.contains("tab-icon-overlay")
|
|
) {
|
|
eventMaySelectTab = false;
|
|
}
|
|
|
|
if (event.button == 1) {
|
|
gBrowser.warmupTab(gBrowser._findTabToBlurTo(this));
|
|
}
|
|
|
|
if (event.button == 0) {
|
|
let shiftKey = event.shiftKey;
|
|
let accelKey = event.getModifierState("Accel");
|
|
if (shiftKey) {
|
|
eventMaySelectTab = false;
|
|
const lastSelectedTab = gBrowser.lastMultiSelectedTab;
|
|
if (!accelKey) {
|
|
gBrowser.selectedTab = lastSelectedTab;
|
|
|
|
// Make sure selection is cleared when tab-switch doesn't happen.
|
|
gBrowser.clearMultiSelectedTabs();
|
|
}
|
|
gBrowser.addRangeToMultiSelectedTabs(lastSelectedTab, this);
|
|
} else if (accelKey) {
|
|
// Ctrl (Cmd for mac) key is pressed
|
|
eventMaySelectTab = false;
|
|
if (this.multiselected) {
|
|
gBrowser.removeFromMultiSelectedTabs(this);
|
|
} else if (this != gBrowser.selectedTab) {
|
|
gBrowser.addToMultiSelectedTabs(this);
|
|
gBrowser.lastMultiSelectedTab = this;
|
|
}
|
|
} else if (!this.selected && this.multiselected) {
|
|
gBrowser.lockClearMultiSelectionOnce();
|
|
}
|
|
}
|
|
|
|
if (gSharedTabWarning.willShowSharedTabWarning(this)) {
|
|
eventMaySelectTab = false;
|
|
}
|
|
|
|
if (eventMaySelectTab) {
|
|
super.on_mousedown(event);
|
|
}
|
|
}
|
|
|
|
on_mouseup() {
|
|
// Make sure that clear-selection is released.
|
|
// Otherwise selection using Shift key may be broken.
|
|
gBrowser.unlockClearMultiSelection();
|
|
|
|
this.style.MozUserFocus = "";
|
|
}
|
|
|
|
on_click(event) {
|
|
if (event.button != 0) {
|
|
return;
|
|
}
|
|
|
|
if (event.getModifierState("Accel") || event.shiftKey) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
gBrowser.multiSelectedTabsCount > 0 &&
|
|
!event.target.classList.contains("tab-close-button") &&
|
|
!event.target.classList.contains("tab-icon-overlay")
|
|
) {
|
|
// Tabs were previously multi-selected and user clicks on a tab
|
|
// without holding Ctrl/Cmd Key
|
|
gBrowser.clearMultiSelectedTabs();
|
|
}
|
|
|
|
if (event.target.classList.contains("tab-icon-overlay")) {
|
|
if (this.activeMediaBlocked) {
|
|
if (this.multiselected) {
|
|
gBrowser.resumeDelayedMediaOnMultiSelectedTabs(this);
|
|
} else {
|
|
this.resumeDelayedMedia();
|
|
}
|
|
} else if (this.soundPlaying || this.muted) {
|
|
if (this.multiselected) {
|
|
gBrowser.toggleMuteAudioOnMultiSelectedTabs(this);
|
|
} else {
|
|
this.toggleMuteAudio();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event.target.classList.contains("tab-close-button")) {
|
|
if (this.multiselected) {
|
|
gBrowser.removeMultiSelectedTabs();
|
|
} else {
|
|
gBrowser.removeTab(this, {
|
|
animate: true,
|
|
triggeringEvent: event,
|
|
});
|
|
}
|
|
// This enables double-click protection for the tab container
|
|
// (see tabbrowser-tabs 'click' handler).
|
|
gBrowser.tabContainer._blockDblClick = true;
|
|
}
|
|
}
|
|
|
|
on_dblclick(event) {
|
|
if (event.button != 0) {
|
|
return;
|
|
}
|
|
|
|
// for the one-close-button case
|
|
if (event.target.classList.contains("tab-close-button")) {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
let tabContainer = this.container;
|
|
if (
|
|
tabContainer._closeTabByDblclick &&
|
|
this._selectedOnFirstMouseDown &&
|
|
this.selected &&
|
|
!event.target.classList.contains("tab-icon-overlay")
|
|
) {
|
|
gBrowser.removeTab(this, {
|
|
animate: true,
|
|
triggeringEvent: event,
|
|
});
|
|
}
|
|
}
|
|
|
|
on_animationend(event) {
|
|
if (event.target.classList.contains("tab-loading-burst")) {
|
|
this.removeAttribute("bursting");
|
|
}
|
|
}
|
|
|
|
_mouseenter() {
|
|
this._hover = true;
|
|
|
|
if (this.selected) {
|
|
this.container._handleTabSelect();
|
|
} else if (this.linkedPanel) {
|
|
this.linkedBrowser.unselectedTabHover(true);
|
|
this.startUnselectedTabHoverTimer();
|
|
}
|
|
|
|
// Prepare connection to host beforehand.
|
|
SessionStore.speculativeConnectOnTabHover(this);
|
|
|
|
const isForegroundWindow =
|
|
this.ownerGlobal ==
|
|
BrowserWindowTracker.getTopWindow({ allowPopups: true });
|
|
if (isForegroundWindow) {
|
|
this.dispatchEvent(new CustomEvent("TabHoverStart", { bubbles: true }));
|
|
}
|
|
}
|
|
|
|
_mouseleave() {
|
|
if (!this._hover) {
|
|
return;
|
|
}
|
|
this._hover = false;
|
|
if (this.linkedPanel && !this.selected) {
|
|
this.linkedBrowser.unselectedTabHover(false);
|
|
this.cancelUnselectedTabHoverTimer();
|
|
}
|
|
this.dispatchEvent(new CustomEvent("TabHoverEnd", { bubbles: true }));
|
|
}
|
|
|
|
setSecondaryTabTooltipLabel(l10nID, l10nArgs) {
|
|
this.querySelector(".tab-secondary-label").toggleAttribute(
|
|
"showtooltip",
|
|
l10nID
|
|
);
|
|
|
|
const tooltipEl = this.querySelector(".tab-icon-sound-tooltip-label");
|
|
|
|
if (l10nArgs) {
|
|
tooltipEl.setAttribute("data-l10n-args", JSON.stringify(l10nArgs));
|
|
} else {
|
|
tooltipEl.removeAttribute("data-l10n-args");
|
|
}
|
|
if (l10nID) {
|
|
tooltipEl.setAttribute("data-l10n-id", l10nID);
|
|
} else {
|
|
tooltipEl.removeAttribute("data-l10n-id");
|
|
}
|
|
// TODO(Itiel): Maybe simplify this when bug 1830989 lands
|
|
}
|
|
|
|
startUnselectedTabHoverTimer() {
|
|
// Only record data when we need to.
|
|
if (!this.linkedBrowser.shouldHandleUnselectedTabHover) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
|
|
) {
|
|
TelemetryStopwatch.start("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
|
|
}
|
|
|
|
if (this._hoverTabTimer) {
|
|
clearTimeout(this._hoverTabTimer);
|
|
this._hoverTabTimer = null;
|
|
}
|
|
}
|
|
|
|
cancelUnselectedTabHoverTimer() {
|
|
// Since we're listening "mouseout" event, instead of "mouseleave".
|
|
// Every time the cursor is moving from the tab to its child node (icon),
|
|
// it would dispatch "mouseout"(for tab) first and then dispatch
|
|
// "mouseover" (for icon, eg: close button, speaker icon) soon.
|
|
// It causes we would cancel present TelemetryStopwatch immediately
|
|
// when cursor is moving on the icon, and then start a new one.
|
|
// In order to avoid this situation, we could delay cancellation and
|
|
// remove it if we get "mouseover" within very short period.
|
|
this._hoverTabTimer = setTimeout(() => {
|
|
if (
|
|
TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
|
|
) {
|
|
TelemetryStopwatch.cancel("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
finishUnselectedTabHoverTimer() {
|
|
// Stop timer when the tab is opened.
|
|
if (
|
|
TelemetryStopwatch.running("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this)
|
|
) {
|
|
TelemetryStopwatch.finish("HOVER_UNTIL_UNSELECTED_TAB_OPENED", this);
|
|
}
|
|
}
|
|
|
|
resumeDelayedMedia() {
|
|
if (this.activeMediaBlocked) {
|
|
Services.telemetry
|
|
.getHistogramById("TAB_AUDIO_INDICATOR_USED")
|
|
.add(3 /* unblockByClickingIcon */);
|
|
this.removeAttribute("activemedia-blocked");
|
|
this.linkedBrowser.resumeMedia();
|
|
gBrowser._tabAttrModified(this, ["activemedia-blocked"]);
|
|
}
|
|
}
|
|
|
|
toggleMuteAudio(aMuteReason) {
|
|
let browser = this.linkedBrowser;
|
|
let hist = Services.telemetry.getHistogramById(
|
|
"TAB_AUDIO_INDICATOR_USED"
|
|
);
|
|
|
|
if (browser.audioMuted) {
|
|
if (this.linkedPanel) {
|
|
// "Lazy Browser" should not invoke its unmute method
|
|
browser.unmute();
|
|
}
|
|
this.removeAttribute("muted");
|
|
hist.add(1 /* unmute */);
|
|
} else {
|
|
if (this.linkedPanel) {
|
|
// "Lazy Browser" should not invoke its mute method
|
|
browser.mute();
|
|
}
|
|
this.toggleAttribute("muted", true);
|
|
hist.add(0 /* mute */);
|
|
}
|
|
this.muteReason = aMuteReason || null;
|
|
|
|
gBrowser._tabAttrModified(this, ["muted"]);
|
|
}
|
|
|
|
setUserContextId(aUserContextId) {
|
|
if (aUserContextId) {
|
|
if (this.linkedBrowser) {
|
|
this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
|
|
}
|
|
this.setAttribute("usercontextid", aUserContextId);
|
|
} else {
|
|
if (this.linkedBrowser) {
|
|
this.linkedBrowser.removeAttribute("usercontextid");
|
|
}
|
|
this.removeAttribute("usercontextid");
|
|
}
|
|
|
|
ContextualIdentityService.setTabStyle(this);
|
|
}
|
|
|
|
updateA11yDescription() {
|
|
let prevDescTab = gBrowser.tabContainer.querySelector(
|
|
"tab[aria-describedby]"
|
|
);
|
|
if (prevDescTab) {
|
|
// We can only have a description for the focused tab.
|
|
prevDescTab.removeAttribute("aria-describedby");
|
|
}
|
|
let desc = document.getElementById("tabbrowser-tab-a11y-desc");
|
|
desc.textContent = gBrowser.getTabTooltip(this, false);
|
|
this.setAttribute("aria-describedby", "tabbrowser-tab-a11y-desc");
|
|
}
|
|
|
|
on_focus() {
|
|
this.updateA11yDescription();
|
|
}
|
|
|
|
on_AriaFocus() {
|
|
this.updateA11yDescription();
|
|
}
|
|
}
|
|
|
|
customElements.define("tabbrowser-tab", MozTabbrowserTab, {
|
|
extends: "tab",
|
|
});
|
|
}
|