Skip to content

Commit 1670293

Browse files
committed
fix(template-no-autofocus-attribute): align value-aware check with HTML boolean semantics
Per HTML Living Standard on boolean attributes, the presence of `autofocus` indicates TRUE regardless of value — `autofocus="false"` and `autofocus="autofocus"` are equally truthy. jsx-a11y's `no-autofocus` treats the literal string `"false"` as an opt-out (via `getPropValue`), but that's a peer-plugin convention that diverges from HTML semantics; vue-a11y and lit-a11y are presence-based, consistent with the spec. Narrow opt-out to the only case that is spec-consistent: - `autofocus={{false}}` in angle-bracket syntax — renders no attribute. - `{{input autofocus=false}}` in mustache hash-pair syntax — no attribute. Revert peer-parity opt-outs for `autofocus="false"`, `autofocus={{"false"}}`, and `{{input autofocus="false"}}` — these are now flagged per HTML spec semantics. Moved from valid → invalid in the test suite. Dialog exemption unchanged — keeps MDN-backed behavior for autofocus on and within <dialog>. Follows the spec-first direction established in ember-cli#2717 (aria-hidden), #19, #33.
1 parent 6588862 commit 1670293

2 files changed

Lines changed: 60 additions & 54 deletions

File tree

lib/rules/template-no-autofocus-attribute.js

