Bug 1884970 - Close current tab button is missing an accessible name and role. r=tabbrowser-reviewers,fluent-reviewers,dao,bolsson,flod

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
This commit is contained in:
Anna Yeddi 2024-08-06 13:51:06 +00:00
parent 19e11b61ea
commit 39d02ec02f
7 changed files with 116 additions and 22 deletions

View File

@ -115,6 +115,11 @@
// xul:text, i.e. the tab label text
role: ROLE_TEXT_LEAF,
children: []
},
{
// xul:toolbarbutton ("Close tab")
role: ROLE_PUSHBUTTON,
children: []
}
]
},
@ -126,6 +131,11 @@
// xul:text, i.e. the tab label text
role: ROLE_TEXT_LEAF,
children: []
},
{
// xul:toolbarbutton ("Close tab")
role: ROLE_PUSHBUTTON,
children: []
}
]
},

View File

@ -37,7 +37,7 @@
<label class="tab-icon-sound-label tab-icon-sound-tooltip-label" role="presentation"/>
</hbox>
</vbox>
<image class="tab-close-button close-icon" role="presentation"/>
<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>
`;

View File

@ -5110,6 +5110,9 @@
this.tabContainer._updateCloseButtons();
this.tabContainer._updateHiddenTabsStatus();
if (aTab.multiselected) {
this._updateMultiselectedTabCloseButtonTooltip();
}
let event = document.createEvent("Events");
event.initEvent("TabShow", true, false);
@ -5133,6 +5136,9 @@
this.tabContainer._updateCloseButtons();
this.tabContainer._updateHiddenTabsStatus();
if (aTab.multiselected) {
this._updateMultiselectedTabCloseButtonTooltip();
}
// Splice this tab out of any lines of succession before any events are
// dispatched.
@ -5499,6 +5505,19 @@
);
},
/**
* Update accessible names of close buttons in the (multi) selected tabs
* collection with how many tabs they will close
*/
_updateMultiselectedTabCloseButtonTooltip() {
const tabCount = gBrowser.selectedTabs.length;
gBrowser.selectedTabs.forEach(selectedTab => {
document.l10n.setArgs(selectedTab.querySelector(".tab-close-button"), {
tabCount,
});
});
},
addToMultiSelectedTabs(aTab) {
if (aTab.multiselected) {
return;
@ -5513,6 +5532,8 @@
} else {
this._multiSelectChangeAdditions.add(aTab);
}
this._updateMultiselectedTabCloseButtonTooltip();
},
/**
@ -5535,6 +5556,8 @@
for (let i = lowerIndex; i <= higherIndex; i++) {
this.addToMultiSelectedTabs(tabs[i]);
}
this._updateMultiselectedTabCloseButtonTooltip();
},
removeFromMultiSelectedTabs(aTab) {
@ -5550,6 +5573,13 @@
} else {
this._multiSelectChangeRemovals.add(aTab);
}
// Update labels for Close buttons of the remaining multiselected tabs:
this._updateMultiselectedTabCloseButtonTooltip();
// Update the label for the Close button of the tab being removed
// from the multiselection:
document.l10n.setArgs(aTab.querySelector(".tab-close-button"), {
tabCount: 1,
});
},
clearMultiSelectedTabs() {
@ -6060,12 +6090,7 @@
const tabCount = this.selectedTabs.includes(tab)
? this.selectedTabs.length
: 1;
if (tab.mOverCloseButton) {
tooltip.label = "";
document.l10n.setAttributes(tooltip, "tabbrowser-close-tabs-tooltip", {
tabCount,
});
} else if (tab._overPlayingIcon) {
if (tab._overPlayingIcon) {
let l10nId;
const l10nArgs = { tabCount };
if (tab.selected) {

View File

@ -14,6 +14,25 @@ async function openTabMenuFor(tab) {
return tabMenu;
}
function checkTabCloseButtonTooltip(
tab,
expectedTabCount = 1 /* not multiselected */
) {
const l10nAttrs = document.l10n.getAttributes(
tab.querySelector(".tab-close-button")
);
Assert.deepEqual(
l10nAttrs,
{
id: "tabbrowser-close-tabs-button",
args: {
tabCount: expectedTabCount,
},
},
`Close tab button has an expected accessible name with ${expectedTabCount} tabs (multi) selected.`
);
}
add_task(async function setPref() {
await SpecialPowers.pushPrefEnv({
set: [[PREF_WARN_ON_CLOSE, false]],
@ -27,37 +46,46 @@ add_task(async function usingTabCloseButton() {
let tab4 = await addTab();
is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
checkTabCloseButtonTooltip(tab1);
await BrowserTestUtils.switchTab(gBrowser, tab1);
await triggerClickOn(tab2, { ctrlKey: true });
ok(tab1.multiselected, "Tab1 is multiselected");
checkTabCloseButtonTooltip(tab1, 2);
ok(tab2.multiselected, "Tab2 is multiselected");
checkTabCloseButtonTooltip(tab2, 2);
ok(!tab3.multiselected, "Tab3 is not multiselected");
checkTabCloseButtonTooltip(tab3);
ok(!tab4.multiselected, "Tab4 is not multiselected");
checkTabCloseButtonTooltip(tab4);
is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
is(gBrowser.selectedTab, tab1, "Tab1 is active");
await triggerClickOn(tab3, { ctrlKey: true });
is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
checkTabCloseButtonTooltip(tab1, 3);
gBrowser.hideTab(tab3);
is(
gBrowser.multiSelectedTabsCount,
2,
"Two multiselected tabs after hiding one tab"
);
checkTabCloseButtonTooltip(tab1, 2);
gBrowser.showTab(tab3);
is(
gBrowser.multiSelectedTabsCount,
3,
"Three multiselected tabs after re-showing hidden tab"
);
checkTabCloseButtonTooltip(tab1, 3);
await triggerClickOn(tab3, { ctrlKey: true });
is(
gBrowser.multiSelectedTabsCount,
2,
"Two multiselected tabs after ctrl-clicking multiselected tab"
);
checkTabCloseButtonTooltip(tab1, 2);
// Closing a tab which is not multiselected
let tab4CloseBtn = tab4.closeButton;

View File

@ -4,10 +4,12 @@ const MOUSE_OFFSET = 7;
// Normal tooltips are positioned vertically at least this amount
const MIN_VERTICAL_TOOLTIP_OFFSET = 18;
function openTooltip(node, tooltip) {
function openTooltip(node) {
let tooltipShownPromise = BrowserTestUtils.waitForEvent(
tooltip,
"popupshown"
document,
"popupshown",
false,
event => event.originalTarget.nodeName == "tooltip"
);
window.windowUtils.disableNonTestMouseEvents(true);
EventUtils.synthesizeMouse(node, 2, 2, { type: "mouseover" });
@ -20,10 +22,12 @@ function openTooltip(node, tooltip) {
return tooltipShownPromise;
}
function closeTooltip(node, tooltip) {
function closeTooltip() {
let tooltipHiddenPromise = BrowserTestUtils.waitForEvent(
tooltip,
"popuphidden"
document,
"popuphidden",
false,
event => event.originalTarget.nodeName == "tooltip"
);
EventUtils.synthesizeMouse(document.documentElement, 2, 2, {
type: "mousemove",
@ -44,8 +48,8 @@ add_task(async function () {
"data:text/html,<html><head><title>A Tab</title></head><body>Hello</body></html>";
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl);
let tooltip = document.getElementById("tabbrowser-tab-tooltip");
await openTooltip(tab, tooltip);
let event = await openTooltip(tab);
let tooltip = event.originalTarget;
let tabRect = tab.getBoundingClientRect();
let tooltipRect = tooltip.getBoundingClientRect();
@ -67,9 +71,10 @@ add_task(async function () {
"tooltip position attribute for tab"
);
await closeTooltip(tab, tooltip);
await closeTooltip();
await openTooltip(tab.closeButton, tooltip);
event = await openTooltip(tab.closeButton);
tooltip = event.originalTarget;
let closeButtonRect = tab.closeButton.getBoundingClientRect();
tooltipRect = tooltip.getBoundingClientRect();
@ -90,7 +95,7 @@ add_task(async function () {
"tooltip position attribute for close button"
);
await closeTooltip(tab, tooltip);
await closeTooltip();
BrowserTestUtils.removeTab(tab);
});
@ -101,8 +106,8 @@ add_task(async function () {
"data:text/html,<html><head><title>A Tab</title></head><body>Hello</body></html>";
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, tabUrl);
let tooltip = document.getElementById("tabbrowser-tab-tooltip");
await openTooltip(tab, tooltip);
let event = await openTooltip(tab);
let tooltip = event.originalTarget;
EventUtils.synthesizeWheel(tab, 4, 4, {
deltaMode: WheelEvent.DOM_DELTA_LINE,

View File

@ -16,10 +16,13 @@ tabbrowser-menuitem-close =
# $containerName (String): the name of the current container.
tabbrowser-container-tab-title = { $title } — { $containerName }
# This text serves as an on-screen tooltip as well as an accessible name for
# the "X" button that is shown on the active tab or, when multiple tabs are
# selected, to all their "X" buttons.
# Variables:
# $tabCount (Number): The number of tabs that will be closed.
tabbrowser-close-tabs-tooltip =
.label =
tabbrowser-close-tabs-button =
.tooltiptext =
{ $tabCount ->
[one] Close tab
*[other] Close { $tabCount } tabs

View File

@ -0,0 +1,23 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
from fluent.migrate.helpers import transforms_from
def migrate(ctx):
"""Bug 1884970 - Close current tab button is missing an accessible name and role, part {index}."""
source = "browser/browser/tabbrowser.ftl"
target = "browser/browser/tabbrowser.ftl"
ctx.add_transforms(
target,
target,
transforms_from(
"""
tabbrowser-close-tabs-button =
.tooltiptext = {COPY_PATTERN(from_path, "tabbrowser-close-tabs-tooltip.label")}
""",
from_path=source,
),
)