Skip to content

Commit

Permalink
[New] add no-aria-hidden-on-focusable rule
Browse files Browse the repository at this point in the history
Raise error when aria-hidden="true" is set on focusable element.
Exceptions are if `tabindex=-1`.

Fixes jsx-eslint#881
  • Loading branch information
khiga8 authored and ljharb committed Aug 26, 2022
1 parent e199d17 commit e22aef4
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 0 deletions.
42 changes: 42 additions & 0 deletions __tests__/src/rules/no-aria-hidden-on-focusable-test.js
@@ -0,0 +1,42 @@
/**
* @fileoverview Enforce `aria-hidden="true"` is not used on focusable elements.
* @author Kate Higa
*/

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-aria-hidden-on-focusable';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

const ruleTester = new RuleTester();

const expectedError = {
message: 'aria-hidden="true" must not be set on focusable elements.',
type: 'JSXOpeningElement',
};

ruleTester.run('no-aria-hidden-on-focusable', rule, {
valid: [
{ code: '<div aria-hidden="true" />;' },
{ code: '<div onClick={() => void 0} aria-hidden="true" />;' },
{ code: '<img aria-hidden="true" />' },
{ code: '<a aria-hidden="false" href="#" />' },
{ code: '<button aria-hidden="true" tabIndex="-1" />' },
{ code: '<button />' },
{ code: '<a href="/" />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<div aria-hidden="true" tabIndex="0" />;', errors: [expectedError] },
{ code: '<input aria-hidden="true" />;', errors: [expectedError] },
{ code: '<a href="/" aria-hidden="true" />', errors: [expectedError] },
{ code: '<button aria-hidden="true" />', errors: [expectedError] },
{ code: '<textarea aria-hidden="true" />', errors: [expectedError] },
{ code: '<p tabindex="0" aria-hidden="true">text</p>;', errors: [expectedError] },
].map(parserOptionsMapper),
});
35 changes: 35 additions & 0 deletions docs/rules/no-aria-hidden-on-focusable.md
@@ -0,0 +1,35 @@
# no-aria-hidden-on-focusable

Enforce that `aria-hidden="true"` is not set on focusable elements.

`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.

## Rule details

### Succeed
```jsx
<div aria-hidden="true" />
<img aria-hidden="true" />
<a aria-hidden="false" href="#" />
<button aria-hidden="true" tabIndex="-1" /> // `tabIndex=-1` removes the element from sequential focus navigation so we don't flag it.
<a href="/" />
<div aria-hidden="true"><a href="#"></a></div> // This is also bad but will not be handled by this rule.
```

### Fail
```jsx
<div aria-hidden="true" tabIndex="0" />
<input aria-hidden="true" />
<a href="/" aria-hidden="true" />
<button aria-hidden="true" />
<textarea aria-hidden="true" />
```

## Accessibility guidelines
General best practice (reference resources)

### Resources

- [aria-hidden elements do not contain focusable elements](https://dequeuniversity.com/rules/axe/html/4.4/aria-hidden-focus)
- [Element with aria-hidden has no content in sequential focus navigation](https://www.w3.org/WAI/standards-guidelines/act/rules/6cfa84/proposed/)
- [MDN aria-hidden](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-hidden)
44 changes: 44 additions & 0 deletions src/rules/no-aria-hidden-on-focusable.js
@@ -0,0 +1,44 @@
/**
* @fileoverview Enforce aria-hidden is not used on interactive elements or contain interactive elements.
* @author Kate Higa
*/

// ----------------------------------------------------------------------------
// Rule Definition
// ----------------------------------------------------------------------------

import { getProp, getPropValue } from 'jsx-ast-utils';
import getElementType from '../util/getElementType';
import isFocusable from '../util/isFocusable';
import { generateObjSchema } from '../util/schemas';

const errorMessage = 'aria-hidden="true" must not be set on focusable elements.';
const schema = generateObjSchema();

export default {
meta: {
docs: {
url: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/no-aria-hidden-on-focusable.md',
description: errorMessage,
},
schema: [schema],
},

create(context) {
const elementType = getElementType(context);
return {
JSXOpeningElement(node) {
const { attributes } = node;
const type = elementType(node);
const isAriaHidden = getPropValue(getProp(attributes, 'aria-hidden')) === true;

if (isAriaHidden && isFocusable(type, attributes)) {
context.report({
node,
message: errorMessage,
});
}
},
};
},
};

0 comments on commit e22aef4

Please sign in to comment.