Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions lib/rules/template-no-invalid-aria-attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ function isNumeric(value) {
return !Number.isNaN(Number(value));
}

function isValidAriaValue(attrName, value) {
const attrDef = aria.get(attrName);
if (!attrDef) {
return true;
}
// 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`.
Comment on lines +14 to +21
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new block comment ties behavior to an exact aria-query version (5.3.2), but package.json depends on aria-query: ^5.3.2, so consumers may run newer versions where allowundefined coverage changes. To avoid the comment becoming stale/misleading, consider rewording it to be version-agnostic (e.g., "currently in aria-query") or pinning the dependency if the version-specific assertion is important.

Copilot uses AI. Check for mistakes.
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);
Expand All @@ -45,7 +51,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
);
Expand All @@ -60,6 +68,14 @@ function isValidAriaValue(attrName, value) {
}
}

function isValidAriaValue(attrName, value) {
const attrDef = aria.get(attrName);
if (!attrDef) {
return true;
}
return validateByType(attrDef, value);
}
Comment on lines +71 to +77
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the rule now explicitly accepts the string "undefined" for attributes where attrDef.allowundefined is true, the error message shown for other invalid values of those attributes (via getExpectedTypeDescription) is currently incomplete (it will still say "a boolean." and omit the valid "undefined" literal). Consider updating getExpectedTypeDescription to include "or the string "undefined"" when attrDef.allowundefined is true so reported expectations match actual validation.

Copilot uses AI. Check for mistakes.

function getExpectedTypeDescription(attrName) {
const attrDef = aria.get(attrName);
if (!attrDef) {
Expand Down
54 changes: 54 additions & 0 deletions tests/lib/rules/template-no-invalid-aria-attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,24 @@ ruleTester.run('template-no-invalid-aria-attributes', rule, {
'<template><CustomComponent @ariaRequired={{this.ariaRequired}} aria-errormessage="errorId" /></template>',
'<template><button type="submit" aria-disabled={{this.isDisabled}}>Submit</button></template>',
'<template><div role="textbox" aria-sort={{if this.hasCustomSort "other" "ascending"}}></div></template>',
// 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.
'<template><div role="combobox" aria-expanded="undefined"></div></template>',
'<template><div aria-hidden="undefined"></div></template>',
'<template><div aria-grabbed="undefined" draggable="true"></div></template>',
'<template><div role="option" aria-selected="undefined"></div></template>',

// Token-type aria-orientation lists "undefined" in its values array;
// passes the natural token check (no special-casing needed).
'<template><div role="slider" aria-orientation="undefined"></div></template>',
'<template><div role="slider" aria-orientation="horizontal"></div></template>',

// aria-pressed is tristate WITHOUT allowundefined — string "undefined"
// is NOT accepted. Explicit valid values still work.
'<template><button aria-pressed="true">Toggle</button></template>',
'<template><button aria-pressed="false">Toggle</button></template>',
'<template><button aria-pressed="mixed">Toggle</button></template>',
'<template><button aria-label={{if @isNew (t "actions.add") (t "actions.edit")}}></button></template>',
],
invalid: [
Expand Down Expand Up @@ -121,6 +138,18 @@ ruleTester.run('template-no-invalid-aria-attributes', rule, {
output: null,
errors: [{ messageId: 'invalidAriaAttributeValue' }],
},
{
code: '<template><div role="slider" aria-orientation="sideways"></div></template>',
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: '<template><button aria-pressed="undefined">Toggle</button></template>',
output: null,
errors: [{ messageId: 'invalidAriaAttributeValue' }],
},
],
});

Expand Down Expand Up @@ -149,7 +178,21 @@ hbsRuleTester.run('template-no-invalid-aria-attributes', rule, {
'<CustomComponent @ariaRequired={{this.ariaRequired}} aria-errormessage="errorId" />',
'<button type="submit" aria-disabled={{this.isDisabled}}>Submit</button>',
'<div role="textbox" aria-sort={{if this.hasCustomSort "other" "ascending"}}></div>',
// Boolean-type attrs with allowundefined (spec-valid "undefined" literal):
'<div role="combobox" aria-expanded="undefined"></div>',
'<div aria-hidden="undefined"></div>',
'<div aria-grabbed="undefined" draggable="true"></div>',
'<div role="option" aria-selected="undefined"></div>',

// Token-type aria-orientation — "undefined" passes via values list:
'<div role="slider" aria-orientation="undefined"></div>',
'<div role="slider" aria-orientation="horizontal"></div>',

// aria-pressed is tristate WITHOUT allowundefined; valid values:
'<button aria-pressed="true">Toggle</button>',
Comment thread
johanrd marked this conversation as resolved.
'<button aria-pressed="false">Toggle</button>',
'<button aria-pressed="mixed">Toggle</button>',

'<button aria-label={{if @isNew (t "actions.add") (t "actions.edit")}}></button>',
],
invalid: [
Expand Down Expand Up @@ -223,5 +266,16 @@ hbsRuleTester.run('template-no-invalid-aria-attributes', rule, {
output: null,
errors: [{ messageId: 'invalidAriaAttributeValue' }],
},
{
code: '<div role="slider" aria-orientation="sideways"></div>',
output: null,
errors: [{ messageId: 'invalidAriaAttributeValue' }],
},
// aria-pressed has no allowundefined — "undefined" is spec-invalid here.
{
code: '<button aria-pressed="undefined">Toggle</button>',
output: null,
errors: [{ messageId: 'invalidAriaAttributeValue' }],
},
],
});
Loading