Skip to content

Commit bcadc82

Browse files
committed
fix(template-no-invalid-aria-attributes): absorb allowundefined handling into validateByType
Removes the top-level string-"undefined" short-circuit in `isValidAriaValue` (previously a two-layer dance: a token-type precheck then a boolean-ish short-circuit). Absorbs the `allowundefined` handling directly into `validateByType` via a new `allowsUndefinedLiteral(attrDef, value)` helper that honors aria-query's convention for the 4 boolean-type attrs that encode "string 'undefined' is spec-valid": aria-expanded, aria-hidden, aria-grabbed, aria-selected. ## Before/after — same outcome, cleaner structure | Attribute | aria-query type / allowundefined | `"undefined"` string accepted? | |---|---|---| | aria-orientation | token / (unset) with `'undefined'` in values | yes — via token-values check | | aria-expanded | boolean / true | yes — via allowundefined | | aria-hidden | boolean / true | yes — via allowundefined | | aria-grabbed | boolean / true | yes — via allowundefined | | aria-selected | boolean / true | yes — via allowundefined | | aria-pressed | tristate / (unset) | NO — no allowundefined flag | | aria-checked | tristate / (unset) | NO — no allowundefined flag | All behavior preserved; just structured so that validity decisions happen in one place. ## Why keep the allowundefined path at all The 4 boolean-type attrs with `allowundefined: true` have spec-valid `"undefined"` values per WAI-ARIA 1.2 (e.g. aria-expanded accepts true/false/undefined). aria-query encodes this. Peers (jsx-a11y, lit-a11y, angular-eslint) effectively reject `aria-expanded="undefined"` because their `allowundefined` branch only fires on JS undefined, not the string. That's a peer bug; our path keeps us spec-compliant. Tests added for each of the 4 allowundefined attrs; new negative test for `aria-pressed="undefined"` (tristate without allowundefined) pins the correct rejection path.
1 parent 24882a3 commit bcadc82

2 files changed

Lines changed: 76 additions & 9 deletions

File tree

lib/rules/template-no-invalid-aria-attributes.js

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,20 @@ function isNumeric(value) {
1111
return !Number.isNaN(Number(value));
1212
}
1313

