Skip to content
Draft
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`.
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);
}

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>',
'<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