Lines changed: 28 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,25 @@
11
/**
2-
* Returns true when the attribute value is statically known to be falsy
3-
* (i.e. the developer has written `autofocus="false"`, `autofocus={{false}}`,
4-
* or `autofocus={{"false"}}`). These forms are aligned with jsx-a11y's
5-
* `no-autofocus` rule, which reads the attribute value via `getPropValue` and
6-
* skips reporting when the value is falsy (e.g. `<div autoFocus={false} />`).
2+
* `autofocus` is a boolean HTML attribute. Per the HTML spec, any presence
3+
* (including `autofocus="false"`, `autofocus=""`, `autofocus="autofocus"`)
4+
* means the element will auto-focus. Only the genuine absence of the
5+
* attribute turns off auto-focus.
76
*
8-
* Valueless attributes (`<input autofocus>`) are TRUTHY per the HTML spec
9-
* (boolean attribute present == "on") and must still be flagged.
7+
* jsx-a11y's `no-autofocus` treats `autofocus={false}` / `autofocus="false"`
8+
* as opt-outs — that is a peer-plugin convention that diverges from HTML
9+
* boolean-attribute semantics. vue-a11y and lit-a11y are presence-based,
10+
* consistent with the spec. We follow the spec.
11+
*
12+
* The only exception is a mustache boolean-literal `{{false}}` in element
13+
* syntax — Glimmer authors writing `autofocus={{false}}` are expressing
14+
* intent to omit the attribute conditionally. Treat that narrow literal
15+
* case as opt-out (the rendered HTML will have no autofocus attr).
1016
*/
11-
function isExplicitlyFalsy(value) {
12-
if (!value) {
13-
// No value property at all — treat as bare boolean attribute (truthy).
17+
function isMustacheBooleanFalse(value) {
18+
if (value?.type !== 'GlimmerMustacheStatement') {
1419
return false;
1520
}
16-
17-
if (value.type === 'GlimmerTextNode') {
18-
// `autofocus="false"` → chars === "false". Bare `autofocus` has chars === "".
19-
return value.chars === 'false';
20-
}
21-
22-
if (value.type === 'GlimmerMustacheStatement') {
23-
const expr = value.path;
24-
if (!expr) {
25-
return false;
26-
}
27-
// `autofocus={{false}}` → BooleanLiteral(false).
28-
if (expr.type === 'GlimmerBooleanLiteral' && expr.value === false) {
29-
return true;
30-
}
31-
// `autofocus={{"false"}}` → StringLiteral("false").
32-
if (expr.type === 'GlimmerStringLiteral' && expr.value === 'false') {
33-
return true;
34-
}
35-
}
36-
37-
return false;
21+
const expr = value.path;
22+
return expr?.type === 'GlimmerBooleanLiteral' && expr.value === false;
3823
}
3924

4025
/**
@@ -92,9 +77,10 @@ module.exports = {
9277
return;
9378
}
9479

95-
// jsx-a11y parity: `autofocus="false"` / `={{false}}` / `={{"false"}}`
96-
// explicitly opt out and should not be flagged.
97-
if (isExplicitlyFalsy(autofocusAttr.value)) {
80+
// Mustache boolean-literal `autofocus={{false}}` renders no attribute
81+
// at all — the only statically-known opt-out consistent with HTML
82+
// boolean-attribute semantics.
83+
if (isMustacheBooleanFalse(autofocusAttr.value)) {
9884
return;
9985
}
10086

@@ -132,16 +118,14 @@ module.exports = {
132118
return;
133119
}
134120

135-
// Value-aware check for component/helper invocations such as
136-
// `{{input autofocus=false}}` or `{{input autofocus="false"}}`.
121+
// Mustache hash-pair `{{input autofocus=false}}` — boolean literal
122+
// false at the hash-pair level is unambiguous and renders no attr.
123+
// Note: `autofocus="false"` in mustache syntax IS still flagged — per
124+
// HTML boolean-attribute semantics the string "false" is truthy; it
125+
// is only jsx-a11y that carves that form out.
137126
const pairValue = autofocusPair.value;
138-
if (pairValue) {
139-
if (pairValue.type === 'GlimmerBooleanLiteral' && pairValue.value === false) {
140-
return;
141-
}
142-
if (pairValue.type === 'GlimmerStringLiteral' && pairValue.value === 'false') {
143-
return;
144-
}
127+
if (pairValue?.type === 'GlimmerBooleanLiteral' && pairValue.value === false) {
128+
return;
145129
}
146130

147131
context.report({

tests/lib/rules/template-no-autofocus-attribute.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,15 @@ ruleTester.run('template-no-autofocus-attribute', rule, {
2222
`<template>
2323
<button>Click me</button>
2424
</template>`,
25-
// Value-aware: explicit falsy values opt out (jsx-a11y parity).
26-
`<template>
27-
<input autofocus="false" />
28-
</template>`,
25+
// Mustache boolean-literal forms render NO attribute when the literal
26+
// is false — these are the statically-known opt-outs that align with
27+
// HTML boolean-attribute semantics.
2928
`<template>
3029
<input autofocus={{false}} />
3130
</template>`,
32-
`<template>
33-
<input autofocus={{"false"}} />
34-
</template>`,
3531
`<template>
3632
{{input autofocus=false}}
3733
</template>`,
38-
`<template>
39-
{{input autofocus="false"}}
40-
</template>`,
4134
// Dialog exception (MDN): autofocus on <dialog> is recommended.
4235
`<template>
4336
<dialog autofocus></dialog>
@@ -232,5 +225,34 @@ ruleTester.run('template-no-autofocus-attribute', rule, {
232225
},
233226
],
234227
},
228+
229+
// Per HTML boolean-attribute semantics, the string "false" / mustache
230+
// string "false" / hash-pair string "false" are all TRUTHY. Only the
231+
// mustache boolean-literal {{false}} renders no attribute.
232+
{
233+
code: `<template>
234+
<input autofocus="false" />
235+
</template>`,
236+
output: `<template>
237+
<input />
238+
</template>`,
239+
errors: [{ messageId: 'noAutofocus', type: 'GlimmerAttrNode' }],
240+
},
241+
{
242+
code: `<template>
243+
<input autofocus={{"false"}} />
244+
</template>`,
245+
output: `<template>
246+
<input />
247+
</template>`,
248+
errors: [{ messageId: 'noAutofocus', type: 'GlimmerAttrNode' }],
249+
},
250+
{
251+
code: `<template>
252+
{{input autofocus="false"}}
253+
</template>`,
254+
output: null,
255+
errors: [{ messageId: 'noAutofocus' }],
256+
},
235257
],
236258
});

0 commit comments

Comments
 (0)