Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 23 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,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.
Comment thread
johanrd marked this conversation as resolved.
Outdated
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 +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
);
Expand All @@ -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);
}
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
53 changes: 53 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,20 @@ 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="mixed">Toggle</button>',

'<button aria-label={{if @isNew (t "actions.add") (t "actions.edit")}}></button>',
],
invalid: [
Expand Down Expand Up @@ -223,5 +265,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