@@ -27,18 +27,13 @@ function isAriaHiddenTrue(node) {
2727 if ( ! value ) {
2828 return false ;
2929 }
30- if ( value . type === 'GlimmerTextNode' ) {
31- return value . chars . trim ( ) . toLowerCase ( ) === 'true' ;
32- }
33- if ( value . type === 'GlimmerMustacheStatement' && value . path ) {
34- if ( value . path . type === 'GlimmerBooleanLiteral' ) {
35- return value . path . value === true ;
36- }
37- if ( value . path . type === 'GlimmerStringLiteral' ) {
38- return value . path . value . trim ( ) . toLowerCase ( ) === 'true' ;
39- }
30+ // Resolve through getStaticAttrValue so quoted-mustache concat forms
31+ // (e.g. aria-hidden="{{true}}") and case variants normalize uniformly.
32+ const resolved = getStaticAttrValue ( value ) ;
33+ if ( typeof resolved !== 'string' ) {
34+ return false ;
4035 }
41- return false ;
36+ return resolved . trim ( ) . toLowerCase ( ) === 'true' ;
4237}
4338
4439// Tags with an unconditional default focusable UI (sequentially focusable per
@@ -67,7 +62,23 @@ function isDisabledFormControl(node, tag) {
6762 if ( ! DISABLEABLE_TAGS . has ( tag ) ) {
6863 return false ;
6964 }
70- return Boolean ( findAttr ( node , 'disabled' ) ) ;
65+ const attr = findAttr ( node , 'disabled' ) ;
66+ if ( ! attr ) {
67+ return false ;
68+ }
69+ // Per docs/glimmer-attribute-behavior.md, ONLY bare-mustache boolean-false
70+ // (`disabled={{false}}`) renders as omitted at runtime — concat
71+ // (`disabled="{{false}}"`) and string-literal (`disabled={{"false"}}`) forms
72+ // still render the attribute as present and the control IS disabled.
73+ const v = attr . value ;
74+ if (
75+ v ?. type === 'GlimmerMustacheStatement' &&
76+ v . path ?. type === 'GlimmerBooleanLiteral' &&
77+ v . path . value === false
78+ ) {
79+ return false ;
80+ }
81+ return true ;
7182}
7283
7384// Narrow rule-local "keyboard-focusable" check. Intentionally distinct from
0 commit comments