Skip to content

Commit 5991e40

Browse files
committed
Sync with template-lint
1 parent 69a9832 commit 5991e40

4 files changed

Lines changed: 191 additions & 296 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ rules in templates can be disabled with eslint directives with mustache or html
197197
| [template-no-nested-interactive](docs/rules/template-no-nested-interactive.md) | disallow nested interactive elements | | | |
198198
| [template-no-nested-landmark](docs/rules/template-no-nested-landmark.md) | disallow nested landmark elements | | | |
199199
| [template-no-pointer-down-event-binding](docs/rules/template-no-pointer-down-event-binding.md) | disallow pointer down event bindings | | | |
200-
| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | | | |
200+
| [template-no-unsupported-role-attributes](docs/rules/template-no-unsupported-role-attributes.md) | disallow ARIA attributes that are not supported by the element role | | 🔧 | |
201201
| [template-no-whitespace-within-word](docs/rules/template-no-whitespace-within-word.md) | disallow excess whitespace within words (e.g. "W e l c o m e") | | | |
202202
| [template-require-aria-activedescendant-tabindex](docs/rules/template-require-aria-activedescendant-tabindex.md) | require non-interactive elements with aria-activedescendant to have tabindex | | 🔧 | |
203203
| [template-require-iframe-title](docs/rules/template-require-iframe-title.md) | require iframe elements to have a title attribute | | | |
Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,35 @@
11
# ember/template-no-unsupported-role-attributes
22

3-
<!-- end auto-generated rule header -->
4-
5-
Disallows ARIA attributes that are not supported by the element's role.
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
64

7-
Different ARIA roles support different sets of ARIA attributes. Using unsupported attributes can cause confusion and doesn't provide the intended accessibility benefits.
5+
<!-- end auto-generated rule header -->
86

9-
## Rule Details
7+
Many ARIA states and properties are only available to elements with particular roles. This ensures that the appropriate information gets exposed to a browser's accessibility API for the given element.
108

11-
This rule checks elements with specific ARIA roles and ensures they only use supported ARIA attributes for that role.
9+
This rule disallows the use of ARIA properties unsupported by an element's defined role. An element's role may either be explicitly set by the `role` attribute, or it may be implicitly defined through the use of HTML elements with inherent roles. For example, `<input type="checkbox"` has the implicit role of `checkbox`.
1210

1311
## Examples
1412

15-
Examples of **incorrect** code for this rule:
16-
17-
```gjs
18-
<template>
19-
<div role="button" aria-checked="true">Button</div>
20-
</template>
21-
```
13+
This rule **forbids** the following:
2214

2315
```gjs
2416
<template>
25-
<div role="checkbox" aria-pressed="false">Checkbox</div>
17+
<div role="link" href="#" aria-checked />
18+
<input type="checkbox" aria-invalid="grammar" />
19+
<CustomComponent role="listbox" aria-level="2" />
2620
</template>
2721
```
2822

29-
```gjs
30-
<template>
31-
<div role="tab" aria-valuenow="1">Tab</div>
32-
</template>
33-
```
34-
35-
Examples of **correct** code for this rule:
36-
37-
```gjs
38-
<template>
39-
<div role="button" aria-pressed="true">Toggle Button</div>
40-
</template>
41-
```
42-
43-
```gjs
44-
<template>
45-
<div role="checkbox" aria-checked="false">Accept Terms</div>
46-
</template>
47-
```
23+
This rule **allows** the following:
4824

4925
```gjs
5026
<template>
51-
<div role="tab" aria-selected="true">Home Tab</div>
27+
<div role="heading" aria-level="1" />
28+
<input type="image" aria-atomic />
29+
<CustomComponent role="textbox" aria-required="true" />
5230
</template>
5331
```
5432

5533
## References
5634

