commit 9d7ec8516734c060cf82cad12006fdf28fc68ad8
parent 07f0962d98a865bf251a057b7e51494a7e2c9ae5
Author: Jacques Newman <janewman@microsoft.com>
Date: Wed, 19 Nov 2025 04:55:50 +0000
Bug 2000733 [wpt PR 56076] - [focusgroup] aria role inference for focusgroup items., a=testonly
Automatic update from web-platform-tests
[focusgroup] aria role inference for focusgroup items.
Implements behavior-specific item role inference, see
https://open-ui.org/components/scoped-focusgroup.explainer/#supported-behaviors.
The focusgroup item logic lives in
AXObject::ComputeFinalRoleForSerialization because it needs final
contextual information that’s only available after regular role
computation:
* Focusgroup Owner's resolved role must match the role it's focusgroup behavior maps to.
* Focusgroup Owner's behavior data
See ComputeFinalRoleForSerialization's declaration header in ax_object.h
for more details.
Related ARIA issue discussion: https://github.com/w3c/aria/issues/2602
Bug: 40210717
Change-Id: Id1c0c0703e3fcb42bad9f858617dc8c9bd7faec6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7099362
Reviewed-by: Benjamin Beaudry <benjamin.beaudry@microsoft.com>
Auto-Submit: Jacques Newman <janewman@microsoft.com>
Commit-Queue: Jacques Newman <janewman@microsoft.com>
Reviewed-by: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1546187}
--
wpt-commits: a941071742f19a792a50349529b7d79cbd308a81
wpt-pr: 56076
Diffstat:
4 files changed, 247 insertions(+), 0 deletions(-)
diff --git a/testing/web-platform/tests/html/interaction/focus/focusgroup/tentative/ax-role-inference-children.html b/testing/web-platform/tests/html/interaction/focus/focusgroup/tentative/ax-role-inference-children.html
@@ -0,0 +1,89 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focusgroup - Child implied AX role inference</title>
+<meta name="assert" content="Generic child elements inside a focusgroup with implied owner role map to behavior-specific child roles; explicit and native child roles are preserved.">
+<link rel="author" title="Microsoft" href="http://www.microsoft.com/">
+<link rel="help" href="https://open-ui.org/components/scoped-focusgroup.explainer/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+
+<!-- Tablist behavior -->
+<div id=fgTablist focusgroup="tablist">
+ <span tabindex=0 id=tabChild>Tab</span>
+</div>
+
+<!-- Radiogroup behavior -->
+<div id=fgRadiogroup focusgroup="radiogroup">
+ <span tabindex=0 id=radioChild>Radio</span>
+</div>
+
+<!-- Listbox behavior -->
+<div id=fgListbox focusgroup="listbox">
+ <span tabindex=0 id=listboxChild>Option</span>
+</div>
+
+<!-- Menu behavior -->
+<div id=fgMenu focusgroup="menu">
+ <span tabindex=0 id=menuChild>Menu Item</span>
+</div>
+
+<!-- Menubar behavior -->
+<div id=fgMenubar focusgroup="menubar">
+ <span tabindex=0 id=menubarChild>Menu Item</span>
+</div>
+
+<!-- Explicit child role (should not be overridden) -->
+<div id=fgExplicitChild focusgroup="tablist">
+ <span id="explicitChild" tabindex=0 role=listitem>List Item</span>
+</div>
+
+<!-- Native child semantics (button) should be preserved -->
+<div id=fgNativeChild focusgroup="radiogroup">
+ <button id=nativeChild>Button</button>
+</div>
+
+<script>
+// Use WebDriver get_computed_role to assert author vs implied roles.
+// This mirrors pattern in wai-aria/role/basic.html.
+
+async function assert_role_equals(idref,expected_role) {
+ const element = document.getElementById(idref);
+ const role = await test_driver.get_computed_role(element);
+ assert_equals(role, expected_role);
+}
+
+promise_test(async t => {
+ await assert_role_equals('tabChild', 'tab');
+}, 'Tablist child gets implied tab role');
+
+promise_test(async t => {
+ // Blink exposes role="radio" as AXRadioButton internally; computed role should be 'radio'.
+ await assert_role_equals('radioChild', 'radio');
+}, 'Radiogroup child gets implied radio role');
+
+promise_test(async t => {
+ await assert_role_equals('listboxChild', 'option');
+}, 'Listbox child gets implied option role');
+
+promise_test(async t => {
+ await assert_role_equals('menuChild', 'menuitem');
+}, 'Menu child gets implied menuitem role');
+
+promise_test(async t => {
+ // Menubar items map to menuitem.
+ await assert_role_equals('menubarChild', 'menuitem');
+}, 'Menubar child gets implied menuitem role');
+
+promise_test(async t => {
+ // Explicit author-supplied listitem role should be preserved.
+ await assert_role_equals('explicitChild', 'listitem');
+}, 'Explicit child role preserved over implied mapping');
+
+promise_test(async t => {
+ // Native button semantics must be preserved.
+ await assert_role_equals('nativeChild', 'button');
+}, 'Native child semantics preserved over implied mapping');
+</script>
diff --git a/testing/web-platform/tests/html/interaction/focus/focusgroup/tentative/ax-role-inference-item-elementinternals-nested.html b/testing/web-platform/tests/html/interaction/focus/focusgroup/tentative/ax-role-inference-item-elementinternals-nested.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focusgroup - Nested owners ElementInternals item roles preserved</title>
+<meta name="assert" content="In nested focusgroups, ElementInternals supplied item roles (button/menuitem) remain preserved and are not coerced by owner inference.">
+<link rel="author" title="Microsoft" href="http://www.microsoft.com/">
+<link rel="help" href="https://open-ui.org/components/scoped-focusgroup.explainer/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="outerToolbar" focusgroup="toolbar">
+ <focusgroup-toolbar-button-internals id="tbItem" tabindex="0"></focusgroup-toolbar-button-internals>
+ <div id="innerMenu" focusgroup="menu">
+ <focusgroup-menuitem-internals id="menuItem" tabindex="0"></focusgroup-menuitem-internals>
+ </div>
+</div>
+
+<script>
+class FocusgroupToolbarButtonInternals extends HTMLElement {
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ this.internals_.role = 'button';
+ this.textContent = 'Tool';
+ }
+}
+customElements.define('focusgroup-toolbar-button-internals', FocusgroupToolbarButtonInternals);
+
+class FocusgroupMenuItemInternals extends HTMLElement {
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ this.internals_.role = 'menuitem';
+ this.textContent = 'Action';
+ }
+}
+customElements.define('focusgroup-menuitem-internals', FocusgroupMenuItemInternals);
+</script>
+
+<script>
+if (!window.accessibilityController) {
+ test(() => { assert_true(true); }, 'accessibilityController not available (noop)');
+} else {
+ test(() => {
+ const tbAX = accessibilityController.accessibleElementById('tbItem');
+ assert_equals(tbAX.role, 'AXRole: AXButton');
+ const menuItemAX = accessibilityController.accessibleElementById('menuItem');
+ assert_equals(menuItemAX.role, 'AXRole: AXMenuItem');
+ }, 'Nested focusgroup ElementInternals item roles preserved');
+}
+</script>
diff --git a/testing/web-platform/tests/html/interaction/focus/focusgroup/tentative/ax-role-inference-item-elementinternals-owner-implied.html b/testing/web-platform/tests/html/interaction/focus/focusgroup/tentative/ax-role-inference-item-elementinternals-owner-implied.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focusgroup - ElementInternals radio item roles preserved (radiogroup mapping)</title>
+<meta name="assert" content="ElementInternals supplied 'radio' roles must not be overridden by implied item mapping within a radiogroup focusgroup.">
+<link rel="author" title="Microsoft" href="http://www.microsoft.com/">
+<link rel="help" href="https://open-ui.org/components/scoped-focusgroup.explainer/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<div id="rgOwner" focusgroup="radiogroup">
+ <focusgroup-radio-internals id="radio1" tabindex="0"></focusgroup-radio-internals>
+ <focusgroup-radio-internals id="radio2" tabindex="-1"></focusgroup-radio-internals>
+</div>
+
+<script>
+class FocusgroupRadioInternals extends HTMLElement {
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ this.internals_.role = 'radio';
+ this.textContent = 'Choice';
+ }
+}
+customElements.define('focusgroup-radio-internals', FocusgroupRadioInternals);
+</script>
+
+<script>
+if (!window.accessibilityController) {
+ test(() => { assert_true(true); }, 'accessibilityController not available (noop)');
+} else {
+ test(() => {
+ const radio1AX = accessibilityController.accessibleElementById('radio1');
+ // Blink exposes ARIA role="radio" as AXRadioButton in the accessibility tree.
+ assert_equals(radio1AX.role, 'AXRole: AXRadioButton');
+ const radio2AX = accessibilityController.accessibleElementById('radio2');
+ assert_equals(radio2AX.role, 'AXRole: AXRadioButton');
+ }, 'ElementInternals radio roles preserved (radiogroup case)');
+}
+</script>
diff --git a/testing/web-platform/tests/html/interaction/focus/focusgroup/tentative/ax-role-inference-item-elementinternals.html b/testing/web-platform/tests/html/interaction/focus/focusgroup/tentative/ax-role-inference-item-elementinternals.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>HTML Test: focusgroup - ElementInternals item role never overridden</title>
+<meta name="assert" content="Author supplied ElementInternals role for a focusgroup item must never be overridden by implied item role inference, even when mismatching expected mapping.">
+<link rel="author" title="Microsoft" href="http://www.microsoft.com/">
+<link rel="help" href="https://open-ui.org/components/scoped-focusgroup.explainer/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<!-- Scenario A: Owner implied tablist, item supplies expected mapped role 'tab'. -->
+<div id="fgOwnerTablist" focusgroup="tablist">
+ <focusgroup-item-internals-expected id="tabItem" tabindex="0"></focusgroup-item-internals-expected>
+</div>
+
+<!-- Scenario B: Same owner type (tablist) but item supplies a different role ('button'); ensure we do NOT coerce it to tab. -->
+<div id="fgOwnerTablistMismatch" focusgroup="tablist">
+ <focusgroup-item-internals-mismatch id="mismatchItem" tabindex="0"></focusgroup-item-internals-mismatch>
+</div>
+
+<script>
+class FocusgroupItemInternalsExpected extends HTMLElement {
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ // ElementInternals author supplied role should be preserved.
+ this.internals_.role = 'tab';
+ this.textContent = 'Item';
+ }
+}
+customElements.define('focusgroup-item-internals-expected', FocusgroupItemInternalsExpected);
+
+class FocusgroupItemInternalsMismatch extends HTMLElement {
+ constructor() {
+ super();
+ this.internals_ = this.attachInternals();
+ // Deliberately provide a role that does NOT match the implied item role mapping.
+ this.internals_.role = 'button';
+ this.textContent = 'Other';
+ }
+}
+customElements.define('focusgroup-item-internals-mismatch', FocusgroupItemInternalsMismatch);
+</script>
+
+<script>
+if (!window.accessibilityController) {
+ test(() => { assert_true(true); }, 'accessibilityController not available (noop)');
+} else {
+ test(() => {
+ const ownerAX = accessibilityController.accessibleElementById('fgOwnerTablist');
+ assert_equals(ownerAX.role, 'AXRole: AXTabList');
+ }, 'Scenario A: Owner receives implied tablist role');
+
+ test(() => {
+ const itemAX = accessibilityController.accessibleElementById('tabItem');
+ assert_equals(itemAX.role, 'AXRole: AXTab');
+ }, 'Scenario A: Expected mapped ElementInternals role (tab) preserved');
+
+ test(() => {
+ const ownerAX = accessibilityController.accessibleElementById('fgOwnerTablistMismatch');
+ assert_equals(ownerAX.role, 'AXRole: AXTabList');
+ }, 'Scenario B: Owner implied tablist role still inferred with mismatched child role');
+
+ test(() => {
+ const itemAX = accessibilityController.accessibleElementById('mismatchItem');
+ // Critical assertion: we do NOT coerce to AXTab; remains AXButton per author intent.
+ assert_equals(itemAX.role, 'AXRole: AXButton');
+ }, 'Scenario B: Mismatched ElementInternals role (button) preserved (no coercion)');
+}
+</script>