11const { dom } = require ( 'aria-query' ) ;
2-
3- // Elements whose default HTML semantics make them interactive — a click handler
4- // here doesn't need a keyboard fallback because keyboard focus/activation is
5- // already built in. Derived from aria-query's `elementRoles` (tags whose
6- // inherent ARIA role descends from `widget`); matches jsx-a11y's
7- // `isInteractiveElement` treatment.
8- const INHERENTLY_INTERACTIVE_TAGS = new Set ( [
9- 'button' ,
10- 'datalist' ,
11- 'details' ,
12- 'embed' ,
13- 'iframe' ,
14- 'input' ,
15- 'label' ,
16- 'option' ,
17- 'select' ,
18- 'summary' ,
19- 'textarea' ,
20- ] ) ;
21-
22- // Roles whose keyboard semantics are widget-like. When a non-interactive element
23- // declares one of these, the user is opting in to a widget contract and the
24- // click handler does need a keyboard equivalent — so we still check.
25- // (Matches the jsx-a11y `isInteractiveRole` set.)
2+ const { isComponentInvocation } = require ( '../utils/is-component-invocation' ) ;
3+ const { isNativeInteractive } = require ( '../utils/native-interactive-elements' ) ;
264
275const KEYBOARD_EVENT_NAMES = new Set ( [ 'keydown' , 'keyup' , 'keypress' ] ) ;
286
297function findAttr ( node , name ) {
308 return node . attributes ?. find ( ( a ) => a . name === name ) ;
319}
3210
33- function getTextAttrValue ( attr ) {
11+ function getAttrTextValue ( attr ) {
3412 if ( attr ?. value ?. type === 'GlimmerTextNode' ) {
3513 return attr . value . chars ;
3614 }
3715 return undefined ;
3816}
3917
18+ // Adapter matching the `isNativeInteractive` util's expected signature:
19+ // `(node, attrName) -> string | undefined` for static attribute text values.
20+ function getTextAttrValue ( node , attrName ) {
21+ return getAttrTextValue ( findAttr ( node , attrName ) ) ;
22+ }
23+
4024// True iff the attribute's mustache value is the literal boolean `true` —
4125// e.g. `aria-hidden={{true}}`. Any other expression (path reference, helper
4226// call, etc.) is left to runtime and not treated as a static escape hatch.
@@ -73,7 +57,7 @@ function isHiddenFromScreenReader(node) {
7357}
7458
7559function hasPresentationRole ( node ) {
76- const role = getTextAttrValue ( findAttr ( node , 'role' ) ) ;
60+ const role = getAttrTextValue ( findAttr ( node , 'role' ) ) ;
7761 if ( ! role ) {
7862 return false ;
7963 }
@@ -84,27 +68,6 @@ function hasPresentationRole(node) {
8468 . some ( ( token ) => token === 'presentation' || token === 'none' ) ;
8569}
8670
87- function isInteractiveElement ( node ) {
88- const tag = node . tag ?. toLowerCase ( ) ;
89- if ( ! tag ) {
90- return false ;
91- }
92- if ( INHERENTLY_INTERACTIVE_TAGS . has ( tag ) ) {
93- // <input type="hidden"> is not interactive.
94- if ( tag === 'input' ) {
95- const type = getTextAttrValue ( findAttr ( node , 'type' ) ) ;
96- if ( type === 'hidden' ) {
97- return false ;
98- }
99- }
100- return true ;
101- }
102- if ( tag === 'a' && findAttr ( node , 'href' ) ) {
103- return true ;
104- }
105- return false ;
106- }
107-
10871function getOnModifierEventName ( modifier ) {
10972 if ( modifier . type !== 'GlimmerElementModifierStatement' ) {
11073 return undefined ;
@@ -156,7 +119,13 @@ module.exports = {
156119 return ;
157120 }
158121
159- // Skip components (not DOM elements).
122+ // Skip component invocations (PascalCase, named-arg, this-path, dot-path, named-block).
123+ if ( isComponentInvocation ( node ) ) {
124+ return ;
125+ }
126+
127+ // Skip tags aria-query doesn't recognize as DOM elements (e.g. hyphenated
128+ // custom elements like `<my-widget>`).
160129 if ( ! dom . has ( node . tag ) ) {
161130 return ;
162131 }
@@ -170,7 +139,7 @@ module.exports = {
170139 return ;
171140 }
172141
173- if ( isInteractiveElement ( node ) ) {
142+ if ( isNativeInteractive ( node , getTextAttrValue ) ) {
174143 return ;
175144 }
176145
0 commit comments