@@ -18,6 +18,18 @@ const { roles } = require('aria-query');
1818// are not usually interactive." jsx-a11y and lit-a11y agree.
1919module . 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+
2133function 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