57-
- [ARIA Roles](https://www.w3.org/TR/wai-aria-1.2/#role_definitions)
58-
- [ARIA States and Properties](https://www.w3.org/TR/wai-aria-1.2/#state_prop_def)
59-
- [eslint-plugin-ember template-no-unsupported-role-attributes](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-unsupported-role-attributes.md)
35+
- [Using ARIA, Roles, States, and Properties](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques)

lib/rules/template-no-unsupported-role-attributes.js

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
const { roles, elementRoles } = require('aria-query');
22

3-
/**
4-
* Get the implicit ARIA role for an HTML element based on its tag name and type attribute.
5-
* Uses the aria-query elementRoles mapping.
6-
*/
3+
function createUnsupportedAttributeErrorMessage(attribute, role, element) {
4+
if (element) {
5+
return `The attribute ${attribute} is not supported by the element ${element} with the implicit role of ${role}`;
6+
}
7+
8+
return `The attribute ${attribute} is not supported by the role ${role}`;
9+
}
10+
711
function getImplicitRole(tagName, typeAttribute) {
8-
// For input elements, match against entries with the specific type attribute
912
if (tagName === 'input') {
1013
for (const key of elementRoles.keys()) {
1114
if (key.name === tagName && key.attributes) {
@@ -17,13 +20,11 @@ function getImplicitRole(tagName, typeAttribute) {
1720
}
1821
}
1922
}
20-
// For all elements, fall back to the first matching entry by tag name
21-
for (const key of elementRoles.keys()) {
22-
if (key.name === tagName) {
23-
return elementRoles.get(key)[0];
24-
}
25-
}
26-
return null;
23+
24+
const key = [...elementRoles.keys()].find((entry) => entry.name === tagName);
25+
const implicitRoles = key && elementRoles.get(key);
26+
27+
return implicitRoles && implicitRoles[0];
2728
}
2829

2930
function getExplicitRole(node) {
@@ -42,6 +43,22 @@ function getTypeAttribute(node) {
4243
return null;
4344
}
4445

46+
function removeRangeWithAdjacentWhitespace(sourceText, range) {
47+
let [start, end] = range;
48+
49+
if (sourceText[end - 1] === ' ') {
50+
return [start, end];
51+
}
52+
53+
if (sourceText[start - 1] === ' ') {
54+
start -= 1;
55+
} else if (sourceText[end] === ' ') {
56+
end += 1;
57+
}
58+
59+
return [start, end];
60+
}
61+
4562
/** @type {import('eslint').Rule.RuleModule} */
4663
module.exports = {
4764
meta: {
@@ -52,11 +69,12 @@ module.exports = {
5269
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-unsupported-role-attributes.md',
5370
templateMode: 'both',
5471
},
55-
fixable: null,
72+
fixable: 'code',
5673
schema: [],
5774
messages: {
58-
unsupported:
59-
'ARIA attribute "{{attribute}}" is not supported for role "{{role}}". Remove the attribute or change the role.',
75+
unsupportedExplicit: 'The attribute {{attribute}} is not supported by the role {{role}}',
76+
unsupportedImplicit:
77+
'The attribute {{attribute}} is not supported by the element {{element}} with the implicit role of {{role}}',
6078
},
6179
originallyFrom: {
6280
name: 'ember-template-lint',
@@ -67,14 +85,34 @@ module.exports = {
6785
},
6886

6987
create(context) {
88+
const sourceCode = context.sourceCode || context.getSourceCode();
89+
90+
function reportUnsupported(node, invalidNode, attribute, role, element) {
91+
const messageId = element ? 'unsupportedImplicit' : 'unsupportedExplicit';
92+
93+
context.report({
94+
node,
95+
messageId,
96+
data: element ? { attribute, role, element } : { attribute, role },
97+
fix(fixer) {
98+
const [start, end] = removeRangeWithAdjacentWhitespace(
99+
sourceCode.getText(),
100+
invalidNode.range
101+
);
102+
return fixer.removeRange([start, end]);
103+
},
104+
});
105+
}
106+
70107
return {
71108
GlimmerElementNode(node) {
72-
// Determine the role: explicit first, then implicit
73109
let role = getExplicitRole(node);
110+
let element;
111+
74112
if (!role) {
75-
const tagName = node.tag;
113+
element = node.tag;
76114
const typeAttribute = getTypeAttribute(node);
77-
role = getImplicitRole(tagName, typeAttribute);
115+
role = getImplicitRole(element, typeAttribute);
78116
}
79117

80118
if (!role) {
@@ -87,20 +125,14 @@ module.exports = {
87125
}
88126

89127
const supportedProps = Object.keys(roleDefinition.props);
90-
const ariaAttributes = node.attributes?.filter(
91-
(attr) => attr.type === 'GlimmerAttrNode' && attr.name && attr.name.startsWith('aria-')
92-
);
93128

94-
for (const attr of ariaAttributes || []) {
129+
for (const attr of node.attributes || []) {
130+
if (attr.type !== 'GlimmerAttrNode' || !attr.name?.startsWith('aria-')) {
131+
continue;
132+
}
133+
95134
if (!supportedProps.includes(attr.name)) {
96-
context.report({
97-
node: attr,
98-
messageId: 'unsupported',
99-
data: {
100-
attribute: attr.name,
101-
role,
102-
},
103-
});
135+
reportUnsupported(node, attr, attr.name, role, element);
104136
}
105137
}
106138
},
@@ -115,7 +147,7 @@ module.exports = {
115147
return;
116148
}
117149

118-
const role = rolePair.value.original;
150+
const role = rolePair.value.value;
119151
if (!role) {
120152
return;
121153
}
@@ -130,14 +162,7 @@ module.exports = {
130162

131163
for (const pair of ariaPairs) {
132164
if (!supportedProps.includes(pair.key)) {
133-
context.report({
134-
node: pair,
135-
messageId: 'unsupported',
136-
data: {
137-
attribute: pair.key,
138-
role,
139-
},
140-
});
165+
reportUnsupported(node, pair, pair.key, role);
141166
}
142167
}
143168
},

0 commit comments

Comments
 (0)