Skip to content

Commit e22aef4

Browse files
khiga8ljharb
authored andcommitted
[New] add no-aria-hidden-on-focusable rule
Raise error when aria-hidden="true" is set on focusable element. Exceptions are if `tabindex=-1`. Fixes #881
1 parent e199d17 commit e22aef4

File tree

3 files changed

+121
-0
lines changed

3 files changed

+121
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @fileoverview Enforce `aria-hidden="true"` is not used on focusable elements.
3+
* @author Kate Higa
4+
*/
5+
6+
// -----------------------------------------------------------------------------
7+
// Requirements
8+
// -----------------------------------------------------------------------------
9+
10+
import { RuleTester } from 'eslint';
11+
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
12+
import rule from '../../../src/rules/no-aria-hidden-on-focusable';
13+
// -----------------------------------------------------------------------------
14+
// Tests
15+
// -----------------------------------------------------------------------------
16+
17+
const ruleTester = new RuleTester();
18+
19+
const expectedError = {
20+
message: 'aria-hidden="true" must not be set on focusable elements.',
21+
type: 'JSXOpeningElement',
22+
};
23+
24+
ruleTester.run('no-aria-hidden-on-focusable', rule, {
25+
valid: [
26+
{ code: '<div aria-hidden="true" />;' },
27+
{ code: '<div onClick={() => void 0} aria-hidden="true" />;' },
28+
{ code: '<img aria-hidden="true" />' },
29+
{ code: '<a aria-hidden="false" href="#" />' },
30+
{ code: '<button aria-hidden="true" tabIndex="-1" />' },
31+
{ code: '<button />' },
32+
{ code: '<a href="/" />' },
33+
].map(parserOptionsMapper),
34+
invalid: [
35+
{ code: '<div aria-hidden="true" tabIndex="0" />;', errors: [expectedError] },
36+
{ code: '<input aria-hidden="true" />;', errors: [expectedError] },
37+
{ code: '<a href="/" aria-hidden="true" />', errors: [expectedError] },
38+
{ code: '<button aria-hidden="true" />', errors: [expectedError] },
39+
{ code: '<textarea aria-hidden="true" />', errors: [expectedError] },
40+
{ code: '<p tabindex="0" aria-hidden="true">text</p>;', errors: [expectedError] },
41+
].map(parserOptionsMapper),
42+
});
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# no-aria-hidden-on-focusable
2+
3+
Enforce that `aria-hidden="true"` is not set on 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+
## Rule details
8+
9+
### Succeed
10+
```jsx
11+
<div aria-hidden="true" />
12+
<img aria-hidden="true" />
13+
<a aria-hidden="false" href="#" />
14+
<button aria-hidden="true" tabIndex="-1" /> // `tabIndex=-1` removes the element from sequential focus navigation so we don't flag it.
15+
<a href="/" />
16+
<div aria-hidden="true"><a href="#"></a></div> // This is also bad but will not be handled by this rule.
17+
```
18+
19+
### Fail
20+
```jsx
21+
<div aria-hidden="true" tabIndex="0" />
22+
<input aria-hidden="true" />
23+
<a href="/" aria-hidden="true" />
24+
<button aria-hidden="true" />
25+
<textarea aria-hidden="true" />
26+
```
27+
28+
## Accessibility guidelines
29+
General best practice (reference resources)
30+
31+
### Resources
32+
33+
- [aria-hidden elements do not contain focusable elements](https://dequeuniversity.com/rules/axe/html/4.4/aria-hidden-focus)
34+
- [Element with aria-hidden has no content in sequential focus navigation](https://www.w3.org/WAI/standards-guidelines/act/rules/6cfa84/proposed/)
35+
- [MDN aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden)
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @fileoverview Enforce aria-hidden is not used on interactive elements or contain interactive elements.
3+
* @author Kate Higa
4+
*/
5+
6+
// ----------------------------------------------------------------------------
7+
// Rule Definition
8+
// ----------------------------------------------------------------------------
9+
10+
import { getProp, getPropValue } from 'jsx-ast-utils';
11+
import getElementType from '../util/getElementType';
12+
import isFocusable from '../util/isFocusable';
13+
import { generateObjSchema } from '../util/schemas';
14+
15+
const errorMessage = 'aria-hidden="true" must not be set on focusable elements.';
16+
const schema = generateObjSchema();
17+
18+
export default {
19+
meta: {
20+
docs: {
21+
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/no-aria-hidden-on-focusable.md',
22+
description: errorMessage,
23+
},
24+
schema: [schema],
25+
},
26+
27+
create(context) {
28+
const elementType = getElementType(context);
29+
return {
30+
JSXOpeningElement(node) {
31+
const { attributes } = node;
32+
const type = elementType(node);
33+
const isAriaHidden = getPropValue(getProp(attributes, 'aria-hidden')) === true;
34+
35+
if (isAriaHidden && isFocusable(type, attributes)) {
36+
context.report({
37+
node,
38+
message: errorMessage,
39+
});
40+
}
41+
},
42+
};
43+
},
44+
};

0 commit comments

Comments
 (0)