Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7e6d8e4

Browse files
committedJul 12, 2024
Added rule for prohibiting use of aria-hidden and role=presentaion on focusable elements
1 parent 4c68b62 commit 7e6d8e4

File tree

3 files changed

+158
-0
lines changed

3 files changed

+158
-0
lines changed
 
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# no-presentaion-or-aria-hidden-on-focusbable
2+
3+
Elements cannot use role='presentation' or aria-hidden='true' on focusable element. Using either of these on a focusable element or parent of a focusbale element will result in some users focusing on 'nothing'. See more in [WAI-ARIA Use in HTML](https://www.w3.org/TR/using-aria/#fourth).
4+
5+
6+
### ✔ Succeed
7+
```vue
8+
<template>
9+
<button>Press Me</button>
10+
</template>
11+
```
12+
13+
```vue
14+
<template>
15+
<div><button>Submit</button></div>
16+
</template>
17+
```
18+
19+
20+
```vue
21+
<template>
22+
<div aria-hidden='true'><span>Some text</div></div>
23+
</template>
24+
```
25+
26+
```vue
27+
<template>
28+
<button tabindex='-1' aria-hidden='true' role='presentation'>Press</button>
29+
</template>
30+
```
31+
32+
```vue
33+
<template>
34+
<div aria-hidden='true'><a href='#' tabindex='-1'>Link</a></div>
35+
</template>
36+
```
37+
38+
```vue
39+
<template>
40+
<div aria-hidden='true'><span>Some text</div></div>
41+
</template>
42+
```
43+
44+
### ❌ Fail
45+
46+
```vue
47+
<template>
48+
<button role=presentation>press me</button>
49+
</template>
50+
```
51+
52+
```vue
53+
<template>
54+
<button aria-hidden="true">press me</button>
55+
</template>
56+
```
57+
```vue
58+
<template>
59+
<a href="#" role='presentation'>press me</a>
60+
</template>
61+
```
62+
```vue
63+
<template>
64+
<div aria-hidden="true">
65+
<button>press me</button>
66+
</div>
67+
</template>
68+
```
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import rule from '../no-presentation-role-or-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'><span>Some text</div></div>",
8+
"<div><button>Submit</button></div>",
9+
"<a href='#'>link</a>",
10+
"<button tabindex='-1' aria-hidden='true' role='presentation'>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' role='presentation'>Submit</button>",
20+
errors: [{messageId: "default"}]
21+
},
22+
{
23+
code: "<button type='button' aria-hidden='true'>Submit</button>",
24+
errors: [{messageId: "default"}]
25+
},
26+
{
27+
code: "<a href='#' aria-hidden='true'>Link</a>",
28+
errors: [{messageId: "default"}]
29+
},
30+
{
31+
code: "<span tabindex='0' role='presentation'><em>Icon</em></span>",
32+
errors: [{messageId: "default"}]
33+
}
34+
]
35+
})
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Rule } from "eslint";
2+
import type { AST } from "vue-eslint-parser";
3+
4+
import { defineTemplateBodyVisitor, getElementAttributeValue, makeDocsURL } from "../utils";
5+
6+
const focusableElements = [
7+
'button',
8+
'a',
9+
'input',
10+
'select',
11+
'textarea',
12+
'[tabindex]',
13+
'[contenteditable]'
14+
];
15+
16+
const hasFocusableElements = (element: AST.VElement): boolean => {
17+
if (focusableElements.includes(element.name) || element.startTag.attributes.some(attr => focusableElements.includes(`[${attr.key.name}]`))) {
18+
if(getElementAttributeValue(element, 'tabindex') === '-1') {
19+
return false;
20+
}
21+
return true;
22+
}
23+
return element.children.some(child => child.type === `VElement` && hasFocusableElements(child));
24+
};
25+
26+
const rule: Rule.RuleModule = {
27+
meta: {
28+
type: "problem",
29+
docs: {
30+
url: makeDocsURL("no-presentation-or-aria-hidden-on-focusable")
31+
},
32+
messages: {
33+
default: "Focusable/Interactive elements must not have a presentation role or aria-hidden attribute."
34+
},
35+
schema: []
36+
},
37+
create(context) {
38+
return defineTemplateBodyVisitor(context, {
39+
VElement(node) {
40+
const hasAriaHidden = getElementAttributeValue(node, 'aria-hidden');
41+
const hasRolePresentation = getElementAttributeValue(node, 'role') === 'presentation';
42+
if (hasAriaHidden || hasRolePresentation) {
43+
if (hasFocusableElements(node)) {
44+
context.report({
45+
node: node as any,
46+
messageId: 'default',
47+
});
48+
}
49+
}
50+
},
51+
});
52+
}
53+
}
54+
55+
export default rule;

0 commit comments

Comments
 (0)
Please sign in to comment.