commit 8bbc3ddd7487ae988ca18e9279bbb877cf2fba1e
parent 1fed30736a26c73b10d63bb7b88ee55d0205350e
Author: DJ <dj@walker.dev>
Date: Tue, 7 Oct 2025 19:48:51 +0000
Bug 1971240 - improve screen reader support for tab group previews. r=sthompson,fluent-reviewers,tabbrowser-reviewers,bolsson
Differential Revision: https://phabricator.services.mozilla.com/D266366
Diffstat:
5 files changed, 113 insertions(+), 22 deletions(-)
diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml
@@ -537,6 +537,8 @@
</panel>
<panel id="tabgroup-preview-panel"
+ role="menu"
+ data-l10n-id="tab-group-preview-name"
type="arrow"
class="toolbar-menupopup animatable-menupopup"
noautofocus="true"
diff --git a/browser/components/tabbrowser/content/tab-hover-preview.mjs b/browser/components/tabbrowser/content/tab-hover-preview.mjs
@@ -569,6 +569,7 @@ class TabGroupPanel extends Panel {
const fragment = this.win.document.createDocumentFragment();
for (let tab of this.#group.tabs) {
let tabbutton = this.win.document.createXULElement("toolbarbutton");
+ tabbutton.setAttribute("role", "button");
tabbutton.setAttribute("label", tab.label);
tabbutton.setAttribute(
"image",
diff --git a/browser/components/tabbrowser/content/tabgroup.js b/browser/components/tabbrowser/content/tabgroup.js
@@ -121,7 +121,6 @@
});
this.#updateLabelAriaAttributes();
- this.#updateCollapsedAriaAttributes();
this.overflowContainer = this.querySelector(
".tab-group-overflow-count-container"
@@ -332,7 +331,7 @@
}
}
this.toggleAttribute("collapsed", val);
- this.#updateCollapsedAriaAttributes();
+ this.#updateLabelAriaAttributes();
this.#updateTooltip();
for (const tab of this.tabs) {
this.#updateTabAriaHidden(tab);
@@ -367,20 +366,28 @@
async #updateLabelAriaAttributes() {
let tabGroupName = this.#label || this.defaultGroupName;
+ this.#labelElement?.setAttribute("aria-label", tabGroupName);
+ this.#labelElement?.setAttribute("aria-level", 1);
+
+ let tabGroupDescriptionL10nID;
+ if (this.collapsed) {
+ this.#labelElement?.setAttribute("aria-haspopup", "menu");
+ this.#labelElement?.setAttribute("aria-expanded", "false");
+ tabGroupDescriptionL10nID = this.hasAttribute("previewpanelactive")
+ ? "tab-group-preview-open-description"
+ : "tab-group-preview-closed-description";
+ } else {
+ this.#labelElement?.removeAttribute("aria-haspopup");
+ this.#labelElement?.setAttribute("aria-expanded", "true");
+ tabGroupDescriptionL10nID = "tab-group-description";
+ }
let tabGroupDescription = await gBrowser.tabLocalization.formatValue(
- "tab-group-description",
+ tabGroupDescriptionL10nID,
{
tabGroupName,
}
);
- this.#labelElement?.setAttribute("aria-label", tabGroupName);
this.#labelElement?.setAttribute("aria-description", tabGroupDescription);
- this.#labelElement?.setAttribute("aria-level", 1);
- }
-
- #updateCollapsedAriaAttributes() {
- const ariaExpanded = this.collapsed ? "false" : "true";
- this.#labelElement?.setAttribute("aria-expanded", ariaExpanded);
}
async #updateTooltip() {
@@ -486,6 +493,7 @@
*/
set hoverPreviewPanelActive(val) {
this.toggleAttribute("previewpanelactive", val);
+ this.#updateLabelAriaAttributes();
}
/**
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_a11y.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_groups_a11y.js
@@ -46,23 +46,25 @@ add_task(async function test_TabGroupA11y() {
);
tabGroup.label = "test";
- tabGroup.collapsed = true;
await BrowserTestUtils.waitForCondition(
() => tabGroup.labelElement.getAttribute("aria-label") == "test",
"Tab group label was updated"
);
+ await flushL10n();
+
Assert.equal(
tabGroup.labelElement.getAttribute("aria-label"),
"test",
"tab group label aria-label should equal the name of the tab group"
);
-
Assert.equal(
tabGroup.labelElement.getAttribute("aria-description"),
"test — Tab Group",
"tab group label aria-description should provide the name of the tab group plus more context"
);
+ await TabGroupTestUtils.toggleCollapsed(tabGroup, true);
+
Assert.equal(
tabGroup.labelElement.getAttribute("aria-expanded"),
"false",
@@ -165,12 +167,7 @@ add_task(async function test_collapsedTabGroupTooltips() {
info(
"Collapse the group to confirm tooltip state when tab group hover preview is enabled"
);
- let collapseFinished = BrowserTestUtils.waitForEvent(
- group,
- "TabGroupCollapse"
- );
- group.collapsed = true;
- await collapseFinished;
+ await TabGroupTestUtils.toggleCollapsed(group, true);
await flushL10n();
Assert.equal(
@@ -192,10 +189,8 @@ add_task(async function test_collapsedTabGroupTooltips() {
info(
"Un-collapse and re-collapse group to ensure group picks up new pref setting"
);
- group.collapsed = false;
- collapseFinished = BrowserTestUtils.waitForEvent(group, "TabGroupCollapse");
- group.collapsed = true;
- await collapseFinished;
+ await TabGroupTestUtils.toggleCollapsed(group, false);
+ await TabGroupTestUtils.toggleCollapsed(group, true);
await flushL10n();
Assert.equal(
@@ -227,3 +222,78 @@ add_task(async function test_collapsedTabGroupTooltips() {
await removeTabGroup(group);
});
+
+add_task(async function test_tabGroupPreviewA11y() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.groups.hoverPreview.enabled", true]],
+ });
+
+ const tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
+ const group = gBrowser.addTabGroup([tab]);
+
+ Assert.ok(
+ !group.labelElement.getAttribute("aria-haspopup"),
+ "Group label does not have aria-haspopup attribute"
+ );
+
+ await TabGroupTestUtils.toggleCollapsed(group, true);
+
+ Assert.equal(
+ group.labelElement.getAttribute("aria-haspopup"),
+ "menu",
+ 'Group label has aria-haspopup="menu"'
+ );
+
+ info("opening collapsed group preview");
+ let groupPreview = document.getElementById("tabgroup-preview-panel");
+ let groupPreviewShown = BrowserTestUtils.waitForPopupEvent(
+ groupPreview,
+ "shown"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ group.labelElement,
+ {
+ type: "mouseover",
+ },
+ window
+ );
+ await groupPreviewShown;
+ await flushL10n();
+
+ Assert.equal(
+ groupPreview.getAttribute("aria-label"),
+ "Tabs in a collapsed group",
+ "Group preview panel has correct label"
+ );
+ Assert.equal(
+ group.labelElement.getAttribute("aria-description"),
+ "Tabs list open",
+ "Group label has a description indicating the preview is open"
+ );
+ Assert.equal(
+ groupPreview.querySelector("toolbarbutton").getAttribute("role"),
+ "button",
+ "Group menu items have correct role"
+ );
+
+ info("dismissing group preview");
+ let groupPreviewHidden = BrowserTestUtils.waitForPopupEvent(
+ groupPreview,
+ "hidden"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ document.body,
+ { type: "mouseover" },
+ window
+ );
+ await groupPreviewHidden;
+
+ Assert.equal(
+ group.labelElement.getAttribute("aria-description"),
+ "Tabs list closed",
+ "Group label has a description indicating the preview is closed"
+ );
+
+ await TabGroupTestUtils.removeTabGroup(group);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/locales/en-US/browser/tabbrowser.ftl b/browser/locales/en-US/browser/tabbrowser.ftl
@@ -260,6 +260,16 @@ tab-group-editor-color-selector2-red = Red
tab-group-description = { $tabGroupName } — Tab Group
tab-group-label-tooltip-collapsed = { $tabGroupName } — Collapsed
tab-group-label-tooltip-expanded = { $tabGroupName } — Expanded
+tab-group-preview-name =
+ .aria-label = Tabs in a collapsed group
+
+## When collapsed, the tab group label's aria-description will indicate
+## whether the hover menu is open or closed.
+
+tab-group-preview-open-description = Tabs list open
+tab-group-preview-closed-description = Tabs list closed
+
+##
tab-context-unnamed-group =
.label = Unnamed group