Skip to content

Commit 525cf6b

Browse files
Merge pull request #2475 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-invalid-role
Extract rule: template-no-invalid-role
2 parents 459164f + 6f39455 commit 525cf6b

4 files changed

Lines changed: 590 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ rules in templates can be disabled with eslint directives with mustache or html
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 | | | |
191191
| [template-no-invalid-interactive](docs/rules/template-no-invalid-interactive.md) | disallow non-interactive elements with interactive handlers | | | |
192+
| [template-no-invalid-role](docs/rules/template-no-invalid-role.md) | disallow invalid ARIA roles | | | |
192193
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
193194
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
194195

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# ember/template-no-invalid-role
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallows invalid ARIA roles in templates.
6+
7+
ARIA roles must be valid according to the ARIA specification. Using invalid roles can confuse assistive technologies and reduce accessibility.
8+
9+
## Rule Details
10+
11+
This rule checks that all `role` attributes contain valid ARIA role values. It also disallows `role="presentation"` and `role="none"` on semantic HTML elements, as doing so strips meaning from elements that inherently convey information.
12+
13+
## Examples
14+
15+
Examples of **incorrect** code for this rule:
16+
17+
```gjs
18+
<template>
19+
<div role="invalid">Content</div>
20+
</template>
21+
```
22+
23+
```gjs
24+
<template>
25+
<div role="btn">Should be "button"</div>
26+
</template>
27+
```
28+
29+
```gjs
30+
<template>
31+
<button role="presentation">Content</button>
32+
</template>
33+
```
34+
35+
```gjs
36+
<template>
37+
<nav role="none">Navigation</nav>
38+
</template>
39+
```
40+
41+
Examples of **correct** code for this rule:
42+
43+
```gjs
44+
<template>
45+
<div role="button">Content</div>
46+
</template>
47+
```
48+
49+
```gjs
50+
<template>
51+
<div role="navigation">Nav</div>
52+
</template>
53+
```
54+
55+
```gjs
56+
<template>
57+
<div role="presentation">Decorative</div>
58+
</template>
59+
```
60+
61+
```gjs
62+
<template>
63+
<div>No role attribute</div>
64+
</template>
65+
```
66+
67+
## Migration
68+
69+
- If violations are found, remediation should be planned to replace the semantic HTML with the `div` element. Additional CSS will likely be required.
70+
71+
## Options
72+
73+
| Name | Type | Default | Description |
74+
| ----------------------- | --------- | ------- | ------------------------------------------------------------- |
75+
| `catchNonexistentRoles` | `boolean` | `true` | When `true`, reports roles that don't exist in the ARIA spec. |
76+
77+
## References
78+
79+
- [eslint-plugin-ember template-no-invalid-role](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-invalid-role.md)
80+
- [WAI-ARIA Roles](https://www.w3.org/TR/wai-aria-1.2/#role_definitions)
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
const VALID_ROLES = new Set([
2+
'alert',
3+
'alertdialog',
4+
'application',
5+
'article',
6+
'associationlist',
7+
'banner',
8+
'blockquote',
9+
'button',
10+
'caption',
11+
'checkbox',
12+
'code',
13+
'columnheader',
14+
'combobox',
15+
'comment',
16+
'complementary',
17+
'contentinfo',
18+
'definition',
19+
'deletion',
20+
'dialog',
21+
'directory',
22+
'document',
23+
'emphasis',
24+
'feed',
25+
'figure',
26+
'form',
27+
'generic',
28+
'grid',
29+
'gridcell',
30+
'group',
31+
'heading',
32+
'img',
33+
'insertion',
34+
'link',
35+
'list',
36+
'listbox',
37+
'listitem',
38+
'log',
39+
'main',
40+
'mark',
41+
'marquee',
42+
'math',
43+
'menu',
44+
'menubar',
45+
'menuitem',
46+
'menuitemcheckbox',
47+
'menuitemradio',
48+
'meter',
49+
'navigation',
50+
'none',
51+
'note',
52+
'option',
53+
'paragraph',
54+
'presentation',
55+
'progressbar',
56+
'radio',
57+
'radiogroup',
58+
'region',
59+
'row',
60+
'rowgroup',
61+
'rowheader',
62+
'scrollbar',
63+
'search',
64+
'searchbox',
65+
'separator',
66+
'slider',
67+
'spinbutton',
68+
'status',
69+
'strong',
70+
'subscript',
71+
'suggestion',
72+
'superscript',
73+
'switch',
74+
'tab',
75+
'table',
76+
'tablist',
77+
'tabpanel',
78+
'term',
79+
'textbox',
80+
'time',
81+
'timer',
82+
'toolbar',
83+
'tooltip',
84+
'tree',
85+
'treegrid',
86+
'treeitem',
87+
]);
88+
89+
// Elements with semantic meaning that should not be given role="presentation" or role="none"
90+
const SEMANTIC_ELEMENTS = new Set([
91+
'a',
92+
'abbr',
93+
'address',
94+
'article',
95+
'aside',
96+
'b',
97+
'bdi',
98+
'bdo',
99+
'blockquote',
100+
'button',
101+
'caption',
102+
'cite',
103+
'code',
104+
'col',
105+
'colgroup',
106+
'data',
107+
'datalist',
108+
'dd',
109+
'del',
110+
'details',
111+
'dfn',
112+
'dialog',
113+
'dl',
114+
'dt',
115+
'em',
116+
'fieldset',
117+
'figcaption',
118+
'figure',
119+
'footer',
120+
'form',
121+
'h1',
122+
'h2',
123+
'h3',
124+
'h4',
125+
'h5',
126+
'h6',
127+
'header',
128+
'hgroup',
129+
'hr',
130+
'i',
131+
'input',
132+
'ins',
133+
'kbd',
134+
'label',
135+
'legend',
136+
'main',
137+
'mark',
138+
'menu',
139+
'meter',
140+
'nav',
141+
'ol',
142+
'optgroup',
143+
'option',
144+
'output',
145+
'p',
146+
'pre',
147+
'progress',
148+
'q',
149+
'rp',
150+
'rt',
151+
'ruby',
152+
's',
153+
'samp',
154+
'section',
155+
'select',
156+
'small',
157+
'strong',
158+
'sub',
159+
'summary',
160+
'sup',
161+
'table',
162+
'tbody',
163+
'td',
164+
'textarea',
165+
'tfoot',
166+
'th',
167+
'thead',
168+
'time',
169+
'tr',
170+
'u',
171+
'ul',
172+
'var',
173+
]);
174+
175+
/** @type {import('eslint').Rule.RuleModule} */
176+
module.exports = {
177+
meta: {
178+
type: 'problem',
179+
docs: {
180+
description: 'disallow invalid ARIA roles',
181+
category: 'Accessibility',
182+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-invalid-role.md',
183+
templateMode: 'both',
184+
},
185+
fixable: null,
186+
schema: [
187+
{
188+
type: 'object',
189+
properties: {
190+
catchNonexistentRoles: { type: 'boolean' },
191+
},
192+
additionalProperties: false,
193+
},
194+
],
195+
messages: {
196+
invalid: "Invalid ARIA role '{{role}}'. Must be a valid ARIA role.",
197+
presentationOnSemantic:
198+
'The role "{{role}}" should not be used on the semantic element <{{tag}}>.',
199+
},
200+
originallyFrom: {
201+
name: 'ember-template-lint',
202+
rule: 'lib/rules/no-invalid-role.js',
203+
docs: 'docs/rule/no-invalid-role.md',
204+
tests: 'test/unit/rules/no-invalid-role-test.js',
205+
},
206+
},
207+
208+
create(context) {
209+
const options = context.options[0] || {};
210+
const catchNonexistentRoles = options.catchNonexistentRoles !== false; // default true
211+
212+
return {
213+
GlimmerElementNode(node) {
214+
const roleAttr = node.attributes?.find((a) => a.name === 'role');
215+
if (!roleAttr || roleAttr.value?.type !== 'GlimmerTextNode') {
216+
return;
217+
}
218+
219+
const role = roleAttr.value.chars.trim();
220+
if (!role) {
221+
return;
222+
}
223+
224+
// Check for nonexistent roles
225+
if (catchNonexistentRoles && !VALID_ROLES.has(role)) {
226+
context.report({
227+
node: roleAttr,
228+
messageId: 'invalid',
229+
data: { role },
230+
});
231+
return;
232+
}
233+
234+
// Check for presentation/none role on semantic elements
235+
if ((role === 'presentation' || role === 'none') && SEMANTIC_ELEMENTS.has(node.tag)) {
236+
context.report({
237+
node: roleAttr,
238+
messageId: 'presentationOnSemantic',
239+
data: { role, tag: node.tag },
240+
});
241+
}
242+
},
243+
};
244+
},
245+
};

0 commit comments

Comments
 (0)