77
88const { roles, elementRoles } = require ( 'aria-query' ) ;
99const { isNativeElement } = require ( '../utils/is-native-element' ) ;
10+ const { classifyAttribute } = require ( '../utils/glimmer-attr-presence' ) ;
1011
1112function findAttr ( node , name ) {
1213 return node . attributes ?. find ( ( attr ) => attr . name === name ) ;
@@ -142,29 +143,23 @@ function getRole(node) {
142143}
143144
144145function hasNonEmptyLabelAttr ( node , name ) {
146+ // Per docs/glimmer-attribute-behavior.md, bare-mustache falsy literals on
147+ // aria-* attributes (rows h6, h9, h10) cause Glimmer to OMIT the attribute
148+ // at runtime — there is no aria-label on the rendered element, so it can't
149+ // be a misuse. Use classifyAttribute so the runtime-presence drives the
150+ // answer rather than AST-presence.
145151 const attr = findAttr ( node , name ) ;
146- if ( ! attr ) {
152+ const { presence, value } = classifyAttribute ( attr ) ;
153+ if ( presence === 'absent' ) {
147154 return false ;
148155 }
149- // A valueless attribute (e.g. `<div aria-label>`) carries no accessible
150- // name. Treat it as empty — not as non-empty — so downstream checks don't
151- // mistake it for an author-declared label.
152- if ( attr . value === null || attr . value === undefined ) {
156+ // 'present' with statically-known value: empty / whitespace renders no
157+ // accessible name, so treat as empty (not a misuse).
158+ if ( value !== null && value . trim ( ) === '' ) {
153159 return false ;
154160 }
155- if ( attr . value . type === 'GlimmerTextNode' ) {
156- return attr . value . chars . trim ( ) !== '' ;
157- }
158- // Mustache with a static string literal path: `aria-label={{""}}` is still
159- // empty, so treat it the same as an empty text node.
160- if (
161- attr . value . type === 'GlimmerMustacheStatement' &&
162- attr . value . path ?. type === 'GlimmerStringLiteral'
163- ) {
164- return attr . value . path . value . trim ( ) !== '' ;
165- }
166- // All other mustache / concat forms — treat as non-empty (author has
167- // declared intent).
161+ // Otherwise the attribute renders a non-empty (or dynamic) value — author
162+ // has declared intent.
168163 return true ;
169164}
170165
@@ -190,8 +185,13 @@ function isExplicitlyDecorative(node) {
190185// high false-positive cost (the author wants the label read on focus)
191186// relative to the true-positive it would catch. Disable via
192187// `strictTabindex: true` to get strict spec-role enforcement.
188+ //
189+ // Per docs/glimmer-attribute-behavior.md (rows t6, t7), bare-mustache
190+ // `tabindex={{false}}` / `{{null}}` / `{{undefined}}` cause Glimmer to omit
191+ // the attribute at runtime — the element has no tabindex and the escape
192+ // hatch should NOT fire.
193193function hasTabindex ( node ) {
194- return Boolean ( findAttr ( node , 'tabindex' ) ) ;
194+ return classifyAttribute ( findAttr ( node , 'tabindex' ) ) . presence === 'present' ;
195195}
196196
197197/** @type {import('eslint').Rule.RuleModule } */
0 commit comments