Skip to content

Commit 1386766

Browse files
committed
fix: restore COMPOSITE_WIDGET_CHILDREN export lost in 55471af rewrite; prettier-format rule
1 parent e25656f commit 1386766

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

lib/rules/template-no-noninteractive-element-to-interactive-role.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ function buildNonInteractiveTagSet() {
110110
const ALLOWED_ROLE_OVERRIDE = new Map([
111111
['ul', new Set(['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'])],
112112
['ol', new Set(['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'])],
113-
['li', new Set(['menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'row', 'tab', 'treeitem'])],
113+
[
114+
'li',
115+
new Set(['menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'row', 'tab', 'treeitem']),
116+
],
114117
['table', new Set(['grid'])],
115118
['td', new Set(['gridcell'])],
116119
['fieldset', new Set(['radiogroup'])],

lib/utils/interactive-roles.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ const { roles } = require('aria-query');
1818
// are not usually interactive." jsx-a11y and lit-a11y agree.
1919
module.exports.INTERACTIVE_ROLES = buildInteractiveRoleSet();
2020

21+
// Composite-widget child map — for each composite-widget parent role, the
22+
// set of child roles that are legitimately nested inside it per ARIA's
23+
// "Required Owned Elements" (aria-query's `requiredOwnedElements`). Closed
24+
// transitively over chains of composite widgets (e.g. `grid` owns `row`,
25+
// `row` owns `gridcell` / `columnheader` / `rowheader`, so `grid` transitively
26+
// allows all of them).
27+
//
28+
// This drives the nested-interactive exception so canonical composite-widget
29+
// patterns (`listbox > option`, `tablist > tab`, `tree > treeitem`,
30+
// `grid > row > gridcell`, `radiogroup > radio`, etc.) are not flagged.
31+
module.exports.COMPOSITE_WIDGET_CHILDREN = buildCompositeWidgetChildren();
32+
2133
function buildInteractiveRoleSet() {
2234
const result = new Set(['toolbar']);
2335
for (const [role, def] of roles) {
@@ -31,3 +43,71 @@ function buildInteractiveRoleSet() {
3143
}
3244
return result;
3345
}
46+
47+
function buildCompositeWidgetChildren() {
48+
const own = new Map();
49+
for (const [role, def] of roles) {
50+
if (def.abstract) {
51+
continue;
52+
}
53+
const owned = def.requiredOwnedElements;
54+
if (!owned || owned.length === 0) {
55+
continue;
56+
}
57+
const kids = new Set();
58+
for (const chain of owned) {
59+
for (const child of chain) {
60+
kids.add(child);
61+
}
62+
}
63+
own.set(role, kids);
64+
}
65+
66+
const direct = new Map();
67+
for (const [role, def] of roles) {
68+
if (def.abstract) {
69+
continue;
70+
}
71+
const merged = new Set(own.get(role) || []);
72+
for (const chain of def.superClass || []) {
73+
for (const ancestor of chain) {
74+
const inherited = own.get(ancestor);
75+
if (inherited) {
76+
for (const child of inherited) {
77+
merged.add(child);
78+
}
79+
}
80+
}
81+
}
82+
if (merged.size > 0) {
83+
direct.set(role, merged);
84+
}
85+
}
86+
87+
const closed = new Map();
88+
function expand(role, visited) {
89+
if (closed.has(role)) {
90+
return closed.get(role);
91+
}
92+
const out = new Set();
93+
const kids = direct.get(role);
94+
if (!kids) {
95+
closed.set(role, out);
96+
return out;
97+
}
98+
for (const child of kids) {
99+
out.add(child);
100+
if (!visited.has(child)) {
101+
for (const grandchild of expand(child, new Set([...visited, child]))) {
102+
out.add(grandchild);
103+
}
104+
}
105+
}
106+
closed.set(role, out);
107+
return out;
108+
}
109+
for (const role of direct.keys()) {
110+
expand(role, new Set([role]));
111+
}
112+
return closed;
113+
}

0 commit comments

Comments
 (0)