14-
function isValidAriaValue(attrName, value) {
15-
const attrDef = aria.get(attrName);
16-
if (!attrDef) {
17-
return true;
18-
}
14+
// Per aria-query's `allowundefined` convention: some attribute definitions
15+
// (notably the 4 boolean-type attrs aria-expanded / aria-hidden / aria-grabbed
16+
// / aria-selected) mark the literal string 'undefined' as a spec-valid value
17+
// meaning "state is not applicable" (per WAI-ARIA 1.2 value table, e.g.
18+
// https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). Any attribute type can
19+
// in principle accept 'undefined' via this flag.
20+
function allowsUndefinedLiteral(attrDef, value) {
21+
return value === 'undefined' && Boolean(attrDef.allowundefined);
22+
}
1923

20-
if (value === 'undefined') {
21-
return Boolean(attrDef.allowundefined);
24+
function validateByType(attrDef, value) {
25+
if (allowsUndefinedLiteral(attrDef, value)) {
26+
return true;
2227
}
23-
2428
switch (attrDef.type) {
2529
case 'boolean': {
2630
return isBoolean(value);
@@ -45,7 +49,9 @@ function isValidAriaValue(attrName, value) {
4549
return isNumeric(value) && !isBoolean(value);
4650
}
4751
case 'token': {
48-
// aria-query stores boolean values as actual booleans, convert for comparison
52+
// aria-query stores boolean values as actual booleans; stringify for comparison.
53+
// The string literal 'undefined' that appears in some values arrays (e.g.
54+
// aria-orientation) passes through this check naturally — no special-casing.
4955
const permittedValues = attrDef.values.map((v) =>
5056
typeof v === 'boolean' ? v.toString() : v
5157
);
@@ -60,6 +66,14 @@ function isValidAriaValue(attrName, value) {
6066
}
6167
}
6268

69+
function isValidAriaValue(attrName, value) {
70+
const attrDef = aria.get(attrName);
71+
if (!attrDef) {
72+
return true;
73+
}
74+
return validateByType(attrDef, value);
75+
}
76+
6377
function getExpectedTypeDescription(attrName) {
6478
const attrDef = aria.get(attrName);
6579
if (!attrDef) {

tests/lib/rules/template-no-invalid-aria-attributes.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,24 @@ ruleTester.run('template-no-invalid-aria-attributes', rule, {
3131
'<template><CustomComponent @ariaRequired={{this.ariaRequired}} aria-errormessage="errorId" /></template>',
3232
'<template><button type="submit" aria-disabled={{this.isDisabled}}>Submit</button></template>',
3333
'<template><div role="textbox" aria-sort={{if this.hasCustomSort "other" "ascending"}}></div></template>',
34+
// Boolean-type attributes with allowundefined: true per aria-query — the
35+
// string "undefined" is spec-valid (WAI-ARIA 1.2 value tables for these
36+
// attrs list true/false/undefined). All 4 share the same code path.
3437
'<template><div role="combobox" aria-expanded="undefined"></div></template>',
38+
'<template><div aria-hidden="undefined"></div></template>',
39+
'<template><div aria-grabbed="undefined" draggable="true"></div></template>',
40+
'<template><div role="option" aria-selected="undefined"></div></template>',
41+
42+
// Token-type aria-orientation lists "undefined" in its values array;
43+
// passes the natural token check (no special-casing needed).
44+
'<template><div role="slider" aria-orientation="undefined"></div></template>',
45+
'<template><div role="slider" aria-orientation="horizontal"></div></template>',
46+
47+
// aria-pressed is tristate WITHOUT allowundefined — string "undefined"
48+
// is NOT accepted. Explicit valid values still work.
49+
'<template><button aria-pressed="true">Toggle</button></template>',
50+
'<template><button aria-pressed="false">Toggle</button></template>',
51+
'<template><button aria-pressed="mixed">Toggle</button></template>',
3552
'<template><button aria-label={{if @isNew (t "actions.add") (t "actions.edit")}}></button></template>',
3653
],
3754
invalid: [
@@ -121,6 +138,18 @@ ruleTester.run('template-no-invalid-aria-attributes', rule, {
121138
output: null,
122139
errors: [{ messageId: 'invalidAriaAttributeValue' }],
123140
},
141+
{
142+
code: '<template><div role="slider" aria-orientation="sideways"></div></template>',
143+
output: null,
144+
errors: [{ messageId: 'invalidAriaAttributeValue' }],
145+
},
146+
// aria-pressed is tristate WITHOUT allowundefined — string "undefined"
147+
// is spec-invalid here (aria-query doesn't mark it allowundefined).
148+
{
149+
code: '<template><button aria-pressed="undefined">Toggle</button></template>',
150+
output: null,
151+
errors: [{ messageId: 'invalidAriaAttributeValue' }],
152+
},
124153
],
125154
});
126155

@@ -149,7 +178,20 @@ hbsRuleTester.run('template-no-invalid-aria-attributes', rule, {
149178
'<CustomComponent @ariaRequired={{this.ariaRequired}} aria-errormessage="errorId" />',
150179
'<button type="submit" aria-disabled={{this.isDisabled}}>Submit</button>',
151180
'<div role="textbox" aria-sort={{if this.hasCustomSort "other" "ascending"}}></div>',
181+
// Boolean-type attrs with allowundefined (spec-valid "undefined" literal):
152182
'<div role="combobox" aria-expanded="undefined"></div>',
183+
'<div aria-hidden="undefined"></div>',
184+
'<div aria-grabbed="undefined" draggable="true"></div>',
185+
'<div role="option" aria-selected="undefined"></div>',
186+
187+
// Token-type aria-orientation — "undefined" passes via values list:
188+
'<div role="slider" aria-orientation="undefined"></div>',
189+
'<div role="slider" aria-orientation="horizontal"></div>',
190+
191+
// aria-pressed is tristate WITHOUT allowundefined; valid values:
192+
'<button aria-pressed="true">Toggle</button>',
193+
'<button aria-pressed="mixed">Toggle</button>',
194+
153195
'<button aria-label={{if @isNew (t "actions.add") (t "actions.edit")}}></button>',
154196
],
155197
invalid: [
@@ -223,5 +265,16 @@ hbsRuleTester.run('template-no-invalid-aria-attributes', rule, {
223265
output: null,
224266
errors: [{ messageId: 'invalidAriaAttributeValue' }],
225267
},
268+
{
269+
code: '<div role="slider" aria-orientation="sideways"></div>',
270+
output: null,
271+
errors: [{ messageId: 'invalidAriaAttributeValue' }],
272+
},
273+
// aria-pressed has no allowundefined — "undefined" is spec-invalid here.
274+
{
275+
code: '<button aria-pressed="undefined">Toggle</button>',
276+
output: null,
277+
errors: [{ messageId: 'invalidAriaAttributeValue' }],
278+
},
226279
],
227280
});

0 commit comments

Comments
 (0)