commit 66085b85b371d0cf577bae9e0e433fbe999f3a8f
parent 506947fb1341f505cb43535414a5f2fcdcc51b71
Author: James Teh <jteh@mozilla.com>
Date: Thu, 16 Oct 2025 00:43:32 +0000
Bug 1994356 part 1: Don't implicitly treat a tab as selected just because it is focused. r=morgan
Instead, for focused tabs, we should apply the same rules we use for options and treeitems.
Note that in browser_caching_states.js, I had to change one of the implicit selected state tests so that nothing in the container is explicitly selected in order to trigger implicit selection with these changed rules.
We have a test elsewhere in states/browser_test_selection.js which verifies that explicit selection takes precedence over implicit selection.
Differential Revision: https://phabricator.services.mozilla.com/D268660
Diffstat:
3 files changed, 24 insertions(+), 26 deletions(-)
diff --git a/accessible/basetypes/Accessible.cpp b/accessible/basetypes/Accessible.cpp
@@ -685,20 +685,15 @@ void Accessible::ApplyImplicitState(uint64_t& aState) const {
roleMapEntry->Is(nsGkAtoms::tab) ||
roleMapEntry->Is(nsGkAtoms::treeitem)) &&
!(aState & states::SELECTED) && ARIASelected().valueOr(true)) {
- // Special case for tabs: focused tab or focus inside related tab panel
- // implies selected state.
- if (roleMapEntry->role == roles::PAGETAB) {
- if (aState & states::FOCUSED) {
- aState |= states::SELECTED;
- } else {
- // If focus is in a child of the tab panel surely the tab is selected!
- Relation rel = RelationByType(RelationType::LABEL_FOR);
- Accessible* relTarget = nullptr;
- while ((relTarget = rel.Next())) {
- if (relTarget->Role() == roles::PROPERTYPAGE &&
- FocusMgr()->IsFocusWithin(relTarget)) {
- aState |= states::SELECTED;
- }
+ if (roleMapEntry->role == roles::PAGETAB && !(aState & states::FOCUSED)) {
+ // If focus is within the tab panel, this should mean the tab is selected.
+ // Note that we handle focus on the tab itself below.
+ Relation rel = RelationByType(RelationType::LABEL_FOR);
+ Accessible* relTarget = nullptr;
+ while ((relTarget = rel.Next())) {
+ if (relTarget->Role() == roles::PROPERTYPAGE &&
+ FocusMgr()->IsFocusWithin(relTarget)) {
+ aState |= states::SELECTED;
}
}
} else if (aState & states::FOCUSED) {
diff --git a/accessible/tests/browser/e10s/browser_caching_states.js b/accessible/tests/browser/e10s/browser_caching_states.js
@@ -440,7 +440,7 @@ addAccessibleTask(
`
<div role="tablist">
<div id="noSel" role="tab" tabindex="0">noSel</div>
- <div id="selFalse" role="tab" aria-selected="false" tabindex="0">selFalse</div>
+ <div id="noSel2" role="tab" tabindex="0">noSel2</div>
</div>
<div role="listbox" aria-multiselectable="true">
<div id="multiNoSel" role="option" tabindex="0">multiNoSel</div>
@@ -460,13 +460,13 @@ addAccessibleTask(
await focused;
testStates(noSel, STATE_FOCUSED | STATE_SELECTED, 0, 0, 0);
- const selFalse = findAccessibleChildByID(docAcc, "selFalse");
- testStates(selFalse, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
- info("Focusing selFalse");
- focused = waitForEvent(EVENT_FOCUS, selFalse);
- selFalse.takeFocus();
+ const noSel2 = findAccessibleChildByID(docAcc, "noSel2");
+ testStates(noSel2, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
+ info("Focusing noSel2");
+ focused = waitForEvent(EVENT_FOCUS, noSel2);
+ noSel2.takeFocus();
await focused;
- testStates(selFalse, STATE_FOCUSED, 0, STATE_SELECTED, 0);
+ testStates(noSel2, STATE_FOCUSED | STATE_SELECTED, 0, 0, 0);
const multiNoSel = findAccessibleChildByID(docAcc, "multiNoSel");
testStates(multiNoSel, 0, 0, STATE_FOCUSED | STATE_SELECTED, 0);
diff --git a/accessible/tests/mochitest/states/test_aria_widgetitems.html b/accessible/tests/mochitest/states/test_aria_widgetitems.html
@@ -79,10 +79,11 @@
testStates("aria_treeitem2", STATE_SELECTED);
testStates("aria_treeitem3", 0, 0, STATE_SELECTED);
- // selected state when widget item is focused
+ // Test the selected state when a widget item is DOM focused if
+ // aria-selected is explicitly set on at least one of the items.
gQueue = new eventQueue(EVENT_FOCUS);
- gQueue.push(new focusARIAItem("aria_tab1", true));
+ gQueue.push(new focusARIAItem("aria_tab1", false));
gQueue.push(new focusARIAItem("aria_tab2", true));
gQueue.push(new focusARIAItem("aria_tab3", false));
gQueue.push(new focusARIAItem("aria_option1", false));
@@ -92,9 +93,11 @@
gQueue.push(new focusARIAItem("aria_treeitem2", true));
gQueue.push(new focusARIAItem("aria_treeitem3", false));
- // selected state when widget item is focused (by aria-activedescendant)
- gQueue.push(new focusActiveDescendantItem("aria_tab5", "aria_tablist2", true));
- gQueue.push(new focusActiveDescendantItem("aria_tab6", "aria_tablist2", true));
+ // Test the selected state when a widget item is focused via
+ // aria-activedescendant if aria-selected is explicitly set on at least
+ // one of the items.
+ gQueue.push(new focusActiveDescendantItem("aria_tab5", "aria_tablist2", false));
+ gQueue.push(new focusActiveDescendantItem("aria_tab6", "aria_tablist2", false));
gQueue.push(new focusActiveDescendantItem("aria_tab4", "aria_tablist2", false));
gQueue.invoke(); // SimpleTest.finish() will be called in the end