Skip to content

Commit 0453cbe

Browse files
Merge pull request #2471 from ember-cli/nvp/template-lint-extract-rule-template-no-invalid-aria-attributes
Extract rule: no-invalid-aria-attributes
2 parents ad734a3 + d15b5b7 commit 0453cbe

6 files changed

Lines changed: 404 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ rules in templates can be disabled with eslint directives with mustache or html
188188
| [template-no-autofocus-attribute](docs/rules/template-no-autofocus-attribute.md) | disallow autofocus attribute | | 🔧 | |
189189
| [template-no-empty-headings](docs/rules/template-no-empty-headings.md) | disallow empty heading elements | | | |
190190
| [template-no-heading-inside-button](docs/rules/template-no-heading-inside-button.md) | disallow heading elements inside button elements | | | |
191+
| [template-no-invalid-aria-attributes](docs/rules/template-no-invalid-aria-attributes.md) | disallow invalid aria-* attributes | | | |
191192
| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | | | |
192193
| [template-no-invalid-link-title](docs/rules/template-no-invalid-link-title.md) | disallow invalid title attributes on link elements | | | |
193194
| [template-no-invalid-role](docs/rules/template-no-invalid-role.md) | disallow invalid ARIA roles | | | |
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# ember/template-no-invalid-aria-attributes
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow invalid ARIA attributes. Only use valid ARIA attributes as defined in the ARIA specification.
6+
7+
## Rule Details
8+
9+
This rule validates that only standard ARIA attributes are used on elements.
10+
11+
## Examples
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```gjs
16+
<template>
17+
<div aria-fake="value">Content</div>
18+
</template>
19+
20+
<template>
21+
<div aria-invalid-attr="value">Content</div>
22+
</template>
23+
```
24+
25+
Examples of **correct** code for this rule:
26+
27+
```gjs
28+
<template>
29+
<div aria-label="Label">Content</div>
30+
</template>
31+
32+
<template>
33+
<div aria-hidden="true">Content</div>
34+
</template>
35+
36+
<template>
37+
<div aria-describedby="description-id">Content</div>
38+
</template>
39+
```
40+
41+
## References
42+
43+
- [Using ARIA, Roles, States, and Properties](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
const { aria } = require('aria-query');
2+
3+
function isBoolean(value) {
4+
return value === 'true' || value === 'false';
5+
}
6+
7+
function isNumeric(value) {
8+
if (typeof value !== 'string' || value === '') {
9+
return false;
10+
}
11+
return !Number.isNaN(Number(value));
12+
}
13+
14+
function isValidAriaValue(attrName, value) {
15+
const attrDef = aria.get(attrName);
16+
if (!attrDef) {
17+
return true;
18+
}
19+
20+
if (value === 'undefined') {
21+
return Boolean(attrDef.allowundefined);
22+
}
23+
24+
switch (attrDef.type) {
25+
case 'boolean': {
26+
return isBoolean(value);
27+
}
28+
case 'tristate': {
29+
return isBoolean(value) || value === 'mixed';
30+
}
31+
case 'string': {
32+
return typeof value === 'string';
33+
}
34+
case 'id': {
35+
return typeof value === 'string' && !isBoolean(value);
36+
}
37+
case 'idlist': {
38+
return (
39+
typeof value === 'string' &&
40+
value.split(' ').every((token) => token.length > 0 && !isBoolean(token))
41+
);
42+
}
43+
case 'integer': {
44+
return /^-?\d+$/.test(value);
45+
}
46+
case 'number': {
47+
return isNumeric(value) && !isBoolean(value);
48+
}
49+
case 'token': {
50+
// aria-query stores boolean values as actual booleans, convert for comparison
51+
const permittedValues = attrDef.values.map((v) =>
52+
typeof v === 'boolean' ? v.toString() : v
53+
);
54+
return permittedValues.includes(value);
55+
}
56+
case 'tokenlist': {
57+
return value.split(' ').every((token) => attrDef.values.includes(token.toLowerCase()));
58+
}
59+
default: {
60+
return true;
61+
}
62+
}
63+
}
64+
65+
/** @type {import('eslint').Rule.RuleModule} */
66+
module.exports = {
67+
meta: {
68+
type: 'problem',
69+
docs: {
70+
description: 'disallow invalid aria-* attributes',
71+
category: 'Accessibility',
72+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-invalid-aria-attributes.md',
73+
templateMode: 'both',
74+
},
75+
schema: [],
76+
messages: {
77+
noInvalidAriaAttribute: 'Invalid ARIA attribute: {{attribute}}',
78+
invalidAriaAttributeValue: 'Invalid value for ARIA attribute {{attribute}}.',
79+
},
80+
originallyFrom: {
81+
name: 'ember-template-lint',
82+
rule: 'lib/rules/no-invalid-aria-attributes.js',
83+
docs: 'docs/rule/no-invalid-aria-attributes.md',
84+
tests: 'test/unit/rules/no-invalid-aria-attributes-test.js',
85+
},
86+
},
87+
88+
create(context) {
89+
return {
90+
GlimmerAttrNode(node) {
91+
if (!node.name.startsWith('aria-')) {
92+
return;
93+
}
94+
95+
// Check for unknown ARIA attribute
96+
if (!aria.has(node.name)) {
97+
context.report({
98+
node,
99+
messageId: 'noInvalidAriaAttribute',
100+
data: { attribute: node.name },
101+
});
102+
return;
103+
}
104+
105+
// Skip value validation for dynamic values (MustacheStatement, ConcatStatement)
106+
if (
107+
!node.value ||
108+
node.value.type === 'GlimmerMustacheStatement' ||
109+
node.value.type === 'GlimmerConcatStatement'
110+
) {
111+
return;
112+
}
113+
114+
// Validate value for text node values
115+
if (node.value.type === 'GlimmerTextNode') {
116+
const value = node.value.chars;
117+
if (!isValidAriaValue(node.name, value)) {
118+
context.report({
119+
node,
120+
messageId: 'invalidAriaAttributeValue',
121+
data: { attribute: node.name },
122+
});
123+
}
124+
}
125+
},
126+
};
127+
},
128+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
},
6161
"dependencies": {
6262
"@ember-data/rfc395-data": "^0.0.4",
63+
"aria-query": "^5.3.2",
6364
"css-tree": "^3.0.1",
6465
"editorconfig": "^3.0.2",
6566
"ember-eslint-parser": "^0.6.0",

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)