Skip to content

Commit 4cf49dc

Browse files
mauryaparivhoyer
authored andcommitted
feat: add rules for prohibiting use of aria-hidden and role='presentation' on focusable elements. (#1169)
* Added rule for prohibiting use of aria-hidden and role=presentaion on focusable elements * Refactored code into seprate files and utility file * Added EOL for files with prettier errors * Removed nested ifs * Fixed EOL errors * Added more lines * Fixed prettier issues * Removed commented code
1 parent 18841d3 commit 4cf49dc

8 files changed

+317
-0
lines changed
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# no-aria-hidden-on-focusbable
2+
3+
Enforce that `aria-hidden="true"` is not set on focusable elements or parent of focusable elements.
4+
5+
`aria-hidden="true"` can be used to hide purely decorative content from screen reader users. An element with `aria-hidden="true"` that can also be reached by keyboard can lead to confusion or unexpected behavior for screen reader users. Avoid using `aria-hidden="true"` on focusable elements.
6+
7+
See more in [WAI-ARIA Use in HTML](https://www.w3.org/TR/using-aria/#fourth).
8+
9+
### ✔ Succeed
10+
11+
```vue
12+
<template>
13+
<button>Press Me</button>
14+
</template>
15+
```
16+
17+
```vue
18+
<template>
19+
<div aria-hidden="true"><button tabindex="-1">Submit</button></div>
20+
</template>
21+
```
22+
23+
```vue
24+
<template>
25+
<div aria-hidden='true'><span>Some text</div></div>
26+
</template>
27+
```
28+
29+
```vue
30+
<template>
31+
<button tabindex="-1" aria-hidden="true">Press</button>
32+
</template>
33+
```
34+
35+
```vue
36+
<template>
37+
<div aria-hidden="true"><a href="#" tabindex="-1">Link</a></div>
38+
</template>
39+
```
40+
41+
```vue
42+
<template>
43+
<div aria-hidden="true"><span>Some text</span></div>
44+
</template>
45+
```
46+
47+
### ❌ Fail
48+
49+
```vue
50+
<template>
51+
<button aria-hidden="true">press me</button>
52+
</template>
53+
```
54+
55+
```vue
56+
<template>
57+
<button aria-hidden="true">press me</button>
58+
</template>
59+
```
60+
61+
```vue
62+
<template>
63+
<a href="#" aria-hidden="true">press me</a>
64+
</template>
65+
```
66+
67+
```vue
68+
<template>
69+
<div aria-hidden="true">
70+
<button>press me</button>
71+
</div>
72+
</template>
73+
```
74+
75+
```vue
76+
<template>
77+
<span tabindex="0" aria-hidden="true"><em>Icon</em></span>
78+
</template>
79+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# no-role-presentaion-on-focusbable
2+
3+
Enforce that `role="presentation"` is not set on focusable elements or parent of focusbale elements.
4+
5+
`role="presentation` can be used to hide purely decorative content from screen reader users. An element with `role="presentation"` that can also be reached by keyboard can lead to confusion or unexpected behavior for screen reader users. Avoid using `role="presentation"` on focusable elements.
6+
7+
See more in [WAI-ARIA Use in HTML](https://www.w3.org/TR/using-aria/#fourth).
8+
9+
### ✔ Succeed
10+
11+
```vue
12+
<template>
13+
<button>Press Me</button>
14+
</template>
15+
```
16+
17+
```vue
18+
<template>
19+
<div role="presentation"><button tabindex="-1">Submit</button></div>
20+
</template>
21+
```
22+
23+
```vue
24+
<template>
25+
<div role="presentation"><span>Some text</div></div>
26+
</template>
27+
```
28+
29+
```vue
30+
<template>
31+
<button tabindex="-1" role="presentation">Press</button>
32+
</template>
33+
```
34+
35+
```vue
36+
<template>
37+
<div role="presentation"><a href="#" tabindex="-1">Link</a></div>
38+
</template>
39+
```
40+
41+
```vue
42+
<template>
43+
<div role="presentation"><span>Some text</span></div>
44+
</template>
45+
```
46+
47+
### ❌ Fail
48+
49+
```vue
50+
<template>
51+
<button role="presentation">press me</button>
52+
</template>
53+
```
54+
55+
```vue
56+
<template>
57+
<button role="presentation">press me</button>
58+
</template>
59+
```
60+
61+
```vue
62+
<template>
63+
<a href="#" role="presentation">press me</a>
64+
</template>
65+
```
66+
67+
```vue
68+
<template>
69+
<div role="presentation">
70+
<button>press me</button>
71+
</div>
72+
</template>
73+
```
74+
75+
```vue
76+
<template>
77+
<span tabindex="0" role="presentation"><em>Icon</em></span>
78+
</template>
79+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import rule from "../no-aria-hidden-on-focusable";
2+
import makeRuleTester from "./makeRuleTester";
3+
4+
makeRuleTester("no-presentation-role-or-aria-hidden-on-focusable", rule, {
5+
valid: [
6+
"<button>Submit</button>",
7+
"<div aria-hidden='true'><button tabindex='-1'>Some text</button></div>",
8+
"<div><button>Submit</button></div>",
9+
"<a href='#' tabindex='-1'>link</a>",
10+
"<button tabindex='-1' aria-hidden='true'>Press</button>",
11+
"<div aria-hidden='true'><a href='#' tabindex='-1'>Link</a></div>"
12+
],
13+
invalid: [
14+
{
15+
code: "<div aria-hidden='true'><button>Submit</button></div>",
16+
errors: [{ messageId: "default" }]
17+
},
18+
{
19+
code: "<button type='button' aria-hidden='true'>Submit</button>",
20+
errors: [{ messageId: "default" }]
21+
},
22+
{
23+
code: "<a href='#' aria-hidden='true'>Link</a>",
24+
errors: [{ messageId: "default" }]
25+
},
26+
{
27+
code: "<span tabindex='0' aria-hidden='true'><em>Icon</em></span>",
28+
errors: [{ messageId: "default" }]
29+
}
30+
]
31+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import rule from "../no-role-presentation-on-focusable";
2+
import makeRuleTester from "./makeRuleTester";
3+
4+
makeRuleTester("no-role-presentation-role-on-focusable", rule, {
5+
valid: [
6+
"<button>Submit</button>",
7+
"<div role='presentation'><button tabindex='-1'>Some text</button></div>",
8+
"<div><button>Submit</button></div>",
9+
"<a href='#' tabindex='-1'>link</a>",
10+
"<button tabindex='-1' role='presentation'>Press</button>",
11+
"<div role='presentation'><a href='#' tabindex='-1'>Link</a></div>"
12+
],
13+
invalid: [
14+
{
15+
code: "<div role='presentation'><button>Submit</button></div>",
16+
errors: [{ messageId: "default" }]
17+
},
18+
{
19+
code: "<button type='button' role='presentation'>Submit</button>",
20+
errors: [{ messageId: "default" }]
21+
},
22+
{
23+
code: "<a href='#' role='presentation'>Link</a>",
24+
errors: [{ messageId: "default" }]
25+
},
26+
{
27+
code: "<span tabindex='0' role='presentation'><em>Icon</em></span>",
28+
errors: [{ messageId: "default" }]
29+
}
30+
]
31+
});
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Rule } from "eslint";
2+
3+
import {
4+
defineTemplateBodyVisitor,
5+
getElementAttributeValue,
6+
makeDocsURL
7+
} from "../utils";
8+
import hasFocusableElements from "../utils/hasFocusableElement";
9+
10+
const rule: Rule.RuleModule = {
11+
meta: {
12+
type: "problem",
13+
docs: {
14+
url: makeDocsURL("no-aria-hidden-on-focusable")
15+
},
16+
messages: {
17+
default:
18+
"Focusable/Interactive elements must not have an aria-hidden attribute."
19+
},
20+
schema: []
21+
},
22+
create(context) {
23+
return defineTemplateBodyVisitor(context, {
24+
VElement(node) {
25+
const hasAriaHidden = getElementAttributeValue(node, "aria-hidden");
26+
if (hasAriaHidden && hasFocusableElements(node)) {
27+
context.report({
28+
node: node as any,
29+
messageId: "default"
30+
});
31+
}
32+
}
33+
});
34+
}
35+
};
36+
37+
export default rule;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type { Rule } from "eslint";
2+
3+
import {
4+
defineTemplateBodyVisitor,
5+
getElementAttributeValue,
6+
makeDocsURL
7+
} from "../utils";
8+
import hasFocusableElements from "../utils/hasFocusableElement";
9+
10+
const rule: Rule.RuleModule = {
11+
meta: {
12+
type: "problem",
13+
docs: {
14+
url: makeDocsURL("no-role-presentation-on-focusable")
15+
},
16+
messages: {
17+
default:
18+
"Focusable/Interactive elements must not have a presentation role attribute."
19+
},
20+
schema: []
21+
},
22+
create(context) {
23+
return defineTemplateBodyVisitor(context, {
24+
VElement(node) {
25+
const hasRolePresentation =
26+
getElementAttributeValue(node, "role") === "presentation";
27+
if (hasRolePresentation && hasFocusableElements(node)) {
28+
context.report({
29+
node: node as any,
30+
messageId: "default"
31+
});
32+
}
33+
}
34+
});
35+
}
36+
};
37+
38+
export default rule;

src/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { default as getInteractiveRoles } from "./utils/getInteractiveRoles";
99
export { default as hasAccessibleChild } from "./utils/hasAccessibleChild";
1010
export { default as hasAriaLabel } from "./utils/hasAriaLabel";
1111
export { default as hasContent } from "./utils/hasContent";
12+
export { default as hasFocusableElement } from "./utils/hasFocusableElement";
1213
export { default as hasOnDirective } from "./utils/hasOnDirective";
1314
export { default as hasOnDirectives } from "./utils/hasOnDirectives";
1415
export { default as interactiveHandlers } from "./utils/interactiveHandlers.json";

src/utils/hasFocusableElement.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { AST } from "vue-eslint-parser";
2+
import getElementAttributeValue from "./getElementAttributeValue";
3+
import isInteractiveElement from "./isInteractiveElement";
4+
5+
function hasFocusableElements(node: AST.VElement): boolean {
6+
const tabindex = getElementAttributeValue(node, "tabindex");
7+
8+
if (isInteractiveElement(node)) {
9+
return tabindex !== "-1";
10+
}
11+
12+
if (tabindex !== null && tabindex !== "-1") {
13+
return true;
14+
}
15+
16+
return node.children.some(
17+
(child) => child.type === "VElement" && hasFocusableElements(child)
18+
);
19+
}
20+
21+
export default hasFocusableElements;

0 commit comments

Comments
 (0)