From bcadc821fb91eb91d276c39ca230d513f76fa938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Tue, 21 Apr 2026 22:37:01 +0200 Subject: [PATCH 1/2] fix(template-no-invalid-aria-attributes): absorb allowundefined handling into validateByType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../template-no-invalid-aria-attributes.js | 32 +++++++---- .../template-no-invalid-aria-attributes.js | 53 +++++++++++++++++++ 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/lib/rules/template-no-invalid-aria-attributes.js b/lib/rules/template-no-invalid-aria-attributes.js index 4a5c7a2a48..43701609e8 100644 --- a/lib/rules/template-no-invalid-aria-attributes.js +++ b/lib/rules/template-no-invalid-aria-attributes.js @@ -11,16 +11,20 @@ function isNumeric(value) { return !Number.isNaN(Number(value)); } -function isValidAriaValue(attrName, value) { - const attrDef = aria.get(attrName); - if (!attrDef) { - return true; - } +// Per aria-query's `allowundefined` convention: some attribute definitions +// (notably the 4 boolean-type attrs aria-expanded / aria-hidden / aria-grabbed +// / aria-selected) mark the literal string 'undefined' as a spec-valid value +// meaning "state is not applicable" (per WAI-ARIA 1.2 value table, e.g. +// https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). Any attribute type can +// in principle accept 'undefined' via this flag. +function allowsUndefinedLiteral(attrDef, value) { + return value === 'undefined' && Boolean(attrDef.allowundefined); +} - if (value === 'undefined') { - return Boolean(attrDef.allowundefined); +function validateByType(attrDef, value) { + if (allowsUndefinedLiteral(attrDef, value)) { + return true; } - switch (attrDef.type) { case 'boolean': { return isBoolean(value); @@ -45,7 +49,9 @@ function isValidAriaValue(attrName, value) { return isNumeric(value) && !isBoolean(value); } case 'token': { - // aria-query stores boolean values as actual booleans, convert for comparison + // aria-query stores boolean values as actual booleans; stringify for comparison. + // The string literal 'undefined' that appears in some values arrays (e.g. + // aria-orientation) passes through this check naturally — no special-casing. const permittedValues = attrDef.values.map((v) => typeof v === 'boolean' ? v.toString() : v ); @@ -60,6 +66,14 @@ function isValidAriaValue(attrName, value) { } } +function isValidAriaValue(attrName, value) { + const attrDef = aria.get(attrName); + if (!attrDef) { + return true; + } + return validateByType(attrDef, value); +} + function getExpectedTypeDescription(attrName) { const attrDef = aria.get(attrName); if (!attrDef) { diff --git a/tests/lib/rules/template-no-invalid-aria-attributes.js b/tests/lib/rules/template-no-invalid-aria-attributes.js index 7f8e10b8e6..379e5a9a5b 100644 --- a/tests/lib/rules/template-no-invalid-aria-attributes.js +++ b/tests/lib/rules/template-no-invalid-aria-attributes.js @@ -31,7 +31,24 @@ ruleTester.run('template-no-invalid-aria-attributes', rule, { '', '', '', + // Boolean-type attributes with allowundefined: true per aria-query — the + // string "undefined" is spec-valid (WAI-ARIA 1.2 value tables for these + // attrs list true/false/undefined). All 4 share the same code path. '', + '', + '', + '', + + // Token-type aria-orientation lists "undefined" in its values array; + // passes the natural token check (no special-casing needed). + '', + '', + + // aria-pressed is tristate WITHOUT allowundefined — string "undefined" + // is NOT accepted. Explicit valid values still work. + '', + '', + '', '', ], invalid: [ @@ -121,6 +138,18 @@ ruleTester.run('template-no-invalid-aria-attributes', rule, { output: null, errors: [{ messageId: 'invalidAriaAttributeValue' }], }, + { + code: '', + output: null, + errors: [{ messageId: 'invalidAriaAttributeValue' }], + }, + // aria-pressed is tristate WITHOUT allowundefined — string "undefined" + // is spec-invalid here (aria-query doesn't mark it allowundefined). + { + code: '', + output: null, + errors: [{ messageId: 'invalidAriaAttributeValue' }], + }, ], }); @@ -149,7 +178,20 @@ hbsRuleTester.run('template-no-invalid-aria-attributes', rule, { '', '', '
', + // Boolean-type attrs with allowundefined (spec-valid "undefined" literal): '
', + '
', + '
', + '
', + + // Token-type aria-orientation — "undefined" passes via values list: + '
', + '
', + + // aria-pressed is tristate WITHOUT allowundefined; valid values: + '', + '', + '', ], invalid: [ @@ -223,5 +265,16 @@ hbsRuleTester.run('template-no-invalid-aria-attributes', rule, { output: null, errors: [{ messageId: 'invalidAriaAttributeValue' }], }, + { + code: '
', + output: null, + errors: [{ messageId: 'invalidAriaAttributeValue' }], + }, + // aria-pressed has no allowundefined — "undefined" is spec-invalid here. + { + code: '', + output: null, + errors: [{ messageId: 'invalidAriaAttributeValue' }], + }, ], }); From 3be7dd4af4a8dd79507da42f96163072d64be1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Wed, 22 Apr 2026 14:03:13 +0200 Subject: [PATCH 2/2] docs+test: tighten allowundefined scope, add HBS test parity (Copilot review) --- lib/rules/template-no-invalid-aria-attributes.js | 14 ++++++++------ .../rules/template-no-invalid-aria-attributes.js | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/rules/template-no-invalid-aria-attributes.js b/lib/rules/template-no-invalid-aria-attributes.js index 43701609e8..6eead8bf76 100644 --- a/lib/rules/template-no-invalid-aria-attributes.js +++ b/lib/rules/template-no-invalid-aria-attributes.js @@ -11,12 +11,14 @@ function isNumeric(value) { return !Number.isNaN(Number(value)); } -// Per aria-query's `allowundefined` convention: some attribute definitions -// (notably the 4 boolean-type attrs aria-expanded / aria-hidden / aria-grabbed -// / aria-selected) mark the literal string 'undefined' as a spec-valid value -// meaning "state is not applicable" (per WAI-ARIA 1.2 value table, e.g. -// https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). Any attribute type can -// in principle accept 'undefined' via this flag. +// In aria-query 5.3.2, `allowundefined: true` is set only on the four +// boolean-like ARIA state attributes — `aria-expanded`, `aria-hidden`, +// `aria-grabbed`, `aria-selected` — whose WAI-ARIA 1.2 value tables list +// the literal string `"undefined"` as a spec-valid value meaning "state +// is not applicable" (e.g. https://www.w3.org/TR/wai-aria-1.2/#aria-expanded). +// The flag is nominally type-agnostic, but in practice this function only +// green-lights `"undefined"` for that boolean-like subset; no non-boolean +// ARIA attribute in aria-query currently sets `allowundefined`. function allowsUndefinedLiteral(attrDef, value) { return value === 'undefined' && Boolean(attrDef.allowundefined); } diff --git a/tests/lib/rules/template-no-invalid-aria-attributes.js b/tests/lib/rules/template-no-invalid-aria-attributes.js index 379e5a9a5b..163792bf3e 100644 --- a/tests/lib/rules/template-no-invalid-aria-attributes.js +++ b/tests/lib/rules/template-no-invalid-aria-attributes.js @@ -190,6 +190,7 @@ hbsRuleTester.run('template-no-invalid-aria-attributes', rule, { // aria-pressed is tristate WITHOUT allowundefined; valid values: '', + '', '', '',