Added logging, changed some directory structure

This commit is contained in:
2018-01-13 21:33:40 -05:00
parent f079a5f067
commit 8e72ffb917
73656 changed files with 35284 additions and 53718 deletions

View File

@@ -0,0 +1,7 @@
{
"presets": ["es2015"],
"plugins": [
"transform-object-rest-spread",
"transform-flow-strip-types",
]
}

View File

@@ -0,0 +1,3 @@
node_modules/
reports/
lib/

View File

@@ -0,0 +1,13 @@
{
extends: [
"airbnb-base",
"plugin:flowtype/recommended"
],
parser: "babel-eslint",
plugins: [
"flowtype"
],
rules: {
'no-template-curly-in-string': 'off'
}
}

View File

@@ -0,0 +1,8 @@
.DS_Store
.flowconfig
.travis.yml
coverage
node_modules
npm-debug.log
reports
yarn-error.log

View File

@@ -0,0 +1,294 @@
5.1.1 / 2017-07-03
==================
- [fix] revert v6 breaking changes unintentionally added in v5.1 (#283)
5.1.0 / 2017-06-26
==================
- [new] Support eslint v4. (#267)
- [new] `label-has-for`: add "required" option to allow customization (#240)
- [new] add `anchor-is-valid` (#224)
- [new] `interactive-supports-focus`: Split interactive supports focus into tabbable and focusable cases (#236)
- [new] `anchor-is-valid`: add `aspects` option (#251)
- [Deps] Bump aria-query to 0.7.0
5.0.3 / 2017-05-16
==================
- [fix] Remove `flow` directory from `.npmignore` to accommodate explicit imports from `v5.0.2`.
5.0.2 / 2017-05-16
==================
- [fix] Explicitly import flow types to resolve flow failures in consuming projects.
5.0.1 / 2017-05-07
==================
- [fix] Polyfill Array.includes for node < 6 support.
5.0.0 / 2017-05-05
==================
- [breaking] Refactor `img-has-alt` rule into `alt-text` rule
- [breaking] Rule `onclick-has-role` is removed. Replaced with `no-static-element-interactions` and `no-noninteractive-element-interactions`.
- [breaking] Rule `onclick-has-focus` is removed. Replaced with `interactive-supports-focus`.
- [new] - Add rule `media-has-caption` rule
- [new] - Add `ignoreNonDOM` option to `no-autofocus`.
- [new] - Add rule `no-interactive-element-to-noninteractive-role`
- [new] - Add rule `no-noninteractive-element-to-interactive-role`
- [new] - Add rule `no-noninteractive-tabindex`
- [new] - Configs split into "recommended" and "strict".
- [enhanced] - Configuration options added to `no-static-element-interactions` and `no-noninteractive-element-interactions`. Options allow for fine-tuning of elements and event handlers to check.
4.0.0 / 2017-02-04
==================
Add new rules:
- `jsx-a11y/accessible-emoji`
- `jsx-a11y/aria-activedescendant-has-tabindex`
- `jsx-a11y/iframe-has-title`
- `jsx-a11y/no-autofocus`
- `jsx-a11y/no-distracting-elements` *(breaking: consolidated no-marquee and no-blink into this rule.)*
- `jsx-a11y/no-redundant-roles`
- [fix] - redundant-alt to only check full words
- [docs] - Documentation upgrades across the board.
- [new] - Add `ignoreNonDom`
- [dev] - Add script to scaffold new rule creation.
3.0.2 / 2016-12-14
==================
- [fix] - make `aria-invalid` values true and false pass for rule `aria-proptypes`
3.0.1 / 2016-10-11
==================
- [breaking] - Update all rule schemas to accept objects. This allows a future schema expansion to not be a breaking change.
- [breaking] - All rules with schemas that accepted a string OR array, now only allows an array.
- [new] - `href-no-hash` accepts new schema property `specialLink` to check for custom `href` properties on elements. (fixes [#76](https://github.com/evcohen/eslint-plugin-jsx-a11y/issues/76))
- [breaking][fix] - `img-has-alt` now prefers `alt=""` over `role="presentation"`. You can set both, but not just `role="presentation"` by itself to ensure a11y across all devices.
Note - see [rule documentation](https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules) for updated schemas.
2.2.3 / 2016-10-08
==================
- [fix] - Add `switch` aria role.
- [devDependencies] - Updgrade dev dependencies and fix linting issues.
2.2.2 / 2016-09-12
==================
- [fix] `x-has-content` rules now pass with children prop set.
2.2.1 / 2016-08-31
==================
- [fix] Update `tablist` role to include missing property `aria-multiselectable`.
2.2.0 / 2016-08-26
==================
- [new] Add `click-events-have-key-events` rule.
- [new] Add `no-static-element-interactions` rule.
- [devDependencies] Upgrade `eslint`, `eslint-config-airbnb`, `mocha` to latest.
- [lint] Fix all new linting errors with upgrade
- [nit] Use `error` syntax over `2` syntax in recommended config.
2.1.0 / 2016-08-10
==================
- [fix] Require `aria-checked` for roles that are subclasses of `checkbox`
- [new] Add `anchor-has-content` rule.
- [refactor] Use new eslint rule syntax
- [new] Add support for custom words in `img-redundant-alt` (mainly for i18n).
2.0.1 / 2016-07-13
==================
- [fix] JSXElement support in expression handlers for prop types.
- [fix] `heading-has-content`: dangerouslySetInnerHTML will pass.
2.0.0 / 2016-07-12
==================
- [breaking] Scope `no-onchange` rule to select menu elements only.
1.5.5 / 2016-07-05
==================
- [fix] Add `eslint` v3 as a `peerDependency`.
1.5.4 / 2016-07-05
==================
- [fix] Add `eslint` as a `peerDependency`.
1.5.3 / 2016-06-16
==================
- [fix] Fix crash when ``<ELEMENT role />`` for `role-supports-aria-props`.
1.5.2 / 2016-06-16
==================
- [fix] Fix `img-redundant-alt` rule to use `getLiteralPropValue` from `jsx-ast-utils`.
1.5.1 / 2016-06-16
==================
- [fix] Fix checking for undefined in `heading-has-content` for children content.
1.5.0 / 2016-06-16
==================
- [new] Add [heading-has-content](docs/rules/heading-has-content.md) rule.
- [new] Add [html-has-lang](docs/rules/html-has-lang.md) rule.
- [new] Add [lang](docs/rules/lang.md) rule.
- [new] Add [no-marquee](docs/rules/no-marquee.md) rule.
- [new] Add [scope](docs/rules/scope.md) rule.
1.4.2 / 2016-06-10
==================
- [new] Integrate with latest `jsx-ast-utils` to use `propName` function. More support for namespaced names on attributes and elements.
1.4.1 / 2016-06-10
==================
- [fix] Handle spread props in `aria-unsupported-elements` and `role-supports-aria-props` when reporting.
1.4.0 / 2016-06-10
==================
- [dependency] Integrate [jsx-ast-utils](https://github.com/evcohen/jsx-ast-utils)
- [fix] Better error reporting for aria-unsupported-elements indicating which prop to remove.
1.3.0 / 2016-06-05
==================
- [new] Spelling suggestions for incorrect `aria-*` props
- [fix] Ensure `role` value is a string before converting to lowercase in `img-has-alt` rule.
1.2.3 / 2016-06-02
==================
- [fix] Handle dynamic `tabIndex` expression values, but still retain validation logic for literal `tabIndex` values.
1.2.2 / 2016-05-20
==================
- [fix] Fix checks involving the tabIndex attribute that do not account for integer literals
1.2.1 / 2016-05-19
==================
- [fix] Avoid testing interactivity of wrapper components with same name but different casing
as DOM elements (such as `Button` vs `button`).
1.2.0 / 2016-05-06
==================
- [new] Import all roles from DPUB-ARIA.
1.1.0 / 2016-05-06
==================
- [new] Add expression value handler for `BinaryExpression` type.
- [new] Add expression value handler for `NewExpression` type.
- [new] Add expression value handler for `ObjectExpression` type.
- [fix] Throws error when getting an expression of type without a handler function.
- This is for more graceful error handling and better issue reporting.
1.0.4 / 2016-04-28
==================
- [fix] Add expression value handler for `ConditionalExpression` type.
1.0.3 / 2016-04-25
==================
- [fix] Fix typo in recommended rules for `onclick-has-focus`.
1.0.2 / 2016-04-20
==================
- [fix] Add expression value handler for `ThisExpression` type.
1.0.1 / 2016-04-19
==================
- [fix] Fix build to copy source JSON files to build output.
1.0.0 / 2016-04-19
==================
- [breaking] Rename `img-uses-alt` to `img-has-alt`
- [breaking] Rename `onlick-uses-role` to `onclick-has-role`
- [breaking] Rename `mouse-events-map-to-key-events` to `mouse-events-have-key-events`
- [breaking] Rename `use-onblur-not-onchange` to `no-onchange`
- [breaking] Rename `label-uses-for` to `label-has-for`
- [breaking] Rename `redundant-alt` to `img-redundant-alt`
- [breaking] Rename `no-hash-href` to `href-no-hash`
- [breaking] Rename `valid-aria-role` to `aria-role`
- [new] Implement `aria-props` rule
- [new] Implement `aria-proptypes` rule
- [new] Implement `aria-unsupported-elements` rule
- [new] Implement `onclick-has-focus` rule
- [new] Implement `role-has-required-aria-props` rule
- [new] Implement `role-supports-aria-props` rule
- [new] Implement `tabindex-no-positive` rule
0.6.2 / 2016-04-08
==================
- [fix] Fix rule details for img-uses-alt: allow alt="" or role="presentation".
0.6.1 / 2016-04-07
==================
- [fix] Do not infer interactivity of components that are not low-level DOM elements.
0.6.0 / 2016-04-06
==================
- [breaking] Allow alt="" when role="presentation" on img-uses-alt rule.
- [new] More descriptive error messaging for img-uses-alt rule.
0.5.2 / 2016-04-05
==================
- [fix] Handle token lists for valid-aria-role.
0.5.1 / 2016-04-05
==================
- [fix] Handle null valued props for valid-aria-role.
0.5.0 / 2016-04-02
==================
- [new] Implement valid-aria-role rule. Based on [AX_ARIA_01](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_01)
0.4.3 / 2016-03-29
==================
- [fix] Handle LogicalExpression attribute types when extracting values. LogicalExpressions are of form `<Component prop={foo || "foobar"} />`
0.4.2 / 2016-03-24
==================
- [fix] Allow component names of form `Object.Property` i.e. `UX.Layout`
0.3.0 / 2016-03-02
==================
- [new] Implement [no-hash-href](docs/rules/no-hash-href.md) rule.
- [fix] Fixed TemplateLiteral AST value building to get more exact values from template strings.
0.2.0 / 2016-03-01
==================
- [new] Implement [redunant-alt](docs/rules/redundant-alt.md) rule.
0.1.2 / 2016-03-01
==================
- Initial pre-release.

View File

@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2016 Ethan Cohen
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,299 @@
<p align="center">
<a href="https://travis-ci.org/evcohen/eslint-plugin-jsx-a11y">
<img src="https://api.travis-ci.org/evcohen/eslint-plugin-jsx-a11y.svg?branch=master"
alt="build status">
</a>
<a href="https://npmjs.org/package/eslint-plugin-jsx-a11y">
<img src="https://img.shields.io/npm/v/eslint-plugin-jsx-a11y.svg"
alt="npm version">
</a>
<a href="https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/LICENSE.md">
<img src="https://img.shields.io/npm/l/eslint-plugin-jsx-a11y.svg"
alt="license">
</a>
<a href='https://coveralls.io/github/evcohen/eslint-plugin-jsx-a11y?branch=master'>
<img src='https://coveralls.io/repos/github/evcohen/eslint-plugin-jsx-a11y/badge.svg?branch=master' alt='Coverage Status' />
</a>
<a href='https://npmjs.org/package/eslint-plugin-jsx-a11y'>
<img src='https://img.shields.io/npm/dt/eslint-plugin-jsx-a11y.svg'
alt='Total npm downloads' />
</a>
</p>
# eslint-plugin-jsx-a11y
Static AST checker for accessibility rules on JSX elements.
## Why?
Ryan Florence built out this awesome runtime-analysis tool called [react-a11y](https://github.com/reactjs/react-a11y). It is super useful. However, since you're probably already using linting in your project, this plugin comes for free and closer to the actual development process. Pairing this plugin with an editor lint plugin, you can bake accessibility standards into your application in real-time.
**Note**: This project does not *replace* react-a11y, but can and should be used in conjunction with it. Static analysis tools cannot determine values of variables that are being placed in props before runtime, so linting will not fail if that value is undefined and/or does not pass the lint rule.
## Installation
**If you are installing this plugin via `eslint-config-airbnb`, please follow [these instructions](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb#eslint-config-airbnb-1).**
You'll first need to install [ESLint](http://eslint.org):
```sh
# npm
npm install eslint --save-dev
# yarn
yarn add eslint --dev
```
Next, install `eslint-plugin-jsx-a11y`:
```sh
# npm
npm install eslint-plugin-jsx-a11y --save-dev
# yarn
yarn add eslint-plugin-jsx-a11y --dev
```
**Note:** If you installed ESLint globally (using the `-g` flag in npm, or the `global` prefix in yarn) then you must also install `eslint-plugin-jsx-a11y` globally.
## Usage
Add `jsx-a11y` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
```json
{
"plugins": [
"jsx-a11y"
]
}
```
Then configure the rules you want to use under the rules section.
```json
{
"rules": {
"jsx-a11y/rule-name": 2
}
}
```
You can also enable all the recommended or strict rules at once.
Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`:
```json
{
"extends": [
"plugin:jsx-a11y/recommended"
]
}
```
## Supported Rules
- [accessible-emoji](docs/rules/accessible-emoji.md): Enforce emojis are wrapped in <span> and provide screenreader access.
- [alt-text](docs/rules/alt-text.md): Enforce all elements that require alternative text have meaningful information to relay back to end user.
- [anchor-has-content](docs/rules/anchor-has-content.md): Enforce all anchors to contain accessible content.
- [anchor-is-valid](docs/rules/anchor-is-valid.md): Enforce all anchors are valid, navigable elements.
- [aria-activedescendant-has-tabindex](docs/rules/aria-activedescendant-has-tabindex.md): Enforce elements with aria-activedescendant are tabbable.
- [aria-props](docs/rules/aria-props.md): Enforce all `aria-*` props are valid.
- [aria-proptypes](docs/rules/aria-proptypes.md): Enforce ARIA state and property values are valid.
- [aria-role](docs/rules/aria-role.md): Enforce that elements with ARIA roles must use a valid, non-abstract ARIA role.
- [aria-unsupported-elements](docs/rules/aria-unsupported-elements.md): Enforce that elements that do not support ARIA roles, states, and properties do not have those attributes.
- [click-events-have-key-events](docs/rules/click-events-have-key-events.md): Enforce a clickable non-interactive element has at least one keyboard event listener.
- [heading-has-content](docs/rules/heading-has-content.md): Enforce heading (`h1`, `h2`, etc) elements contain accessible content.
- [href-no-hash](docs/rules/href-no-hash.md): Enforce an anchor element's `href` prop value is not just `#`.
- [html-has-lang](docs/rules/html-has-lang.md): Enforce `<html>` element has `lang` prop.
- [iframe-has-title](docs/rules/iframe-has-title.md): Enforce iframe elements have a title attribute.
- [img-redundant-alt](docs/rules/img-redundant-alt.md): Enforce `<img>` alt prop does not contain the word "image", "picture", or "photo".
- [interactive-supports-focus](docs/rules/interactive-supports-focus.md): Enforce that elements with interactive handlers like `onClick` must be focusable.
- [label-has-for](docs/rules/label-has-for.md): Enforce that `<label>` elements have the `htmlFor` prop.
- [lang](docs/rules/lang.md): Enforce lang attribute has a valid value.
- [media-has-caption](docs/rules/media-has-caption.md): Enforces that `<audio>` and `<video>` elements must have a `<track>` for captions.
- [mouse-events-have-key-events](docs/rules/mouse-events-have-key-events.md): Enforce that `onMouseOver`/`onMouseOut` are accompanied by `onFocus`/`onBlur` for keyboard-only users.
- [no-access-key](docs/rules/no-access-key.md): Enforce that the `accessKey` prop is not used on any element to avoid complications with keyboard commands used by a screenreader.
- [no-autofocus](docs/rules/no-autofocus.md): Enforce autoFocus prop is not used.
- [no-distracting-elements](docs/rules/no-distracting-elements.md): Enforce distracting elements are not used.
- [no-interactive-element-to-noninteractive-role](docs/rules/no-interactive-element-to-noninteractive-role.md): Interactive elements should not be assigned non-interactive roles.
- [no-noninteractive-element-interactions](docs/rules/no-noninteractive-element-interactions.md): Non-interactive elements should not be assigned mouse or keyboard event listeners.
- [no-noninteractive-element-to-interactive-role](docs/rules/no-noninteractive-element-to-interactive-role.md): Non-interactive elements should not be assigned interactive roles.
- [no-noninteractive-tabindex](docs/rules/no-noninteractive-tabindex.md): `tabIndex` should only be declared on interactive elements.
- [no-onchange](docs/rules/no-onchange.md): Enforce usage of `onBlur` over `onChange` on select menus for accessibility.
- [no-redundant-roles](docs/rules/no-redundant-roles.md): Enforce explicit role property is not the same as implicit/default role property on element.
- [no-static-element-interactions](docs/rules/no-static-element-interactions.md): Enforce that non-interactive, visible elements (such as `<div>`) that have click handlers use the role attribute.
- [role-has-required-aria-props](docs/rules/role-has-required-aria-props.md): Enforce that elements with ARIA roles must have all required attributes for that role.
- [role-supports-aria-props](docs/rules/role-supports-aria-props.md): Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`.
- [scope](docs/rules/scope.md): Enforce `scope` prop is only used on `<th>` elements.
- [tabindex-no-positive](docs/rules/tabindex-no-positive.md): Enforce `tabIndex` value is not greater than zero.
### Difference between 'recommended' and 'strict' mode
Rule | Recommended | Strict
------------ | ------------- | -------------
[accessible-emoji](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/accessible-emoji.md) | error | error
[alt-text](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/alt-text.md) | error | error
[anchor-has-content](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/anchor-has-content.md) | error | error
[aria-activedescendant-has-tabindex](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-activedescendant-has-tabindex.md) | error | error
[aria-props](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-props.md) | error | error
[aria-proptypes](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-proptypes.md) | error | error
[aria-role](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-role.md) | error | error
[aria-unsupported-elements](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/aria-unsupported-elements.md) | error | error
[click-events-have-key-events](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/click-events-have-key-events.md) | error | error
[heading-has-content](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/heading-has-content.md) | error | error
[href-no-hash](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/href-no-hash.md) | error | error
[html-has-lang](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/html-has-lang.md) | error | error
[iframe-has-title](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/iframe-has-title.md) | error | error
[img-redundant-alt](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/img-redundant-alt.md) | error | error
[interactive-supports-focus](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/interactive-supports-focus.md) | error | error
[label-has-for](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-for.md) | error | error
[media-has-caption](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/media-has-caption.md) | error | error
[mouse-events-have-key-events](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/mouse-events-have-key-events.md) | error | error
[no-access-key](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-access-key.md) | error | error
[no-autofocus](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md) | error | error
[no-distracting-elements](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-distracting-elements.md) | error | error
[no-interactive-element-to-noninteractive-role](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-interactive-element-to-noninteractive-role.md) | error, with options | error
[no-noninteractive-element-interactions](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-noninteractive-element-interactions.md) | error, with options | error
[no-noninteractive-element-to-interactive-role](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-noninteractive-element-to-interactive-role.md) | error, with options | error
[no-noninteractive-tabindex](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-noninteractive-tabindex.md) | error, with options | error
[no-onchange](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-onchange.md) | error | error
[no-redundant-roles](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-redundant-roles.md) | error | error
[no-static-element-interactions](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md) | error, with options | error
[role-has-required-aria-props](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/role-has-required-aria-props.md) | error | error
[role-supports-aria-props](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/role-supports-aria-props.md) | error | error
[scope](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/scope.md) | error, with options | error
[tabindex-no-positive](https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/tabindex-no-positive.md) | error | error
The following rules have extra options when in *recommended* mode:
#### no-interactive-element-to-noninteractive-role
```
'jsx-a11y/no-interactive-element-to-noninteractive-role': [
'error',
{
tr: ['none', 'presentation'],
},
]
```
#### no-noninteractive-element-interactions
```
'jsx-a11y/no-noninteractive-element-interactions': [
'error',
{
handlers: [
'onClick',
'onMouseDown',
'onMouseUp',
'onKeyPress',
'onKeyDown',
'onKeyUp',
],
},
]
```
#### no-noninteractive-element-to-interactive-role
```
'jsx-a11y/no-noninteractive-element-to-interactive-role': [
'error',
{
ul: [
'listbox',
'menu',
'menubar',
'radiogroup',
'tablist',
'tree',
'treegrid',
],
ol: [
'listbox',
'menu',
'menubar',
'radiogroup',
'tablist',
'tree',
'treegrid',
],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],
},
]
```
#### no-noninteractive-tabindex
```
'jsx-a11y/no-noninteractive-tabindex': [
'error',
{
tags: [],
roles: ['tabpanel'],
},
]
```
#### no-static-element-interactions
```
'jsx-a11y/no-noninteractive-element-interactions': [
'error',
{
handlers: [
'onClick',
'onMouseDown',
'onMouseUp',
'onKeyPress',
'onKeyDown',
'onKeyUp',
],
},
]
```
## Creating a new rule
If you are developing new rules for this project, you can use the `create-rule`
script to scaffold the new files.
```
$ ./scripts/create-rule.js my-new-rule
```
## Some background on WAI-ARIA, the AX Tree and Browsers
### Accessibility API
An operating system will provide an accessibility API that maps application state and content onto input/output controllers such as a screen reader, braille device, keyboard, etc.
These APIs were developed as computer interfaces shifted from buffers (which are text based and inherently quite accessible) to graphical user interfaces (GUIs). The first attempts to make GUIs accessible involved raster image parsing to recognize characters, words, etc. This information was stored in a parallel buffer and made accessible to assistive technology (AT) devices.
As GUIs became more complex, the raster parsing approach became untenable. Accessibility APIs were developed to replace them. Check out [NSAccessibility (AXAPI)](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Protocols/NSAccessibility_Protocol/index.html) for an example. See [Core Accessibility API Mappings 1.1](https://www.w3.org/TR/core-aam-1.1/) for more details.
### Browsers
Browsers support an Accessibility API on a per operating system basis. For instance Firefox implements the MSAA accessibility API on Windows, but does not implement the AXAPI on OSX.
### The Accessibility (AX) Tree & DOM
From the [W3 Core Accessibility API Mappings 1.1](https://www.w3.org/TR/core-aam-1.1/#intro_treetypes)
> The accessibility tree and the DOM tree are parallel structures. Roughly speaking the accessibility tree is a subset of the DOM tree. It includes the user interface objects of the user agent and the objects of the document. Accessible objects are created in the accessibility tree for every DOM element that should be exposed to an assistive technology, either because it may fire an accessibility event or because it has a property, relationship or feature which needs to be exposed. Generally if something can be trimmed out it will be, for reasons of performance and simplicity. For example, a <span> with just a style change and no semantics may not get its own accessible object, but the style change will be exposed by other means.
Browser vendors are beginning to expose the AX Tree through inspection tools. Chrome has an experiment available to enable their inspection tool.
You can also see a text-based version of the AX Tree in Chrome in the stable release version.
#### Viewing the AX Tree in Chrome
1. Navigate to `chrome://accessibility/` in Chrome.
1. Toggle the `accessibility off` link for any tab that you want to inspect.
1. A link labeled `show accessibility tree` will appear; click this link.
1. Balk at the wall of text that gets displayed, but then regain your conviction.
1. Use the browser's find command to locate strings and values in the wall of text.
### Pulling it all together
A browser constructs an AX Tree as a subset of the DOM. ARIA heavily informs the properties of this AX Tree. This AX Tree is exposed to the system level Accessibility API which mediates assistive technology agents.
We model ARIA in the [aria-query](https://github.com/a11yance/aria-query) project. We model AXObjects (that comprise the AX Tree) in the [axobject-query](https://github.com/A11yance/axobject-query) project. The goal of the WAI-ARIA specification is to be a complete complete declarative interface to the AXObject model. The [in-draft 1.2 version](https://github.com/w3c/aria/issues?q=is%3Aissue+is%3Aopen+label%3A%22ARIA+1.2%22) is moving towards this goal. But until then, we must consider the semantics constructs affored by ARIA as well as those afforded by the AXObject model (AXAPI) in order to determine how HTML can be used to express user interface affordances to assistive technology users.
## License
eslint-plugin-jsx-a11y is licensed under the [MIT License](LICENSE.md).

View File

@@ -0,0 +1,6 @@
export default function IdentifierMock(ident) {
return {
type: 'Identifier',
name: ident,
};
}

View File

@@ -0,0 +1,24 @@
import toAST from 'to-ast'; // eslint-disable-line import/no-extraneous-dependencies
import JSXExpressionContainerMock from './JSXExpressionContainerMock';
export default function JSXAttributeMock(prop, value, isExpressionContainer = false) {
let astValue;
if (value && value.type !== undefined) {
astValue = value;
} else {
astValue = toAST(value);
}
let attributeValue = astValue;
if (isExpressionContainer || astValue.type !== 'Literal') {
attributeValue = JSXExpressionContainerMock(astValue);
}
return {
type: 'JSXAttribute',
name: {
type: 'JSXIdentifier',
name: prop,
},
value: attributeValue,
};
}

View File

@@ -0,0 +1,14 @@
export default function JSXElementMock(tagName, attributes, children = []) {
return {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: {
type: 'JSXIdentifier',
name: tagName,
},
attributes,
},
children,
};
}

View File

@@ -0,0 +1,6 @@
export default function JSXExpressionContainerMock(exp) {
return {
type: 'JSXExpressionContainer',
expression: exp,
};
}

View File

@@ -0,0 +1,206 @@
/**
* @flow
*/
import { dom, roles } from 'aria-query';
import includes from 'array-includes';
import JSXAttributeMock from './JSXAttributeMock';
import JSXElementMock from './JSXElementMock';
const domElements = [...dom.keys()];
const roleNames = [...roles.keys()];
const interactiveElementsMap = {
a: [{ prop: 'href', value: '#' }],
area: [{ prop: 'href', value: '#' }],
audio: [],
button: [],
canvas: [],
embed: [],
link: [],
input: [],
'input[type="button"]': [{ prop: 'type', value: 'button' }],
'input[type="checkbox"]': [{ prop: 'type', value: 'checkbox' }],
'input[type="color"]': [{ prop: 'type', value: 'color' }],
'input[type="date"]': [{ prop: 'type', value: 'date' }],
'input[type="datetime"]': [{ prop: 'type', value: 'datetime' }],
'input[type="email"]': [{ prop: 'type', value: 'email' }],
'input[type="file"]': [{ prop: 'type', value: 'file' }],
'input[type="image"]': [{ prop: 'type', value: 'image' }],
'input[type="month"]': [{ prop: 'type', value: 'month' }],
'input[type="number"]': [{ prop: 'type', value: 'number' }],
'input[type="password"]': [{ prop: 'type', value: 'password' }],
'input[type="radio"]': [{ prop: 'type', value: 'radio' }],
'input[type="range"]': [{ prop: 'type', value: 'range' }],
'input[type="reset"]': [{ prop: 'type', value: 'reset' }],
'input[type="search"]': [{ prop: 'type', value: 'search' }],
'input[type="submit"]': [{ prop: 'type', value: 'submit' }],
'input[type="tel"]': [{ prop: 'type', value: 'tel' }],
'input[type="text"]': [{ prop: 'type', value: 'text' }],
'input[type="time"]': [{ prop: 'type', value: 'time' }],
'input[type="url"]': [{ prop: 'type', value: 'url' }],
'input[type="week"]': [{ prop: 'type', value: 'week' }],
menuitem: [],
option: [],
select: [],
// Whereas ARIA makes a distinction between cell and gridcell, the AXObject
// treats them both as CellRole and since gridcell is interactive, we consider
// cell interactive as well.
// td: [],
th: [],
tr: [],
textarea: [],
video: [],
};
const nonInteractiveElementsMap = {
abbr: [],
article: [],
blockquote: [],
br: [],
caption: [],
dd: [],
details: [],
dfn: [],
dialog: [],
dir: [],
dl: [],
dt: [],
fieldset: [],
figcaption: [],
figure: [],
footer: [],
form: [],
frame: [],
h1: [],
h2: [],
h3: [],
h4: [],
h5: [],
h6: [],
hr: [],
iframe: [],
img: [],
label: [],
legend: [],
li: [],
main: [],
mark: [],
marquee: [],
menu: [],
meter: [],
nav: [],
ol: [],
p: [],
pre: [],
progress: [],
ruby: [],
table: [],
tbody: [],
td: [],
tfoot: [],
thead: [],
time: [],
ul: [],
};
const indeterminantInteractiveElementsMap = domElements.reduce(
(accumulator: { [key: string]: Array<any> }, name: string): { [key: string]: Array<any> } => ({
...accumulator,
[name]: [],
}),
{},
);
Object.keys(interactiveElementsMap)
.concat(Object.keys(nonInteractiveElementsMap))
.forEach((name: string) => delete indeterminantInteractiveElementsMap[name]);
const abstractRoles = roleNames.filter(role => roles.get(role).abstract);
const nonAbstractRoles = roleNames.filter(role => !roles.get(role).abstract);
const interactiveRoles = []
.concat(
roleNames,
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
'toolbar',
)
.filter(role => !roles.get(role).abstract)
.filter(role => roles.get(role).superClass.some(klasses => includes(klasses, 'widget')));
const nonInteractiveRoles = roleNames
.filter(role => !roles.get(role).abstract)
.filter(role => !roles.get(role).superClass.some(klasses => includes(klasses, 'widget')))
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
.filter(role => !includes(['toolbar'], role));
export function genElementSymbol(openingElement: Object) {
return (
openingElement.name.name +
(openingElement.attributes.length > 0
? `${openingElement.attributes
.map(attr => `[${attr.name.name}="${attr.value.value}"]`)
.join('')}`
: '')
);
}
export function genInteractiveElements() {
return Object.keys(interactiveElementsMap).map((elementSymbol) => {
const bracketIndex = elementSymbol.indexOf('[');
let name = elementSymbol;
if (bracketIndex > -1) {
name = elementSymbol.slice(0, bracketIndex);
}
const attributes = interactiveElementsMap[elementSymbol].map(({ prop, value }) =>
JSXAttributeMock(prop, value),
);
return JSXElementMock(name, attributes);
});
}
export function genInteractiveRoleElements() {
return [...interactiveRoles, 'button article', 'fakerole button article'].map(value =>
JSXElementMock('div', [JSXAttributeMock('role', value)]),
);
}
export function genNonInteractiveElements() {
return Object.keys(nonInteractiveElementsMap).map((elementSymbol) => {
const bracketIndex = elementSymbol.indexOf('[');
let name = elementSymbol;
if (bracketIndex > -1) {
name = elementSymbol.slice(0, bracketIndex);
}
const attributes = nonInteractiveElementsMap[elementSymbol].map(({ prop, value }) =>
JSXAttributeMock(prop, value),
);
return JSXElementMock(name, attributes);
});
}
export function genNonInteractiveRoleElements() {
return [...nonInteractiveRoles, 'article button', 'fakerole article button'].map(value =>
JSXElementMock('div', [JSXAttributeMock('role', value)]),
);
}
export function genAbstractRoleElements() {
return abstractRoles.map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}
export function genNonAbstractRoleElements() {
return nonAbstractRoles.map(value => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}
export function genIndeterminantInteractiveElements() {
return Object.keys(indeterminantInteractiveElementsMap).map((name) => {
const attributes = indeterminantInteractiveElementsMap[name].map(({ prop, value }) =>
JSXAttributeMock(prop, value),
);
return JSXElementMock(name, attributes);
});
}

View File

@@ -0,0 +1,23 @@
const defaultParserOptions = {
ecmaVersion: 6,
ecmaFeatures: {
jsx: true,
},
};
export default function parserOptionsMapper({
code,
errors,
options,
parserOptions = {},
}) {
return {
code,
errors,
options,
parserOptions: {
...defaultParserOptions,
...parserOptions,
},
};
}

View File

@@ -0,0 +1,29 @@
/**
* @flow
*/
type ESLintTestRunnerTestCase = {
code: string,
errors: ?Array<{
message: string,
type: string,
}>,
options: ?Array<mixed>,
parserOptions: ?Array<mixed>,
};
export default function ruleOptionsMapperFactory(
ruleOptions: Array<mixed> = [],
) {
return ({
code,
errors,
options,
parserOptions,
}: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => ({
code,
errors,
options: (options || []).concat(ruleOptions),
parserOptions,
});
}

View File

@@ -0,0 +1,39 @@
/* eslint-env jest */
/* eslint global-require: 0 */
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import plugin from '../src';
const rules = fs.readdirSync(path.resolve(__dirname, '../src/rules/'))
.map(f => path.basename(f, '.js'));
describe('all rule files should be exported by the plugin', () => {
rules.forEach((ruleName) => {
it(`should export ${ruleName}`, () => {
assert.equal(
plugin.rules[ruleName],
require(path.join('../src/rules', ruleName)) // eslint-disable-line
);
});
});
});
describe('configurations', () => {
it('should export a \'recommended\' configuration', () => {
assert(plugin.configs.recommended);
});
});
describe('schemas', () => {
rules.forEach((ruleName) => {
it(`${ruleName} should export a schema with type object`, () => {
const rule = require(path.join('../src/rules', ruleName)); // eslint-disable-line
const schema = rule.meta && rule.meta.schema && rule.meta.schema[0];
const { type } = schema;
assert.deepEqual(type, 'object');
});
});
});

View File

@@ -0,0 +1,46 @@
/* eslint-env jest */
/**
* @fileoverview Enforce <marquee> elements are not used.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/accessible-emoji';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Emojis should be wrapped in <span>, have role="img", and have an accessible description with aria-label or aria-labelledby.',
type: 'JSXOpeningElement',
};
ruleTester.run('accessible-emoji', rule, {
valid: [
{ code: '<div />;' },
{ code: '<span />' },
{ code: '<span>No emoji here!</span>' },
{ code: '<span role="img" aria-label="Panda face">🐼</span>' },
{ code: '<span role="img" aria-label="Snowman">&#9731;</span>' },
{ code: '<span role="img" aria-labelledby="id1">🐼</span>' },
{ code: '<span role="img" aria-labelledby="id1">&#9731;</span>' },
{ code: '<span role="img" aria-labelledby="id1" aria-label="Snowman">&#9731;</span>' },
{ code: '<span>{props.emoji}</span>' },
].map(parserOptionsMapper),
invalid: [
{ code: '<span>🐼</span>', errors: [expectedError] },
{ code: '<span>foo🐼bar</span>', errors: [expectedError] },
{ code: '<span>foo 🐼 bar</span>', errors: [expectedError] },
{ code: '<i role="img" aria-label="Panda face">🐼</i>', errors: [expectedError] },
{ code: '<i role="img" aria-labelledby="id1">🐼</i>', errors: [expectedError] },
{ code: '<Foo>🐼</Foo>', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,248 @@
/* eslint-env jest */
/**
* @fileoverview Enforce all elements that require alternative text have it.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/alt-text';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const missingPropError = type => ({
message: `${type} elements must have an alt prop, either with meaningful text, or an empty string for decorative images.`,
type: 'JSXOpeningElement',
});
const altValueError = type => ({
message: `Invalid alt value for ${type}. \
Use alt="" for presentational images.`,
type: 'JSXOpeningElement',
});
const preferAltError = () => ({
message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.',
type: 'JSXOpeningElement',
});
const objectError = 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.';
const areaError = 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.';
const inputImageError = '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.';
const array = [{
img: ['Thumbnail', 'Image'],
object: ['Object'],
area: ['Area'],
'input[type="image"]': ['InputImage'],
}];
ruleTester.run('alt-text', rule, {
valid: [
// DEFAULT ELEMENT 'img' TESTS
{ code: '<img alt="foo" />;' },
{ code: '<img alt={"foo"} />;' },
{ code: '<img alt={alt} />;' },
{ code: '<img ALT="foo" />;' },
{ code: '<img ALT={`This is the ${alt} text`} />;' },
{ code: '<img ALt="foo" />;' },
{ code: '<img alt="foo" salt={undefined} />;' },
{ code: '<img {...this.props} alt="foo" />' },
{ code: '<a />' },
{ code: '<div />' },
{ code: '<img alt={function(e) {} } />' },
{ code: '<div alt={function(e) {} } />' },
{ code: '<img alt={() => void 0} />' },
{ code: '<IMG />' },
{ code: '<UX.Layout>test</UX.Layout>' },
{ code: '<img alt={alt || "Alt text" } />' },
{ code: '<img alt={photo.caption} />;' },
{ code: '<img alt={bar()} />;' },
{ code: '<img alt={foo.bar || ""} />' },
{ code: '<img alt={bar() || ""} />' },
{ code: '<img alt={foo.bar() || ""} />' },
{ code: '<img alt="" />' },
{ code: '<img alt={`${undefined}`} />' },
{ code: '<img alt=" " />' },
{ code: '<img alt="" role="presentation" />' },
{ code: '<img alt="" role="none" />' },
{ code: '<img alt="" role={`presentation`} />' },
{ code: '<img alt="" role={"presentation"} />' },
{ code: '<img alt="this is lit..." role="presentation" />' },
{ code: '<img alt={error ? "not working": "working"} />' },
{ code: '<img alt={undefined ? "working": "not working"} />' },
{ code: '<img alt={plugin.name + " Logo"} />' },
// DEFAULT <object> TESTS
{ code: '<object aria-label="foo" />' },
{ code: '<object aria-labelledby="id1" />' },
{ code: '<object>Foo</object>' },
{ code: '<object><p>This is descriptive!</p></object>' },
{ code: '<Object />' },
{ code: '<object title="An object" />' },
// DEFAULT <area> TESTS
{ code: '<area aria-label="foo" />' },
{ code: '<area aria-labelledby="id1" />' },
{ code: '<area alt="" />' },
{ code: '<area alt="This is descriptive!" />' },
{ code: '<area alt={altText} />' },
{ code: '<Area />' },
// DEFAULT <input type="image"> TESTS
{ code: '<input />' },
{ code: '<input type="foo" />' },
{ code: '<input type="image" aria-label="foo" />' },
{ code: '<input type="image" aria-labelledby="id1" />' },
{ code: '<input type="image" alt="" />' },
{ code: '<input type="image" alt="This is descriptive!" />' },
{ code: '<input type="image" alt={altText} />' },
{ code: '<InputImage />' },
// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
{ code: '<Thumbnail alt="foo" />;', options: array },
{ code: '<Thumbnail alt={"foo"} />;', options: array },
{ code: '<Thumbnail alt={alt} />;', options: array },
{ code: '<Thumbnail ALT="foo" />;', options: array },
{ code: '<Thumbnail ALT={`This is the ${alt} text`} />;', options: array },
{ code: '<Thumbnail ALt="foo" />;', options: array },
{ code: '<Thumbnail alt="foo" salt={undefined} />;', options: array },
{ code: '<Thumbnail {...this.props} alt="foo" />', options: array },
{ code: '<thumbnail />', options: array },
{ code: '<Thumbnail alt={function(e) {} } />', options: array },
{ code: '<div alt={function(e) {} } />', options: array },
{ code: '<Thumbnail alt={() => void 0} />', options: array },
{ code: '<THUMBNAIL />', options: array },
{ code: '<Thumbnail alt={alt || "foo" } />', options: array },
{ code: '<Image alt="foo" />;', options: array },
{ code: '<Image alt={"foo"} />;', options: array },
{ code: '<Image alt={alt} />;', options: array },
{ code: '<Image ALT="foo" />;', options: array },
{ code: '<Image ALT={`This is the ${alt} text`} />;', options: array },
{ code: '<Image ALt="foo" />;', options: array },
{ code: '<Image alt="foo" salt={undefined} />;', options: array },
{ code: '<Image {...this.props} alt="foo" />', options: array },
{ code: '<image />', options: array },
{ code: '<Image alt={function(e) {} } />', options: array },
{ code: '<div alt={function(e) {} } />', options: array },
{ code: '<Image alt={() => void 0} />', options: array },
{ code: '<IMAGE />', options: array },
{ code: '<Image alt={alt || "foo" } />', options: array },
{ code: '<Object aria-label="foo" />', options: array },
{ code: '<Object aria-labelledby="id1" />', options: array },
{ code: '<Object>Foo</Object>', options: array },
{ code: '<Object><p>This is descriptive!</p></Object>', options: array },
{ code: '<Object title="An object" />', options: array },
{ code: '<Area aria-label="foo" />', options: array },
{ code: '<Area aria-labelledby="id1" />', options: array },
{ code: '<Area alt="" />', options: array },
{ code: '<Area alt="This is descriptive!" />', options: array },
{ code: '<Area alt={altText} />', options: array },
{ code: '<InputImage aria-label="foo" />', options: array },
{ code: '<InputImage aria-labelledby="id1" />', options: array },
{ code: '<InputImage alt="" />', options: array },
{ code: '<InputImage alt="This is descriptive!" />', options: array },
{ code: '<InputImage alt={altText} />', options: array },
].map(parserOptionsMapper),
invalid: [
// DEFAULT ELEMENT 'img' TESTS
{ code: '<img />;', errors: [missingPropError('img')] },
{ code: '<img alt />;', errors: [altValueError('img')] },
{ code: '<img alt={undefined} />;', errors: [altValueError('img')] },
{ code: '<img src="xyz" />', errors: [missingPropError('img')] },
{ code: '<img role />', errors: [missingPropError('img')] },
{ code: '<img {...this.props} />', errors: [missingPropError('img')] },
{ code: '<img alt={false || false} />', errors: [altValueError('img')] },
{ code: '<img alt={undefined} role="presentation" />;', errors: [altValueError('img')] },
{ code: '<img alt role="presentation" />;', errors: [altValueError('img')] },
{ code: '<img role="presentation" />;', errors: [preferAltError()] },
{ code: '<img role="none" />;', errors: [preferAltError()] },
// DEFAULT ELEMENT 'object' TESTS
{ code: '<object />', errors: [objectError] },
{ code: '<object><div aria-hidden /></object>', errors: [objectError] },
{ code: '<object title={undefined} />', errors: [objectError] },
// DEFAULT ELEMENT 'area' TESTS
{ code: '<area />', errors: [areaError] },
{ code: '<area alt />', errors: [areaError] },
{ code: '<area alt={undefined} />', errors: [areaError] },
{ code: '<area src="xyz" />', errors: [areaError] },
{ code: '<area {...this.props} />', errors: [areaError] },
// DEFAULT ELEMENT 'input type="image"' TESTS
{ code: '<input type="image" />', errors: [inputImageError] },
{ code: '<input type="image" alt />', errors: [inputImageError] },
{ code: '<input type="image" alt={undefined} />', errors: [inputImageError] },
{ code: '<input type="image">Foo</input>', errors: [inputImageError] },
{ code: '<input type="image" {...this.props} />', errors: [inputImageError] },
// CUSTOM ELEMENT TESTS FOR ARRAY OPTION TESTS
{
code: '<Thumbnail />;',
errors: [missingPropError('Thumbnail')],
options: array,
},
{
code: '<Thumbnail alt />;',
errors: [altValueError('Thumbnail')],
options: array,
},
{
code: '<Thumbnail alt={undefined} />;',
errors: [altValueError('Thumbnail')],
options: array,
},
{
code: '<Thumbnail src="xyz" />',
errors: [missingPropError('Thumbnail')],
options: array,
},
{
code: '<Thumbnail {...this.props} />',
errors: [missingPropError('Thumbnail')],
options: array,
},
{ code: '<Image />;', errors: [missingPropError('Image')], options: array },
{ code: '<Image alt />;', errors: [altValueError('Image')], options: array },
{
code: '<Image alt={undefined} />;',
errors: [altValueError('Image')],
options: array,
},
{
code: '<Image src="xyz" />',
errors: [missingPropError('Image')],
options: array,
},
{
code: '<Image {...this.props} />',
errors: [missingPropError('Image')],
options: array,
},
{ code: '<Object />', errors: [objectError], options: array },
{ code: '<Object><div aria-hidden /></Object>', errors: [objectError], options: array },
{ code: '<Object title={undefined} />', errors: [objectError], options: array },
{ code: '<Area />', errors: [areaError], options: array },
{ code: '<Area alt />', errors: [areaError], options: array },
{ code: '<Area alt={undefined} />', errors: [areaError], options: array },
{ code: '<Area src="xyz" />', errors: [areaError], options: array },
{ code: '<Area {...this.props} />', errors: [areaError], options: array },
{ code: '<InputImage />', errors: [inputImageError], options: array },
{ code: '<InputImage alt />', errors: [inputImageError], options: array },
{ code: '<InputImage alt={undefined} />', errors: [inputImageError], options: array },
{ code: '<InputImage>Foo</InputImage>', errors: [inputImageError], options: array },
{ code: '<InputImage {...this.props} />', errors: [inputImageError], options: array },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,41 @@
/* eslint-env jest */
/**
* @fileoverview Enforce anchor elements to contain accessible content.
* @author Lisa Ring & Niklas Holmberg
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/anchor-has-content';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Anchors must have content and the content must be accessible by a screen reader.',
type: 'JSXOpeningElement',
};
ruleTester.run('anchor-has-content', rule, {
valid: [
{ code: '<div />;' },
{ code: '<a>Foo</a>' },
{ code: '<a><Bar /></a>' },
{ code: '<a>{foo}</a>' },
{ code: '<a>{foo.bar}</a>' },
{ code: '<a dangerouslySetInnerHTML={{ __html: "foo" }} />' },
{ code: '<a children={children} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<a />', errors: [expectedError] },
{ code: '<a><Bar aria-hidden /></a>', errors: [expectedError] },
{ code: '<a>{undefined}</a>', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,540 @@
/* eslint-env jest */
/**
* @fileoverview Performs validity check on anchor hrefs. Warns when anchors are used as buttons.
* @author Almero Steyn
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/anchor-is-valid';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const preferButtonErrorMessage = 'Anchor used as a button. ' +
'Anchors are primarily expected to navigate. ' +
'Use the button element instead.';
const noHrefErrorMessage = 'The href attribute is required on an anchor. ' +
'Provide a valid, navigable address as the href value.';
const invalidHrefErrorMessage = 'The href attribute requires a valid address. ' +
'Provide a valid, navigable address as the href value.';
const preferButtonexpectedError = {
message: preferButtonErrorMessage,
type: 'JSXOpeningElement',
};
const noHrefexpectedError = {
message: noHrefErrorMessage,
type: 'JSXOpeningElement',
};
const invalidHrefexpectedError = {
message: invalidHrefErrorMessage,
type: 'JSXOpeningElement',
};
const components = [{
components: ['Anchor', 'Link'],
}];
const specialLink = [{
specialLink: ['hrefLeft', 'hrefRight'],
}];
const noHrefAspect = [{
aspects: ['noHref'],
}];
const invalidHrefAspect = [{
aspects: ['invalidHref'],
}];
const preferButtonAspect = [{
aspects: ['preferButton'],
}];
const noHrefInvalidHrefAspect = [{
aspects: ['noHref', 'invalidHref'],
}];
const noHrefPreferButtonAspect = [{
aspects: ['noHref', 'preferButton'],
}];
const preferButtonInvalidHrefAspect = [{
aspects: ['preferButton', 'invalidHref'],
}];
const componentsAndSpecialLink = [{
components: ['Anchor'],
specialLink: ['hrefLeft'],
}];
const componentsAndSpecialLinkAndInvalidHrefAspect = [{
components: ['Anchor'],
specialLink: ['hrefLeft'],
aspects: ['invalidHref'],
}];
const componentsAndSpecialLinkAndNoHrefAspect = [{
components: ['Anchor'],
specialLink: ['hrefLeft'],
aspects: ['noHref'],
}];
ruleTester.run('anchor-is-valid', rule, {
valid: [
// DEFAULT ELEMENT 'a' TESTS
{ code: '<a {...props} />' },
{ code: '<a href="foo" />' },
{ code: '<a href={foo} />' },
{ code: '<a href="/foo" />' },
{ code: '<a href="https://foo.bar.com" />' },
{ code: '<div href="foo" />' },
{ code: '<a href={`#foo`}/>' },
{ code: '<a href={"foo"}/>' },
{ code: '<a href="#foo" />' },
{ code: '<UX.Layout>test</UX.Layout>' },
{ code: '<a href={this} />' },
// CUSTOM ELEMENT TEST FOR ARRAY OPTION
{ code: '<Anchor {...props} />', options: components },
{ code: '<Anchor href="foo" />', options: components },
{ code: '<Anchor href={foo} />', options: components },
{ code: '<Anchor href="/foo" />', options: components },
{ code: '<Anchor href="https://foo.bar.com" />', options: components },
{ code: '<div href="foo" />', options: components },
{ code: '<Anchor href={`#foo`}/>', options: components },
{ code: '<Anchor href={"foo"}/>', options: components },
{ code: '<Anchor href="#foo" />', options: components },
{ code: '<Link {...props} />', options: components },
{ code: '<Link href="foo" />', options: components },
{ code: '<Link href={foo} />', options: components },
{ code: '<Link href="/foo" />', options: components },
{ code: '<Link href="https://foo.bar.com" />', options: components },
{ code: '<div href="foo" />', options: components },
{ code: '<Link href={`#foo`}/>', options: components },
{ code: '<Link href={"foo"}/>', options: components },
{ code: '<Link href="#foo" />', options: components },
// CUSTOM PROP TESTS
{ code: '<a {...props} />', options: specialLink },
{ code: '<a hrefLeft="foo" />', options: specialLink },
{ code: '<a hrefLeft={foo} />', options: specialLink },
{ code: '<a hrefLeft="/foo" />', options: specialLink },
{ code: '<a hrefLeft="https://foo.bar.com" />', options: specialLink },
{ code: '<div hrefLeft="foo" />', options: specialLink },
{ code: '<a hrefLeft={`#foo`}/>', options: specialLink },
{ code: '<a hrefLeft={"foo"}/>', options: specialLink },
{ code: '<a hrefLeft="#foo" />', options: specialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: specialLink },
{ code: '<a hrefRight={this} />', options: specialLink },
{ code: '<a {...props} />', options: specialLink },
{ code: '<a hrefRight="foo" />', options: specialLink },
{ code: '<a hrefRight={foo} />', options: specialLink },
{ code: '<a hrefRight="/foo" />', options: specialLink },
{ code: '<a hrefRight="https://foo.bar.com" />', options: specialLink },
{ code: '<div hrefRight="foo" />', options: specialLink },
{ code: '<a hrefRight={`#foo`}/>', options: specialLink },
{ code: '<a hrefRight={"foo"}/>', options: specialLink },
{ code: '<a hrefRight="#foo" />', options: specialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: specialLink },
{ code: '<a hrefRight={this} />', options: specialLink },
// CUSTOM BOTH COMPONENTS AND SPECIALLINK TESTS
{ code: '<Anchor {...props} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={foo} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="/foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="https://foo.bar.com" />', options: componentsAndSpecialLink },
{ code: '<div hrefLeft="foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={`#foo`}/>', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"foo"}/>', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="#foo" />', options: componentsAndSpecialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: componentsAndSpecialLink },
// WITH ONCLICK
// DEFAULT ELEMENT 'a' TESTS
{ code: '<a {...props} onClick={() => void 0} />' },
{ code: '<a href="foo" onClick={() => void 0} />' },
{ code: '<a href={foo} onClick={() => void 0} />' },
{ code: '<a href="/foo" onClick={() => void 0} />' },
{ code: '<a href="https://foo.bar.com" onClick={() => void 0} />' },
{ code: '<div href="foo" onClick={() => void 0} />' },
{ code: '<a href={`#foo`} onClick={() => void 0} />' },
{ code: '<a href={"foo"} onClick={() => void 0} />' },
{ code: '<a href="#foo" onClick={() => void 0} />' },
{ code: '<a href={this} onClick={() => void 0} />' },
// CUSTOM ELEMENT TEST FOR ARRAY OPTION
{ code: '<Anchor {...props} onClick={() => void 0} />', options: components },
{ code: '<Anchor href="foo" onClick={() => void 0} />', options: components },
{ code: '<Anchor href={foo} onClick={() => void 0} />', options: components },
{ code: '<Anchor href="/foo" onClick={() => void 0} />', options: components },
{ code: '<Anchor href="https://foo.bar.com" onClick={() => void 0} />', options: components },
{ code: '<Anchor href={`#foo`} onClick={() => void 0} />', options: components },
{ code: '<Anchor href={"foo"} onClick={() => void 0} />', options: components },
{ code: '<Anchor href="#foo" onClick={() => void 0} />', options: components },
{ code: '<Link {...props} onClick={() => void 0} />', options: components },
{ code: '<Link href="foo" onClick={() => void 0} />', options: components },
{ code: '<Link href={foo} onClick={() => void 0} />', options: components },
{ code: '<Link href="/foo" onClick={() => void 0} />', options: components },
{ code: '<Link href="https://foo.bar.com" onClick={() => void 0} />', options: components },
{ code: '<div href="foo" onClick={() => void 0} />', options: components },
{ code: '<Link href={`#foo`} onClick={() => void 0} />', options: components },
{ code: '<Link href={"foo"} onClick={() => void 0} />', options: components },
{ code: '<Link href="#foo" onClick={() => void 0} />', options: components },
// CUSTOM PROP TESTS
{ code: '<a {...props} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft="foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft={foo} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft="/foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft href="https://foo.bar.com" onClick={() => void 0} />', options: specialLink },
{ code: '<div hrefLeft="foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft={`#foo`} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft={"foo"} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefLeft="#foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={this} onClick={() => void 0} />', options: specialLink },
{ code: '<a {...props} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight="foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={foo} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight="/foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight href="https://foo.bar.com" onClick={() => void 0} />', options: specialLink },
{ code: '<div hrefRight="foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={`#foo`} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={"foo"} onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight="#foo" onClick={() => void 0} />', options: specialLink },
{ code: '<a hrefRight={this} onClick={() => void 0} />', options: specialLink },
// CUSTOM BOTH COMPONENTS AND SPECIALLINK TESTS
{ code: '<Anchor {...props} onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="foo" onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={foo} onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="/foo" onClick={() => void 0} />', options: componentsAndSpecialLink },
{
code: '<Anchor hrefLeft href="https://foo.bar.com" onClick={() => void 0} />',
options: componentsAndSpecialLink,
},
{ code: '<Anchor hrefLeft={`#foo`} onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"foo"} onClick={() => void 0} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="#foo" onClick={() => void 0} />', options: componentsAndSpecialLink },
// WITH ASPECTS TESTS
// NO HREF
{ code: '<a />', options: invalidHrefAspect },
{ code: '<a href={undefined} />', options: invalidHrefAspect },
{ code: '<a href={null} />', options: invalidHrefAspect },
{ code: '<a />', options: preferButtonAspect },
{ code: '<a href={undefined} />', options: preferButtonAspect },
{ code: '<a href={null} />', options: preferButtonAspect },
{ code: '<a />', options: preferButtonInvalidHrefAspect },
{ code: '<a href={undefined} />', options: preferButtonInvalidHrefAspect },
{ code: '<a href={null} />', options: preferButtonInvalidHrefAspect },
// INVALID HREF
{ code: '<a href="" />;', options: preferButtonAspect },
{ code: '<a href="#" />', options: preferButtonAspect },
{ code: '<a href={"#"} />', options: preferButtonAspect },
{ code: '<a href="javascript:void(0)" />', options: preferButtonAspect },
{ code: '<a href={"javascript:void(0)"} />', options: preferButtonAspect },
{ code: '<a href="" />;', options: noHrefAspect },
{ code: '<a href="#" />', options: noHrefAspect },
{ code: '<a href={"#"} />', options: noHrefAspect },
{ code: '<a href="javascript:void(0)" />', options: noHrefAspect },
{ code: '<a href={"javascript:void(0)"} />', options: noHrefAspect },
{ code: '<a href="" />;', options: noHrefPreferButtonAspect },
{ code: '<a href="#" />', options: noHrefPreferButtonAspect },
{ code: '<a href={"#"} />', options: noHrefPreferButtonAspect },
{ code: '<a href="javascript:void(0)" />', options: noHrefPreferButtonAspect },
{ code: '<a href={"javascript:void(0)"} />', options: noHrefPreferButtonAspect },
// SHOULD BE BUTTON
{ code: '<a onClick={() => void 0} />', options: invalidHrefAspect },
{ code: '<a href="#" onClick={() => void 0} />', options: noHrefAspect },
{ code: '<a href="javascript:void(0)" onClick={() => void 0} />', options: noHrefAspect },
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: noHrefAspect,
},
// CUSTOM COMPONENTS AND SPECIALLINK AND ASPECT
{ code: '<Anchor hrefLeft={undefined} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
{ code: '<Anchor hrefLeft={null} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
{ code: '<Anchor hrefLeft={undefined} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
{ code: '<Anchor hrefLeft={null} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
{ code: '<Anchor hrefLeft={undefined} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
{ code: '<Anchor hrefLeft={null} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
].map(parserOptionsMapper),
invalid: [
// DEFAULT ELEMENT 'a' TESTS
// NO HREF
{ code: '<a />', errors: [noHrefexpectedError] },
{ code: '<a href={undefined} />', errors: [noHrefexpectedError] },
{ code: '<a href={null} />', errors: [noHrefexpectedError] },
// INVALID HREF
{ code: '<a href="" />;', errors: [invalidHrefexpectedError] },
{ code: '<a href="#" />', errors: [invalidHrefErrorMessage] },
{ code: '<a href={"#"} />', errors: [invalidHrefErrorMessage] },
{ code: '<a href="javascript:void(0)" />', errors: [invalidHrefexpectedError] },
{ code: '<a href={"javascript:void(0)"} />', errors: [invalidHrefexpectedError] },
// SHOULD BE BUTTON
{ code: '<a onClick={() => void 0} />', errors: [preferButtonexpectedError] },
{ code: '<a href="#" onClick={() => void 0} />', errors: [preferButtonexpectedError] },
{ code: '<a href="javascript:void(0)" onClick={() => void 0} />', errors: [preferButtonexpectedError] },
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
},
// CUSTOM ELEMENT TEST FOR ARRAY OPTION
// NO HREF
{ code: '<Link />', errors: [noHrefexpectedError], options: components },
{ code: '<Link href={undefined} />', errors: [noHrefexpectedError], options: components },
{ code: '<Link href={null} />', errors: [noHrefexpectedError], options: components },
// INVALID HREF
{ code: '<Link href="" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Link href="#" />', errors: [invalidHrefErrorMessage], options: components },
{ code: '<Link href={"#"} />', errors: [invalidHrefErrorMessage], options: components },
{ code: '<Link href="javascript:void(0)" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Link href={"javascript:void(0)"} />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href="" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href="#" />', errors: [invalidHrefErrorMessage], options: components },
{ code: '<Anchor href={"#"} />', errors: [invalidHrefErrorMessage], options: components },
{ code: '<Anchor href="javascript:void(0)" />', errors: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href={"javascript:void(0)"} />', errors: [invalidHrefexpectedError], options: components },
// SHOULD BE BUTTON
{ code: '<Link onClick={() => void 0} />', errors: [preferButtonexpectedError], options: components },
{ code: '<Link href="#" onClick={() => void 0} />', errors: [preferButtonexpectedError], options: components },
{
code: '<Link href="javascript:void(0)" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: components,
},
{
code: '<Link href={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: components,
},
{ code: '<Anchor onClick={() => void 0} />', errors: [preferButtonexpectedError], options: components },
{ code: '<Anchor href="#" onClick={() => void 0} />', errors: [preferButtonexpectedError], options: components },
{
code: '<Anchor href="javascript:void(0)" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: components,
},
{
code: '<Anchor href={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: components,
},
// CUSTOM PROP TESTS
// NO HREF
{ code: '<a hrefLeft={undefined} />', errors: [noHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft={null} />', errors: [noHrefexpectedError], options: specialLink },
// INVALID HREF
{ code: '<a hrefLeft="" />;', errors: [invalidHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft="#" />', errors: [invalidHrefErrorMessage], options: specialLink },
{ code: '<a hrefLeft={"#"} />', errors: [invalidHrefErrorMessage], options: specialLink },
{ code: '<a hrefLeft="javascript:void(0)" />', errors: [invalidHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft={"javascript:void(0)"} />', errors: [invalidHrefexpectedError], options: specialLink },
// SHOULD BE BUTTON
{ code: '<a hrefLeft="#" onClick={() => void 0} />', errors: [preferButtonexpectedError], options: specialLink },
{
code: '<a hrefLeft="javascript:void(0)" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: specialLink,
},
{
code: '<a hrefLeft={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: specialLink,
},
// CUSTOM BOTH COMPONENTS AND SPECIALLINK TESTS
// NO HREF
{ code: '<Anchor Anchor={undefined} />', errors: [noHrefexpectedError], options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={null} />', errors: [noHrefexpectedError], options: componentsAndSpecialLink },
// INVALID HREF
{ code: '<Anchor hrefLeft="" />;', errors: [invalidHrefexpectedError], options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="#" />', errors: [invalidHrefErrorMessage], options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"#"} />', errors: [invalidHrefErrorMessage], options: componentsAndSpecialLink },
{
code: '<Anchor hrefLeft="javascript:void(0)" />',
errors: [invalidHrefexpectedError],
options: componentsAndSpecialLink,
},
{
code: '<Anchor hrefLeft={"javascript:void(0)"} />',
errors: [invalidHrefexpectedError],
options: componentsAndSpecialLink,
},
// SHOULD BE BUTTON
{
code: '<Anchor hrefLeft="#" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: componentsAndSpecialLink,
},
{
code: '<Anchor hrefLeft="javascript:void(0)" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: componentsAndSpecialLink,
},
{
code: '<Anchor hrefLeft={"javascript:void(0)"} onClick={() => void 0} />',
errors: [preferButtonexpectedError],
options: componentsAndSpecialLink,
},
// WITH ASPECTS TESTS
// NO HREF
{ code: '<a />', options: noHrefAspect, errors: [noHrefErrorMessage] },
{ code: '<a />', options: noHrefPreferButtonAspect, errors: [noHrefErrorMessage] },
{ code: '<a />', options: noHrefInvalidHrefAspect, errors: [noHrefErrorMessage] },
{ code: '<a href={undefined} />', options: noHrefAspect, errors: [noHrefErrorMessage] },
{ code: '<a href={undefined} />', options: noHrefPreferButtonAspect, errors: [noHrefErrorMessage] },
{ code: '<a href={undefined} />', options: noHrefInvalidHrefAspect, errors: [noHrefErrorMessage] },
{ code: '<a href={null} />', options: noHrefAspect, errors: [noHrefErrorMessage] },
{ code: '<a href={null} />', options: noHrefPreferButtonAspect, errors: [noHrefErrorMessage] },
{ code: '<a href={null} />', options: noHrefInvalidHrefAspect, errors: [noHrefErrorMessage] },
// INVALID HREF
{ code: '<a href="" />;', options: invalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href="" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href="" />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href="#" />;', options: invalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href="#" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href="#" />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href={"#"} />;', options: invalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href={"#"} />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href={"#"} />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href="javascript:void(0)" />;', options: invalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href="javascript:void(0)" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefErrorMessage] },
{
code: '<a href="javascript:void(0)" />;',
options: preferButtonInvalidHrefAspect,
errors: [invalidHrefErrorMessage],
},
{ code: '<a href={"javascript:void(0)"} />;', options: invalidHrefAspect, errors: [invalidHrefErrorMessage] },
{ code: '<a href={"javascript:void(0)"} />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefErrorMessage] },
{
code: '<a href={"javascript:void(0)"} />;',
options: preferButtonInvalidHrefAspect,
errors: [invalidHrefErrorMessage],
},
// SHOULD BE BUTTON
{ code: '<a onClick={() => void 0} />', options: preferButtonAspect, errors: [preferButtonErrorMessage] },
{
code: '<a onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonErrorMessage],
},
{ code: '<a onClick={() => void 0} />', options: noHrefPreferButtonAspect, errors: [preferButtonErrorMessage] },
{ code: '<a onClick={() => void 0} />', options: noHrefAspect, errors: [noHrefErrorMessage] },
{ code: '<a onClick={() => void 0} />', options: noHrefInvalidHrefAspect, errors: [noHrefErrorMessage] },
{ code: '<a href="#" onClick={() => void 0} />', options: preferButtonAspect, errors: [preferButtonErrorMessage] },
{
code: '<a href="#" onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonErrorMessage],
},
{
code: '<a href="#" onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonErrorMessage],
},
{ code: '<a href="#" onClick={() => void 0} />', options: invalidHrefAspect, errors: [invalidHrefErrorMessage] },
{
code: '<a href="#" onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefErrorMessage],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: preferButtonAspect,
errors: [preferButtonErrorMessage],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonErrorMessage],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonErrorMessage],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: invalidHrefAspect,
errors: [invalidHrefErrorMessage],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefErrorMessage],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: preferButtonAspect,
errors: [preferButtonErrorMessage],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonErrorMessage],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonErrorMessage],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: invalidHrefAspect,
errors: [invalidHrefErrorMessage],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefErrorMessage],
},
// CUSTOM COMPONENTS AND SPECIALLINK AND ASPECT
{
code: '<Anchor hrefLeft={undefined} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefErrorMessage],
},
{
code: '<Anchor hrefLeft={null} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefErrorMessage],
},
{
code: '<Anchor hrefLeft={undefined} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefErrorMessage],
},
{
code: '<Anchor hrefLeft={null} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefErrorMessage],
},
{
code: '<Anchor hrefLeft={undefined} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefErrorMessage],
},
{
code: '<Anchor hrefLeft={null} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefErrorMessage],
},
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,83 @@
/**
* @fileoverview Enforce elements with aria-activedescendant are tabbable.
* @author Jesse Beach <@jessebeach>
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/aria-activedescendant-has-tabindex';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'An element that manages focus with `aria-activedescendant` ' +
'must be tabbable',
type: 'JSXOpeningElement',
};
ruleTester.run('aria-activedescendant-has-tabindex', rule, {
valid: [
{
code: '<CustomComponent />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={0} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={-1} />;',
},
{
code: '<div />;',
},
{
code: '<input />;',
},
{
code: '<div tabIndex={0} />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex={0} />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex="0" />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex={1} />;',
},
{
code: '<input aria-activedescendant={someID} />;',
},
{
code: '<input aria-activedescendant={someID} tabIndex={0} />;',
},
].map(parserOptionsMapper),
invalid: [
{
code: '<div aria-activedescendant={someID} />;',
errors: [expectedError],
},
{
code: '<div aria-activedescendant={someID} tabIndex={-1} />;',
errors: [expectedError],
},
{
code: '<div aria-activedescendant={someID} tabIndex="-1" />;',
errors: [expectedError],
},
{
code: '<input aria-activedescendant={someID} tabIndex={-1} />;',
errors: [expectedError],
},
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,68 @@
/* eslint-env jest */
/**
* @fileoverview Enforce all aria-* properties are valid.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { aria } from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/aria-props';
import getSuggestion from '../../../src/util/getSuggestion';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const ariaAttributes = [...aria.keys()];
const errorMessage = (name) => {
const suggestions = getSuggestion(name, ariaAttributes);
const message = `${name}: This attribute is an invalid ARIA attribute.`;
if (suggestions.length > 0) {
return {
type: 'JSXAttribute',
message: `${message} Did you mean to use ${suggestions}?`,
};
}
return {
type: 'JSXAttribute',
message,
};
};
// Create basic test cases using all valid role types.
const basicValidityTests = ariaAttributes.map(prop => ({
code: `<div ${prop.toLowerCase()}="foobar" />`,
}));
ruleTester.run('aria-props', rule, {
valid: [
// Variables should pass, as we are only testing literals.
{ code: '<div />' },
{ code: '<div></div>' },
{ code: '<div aria="wee"></div>' }, // Needs aria-*
{ code: '<div abcARIAdef="true"></div>' },
{ code: '<div fooaria-foobar="true"></div>' },
{ code: '<div fooaria-hidden="true"></div>' },
{ code: '<Bar baz />' },
].concat(basicValidityTests).map(parserOptionsMapper),
invalid: [
{ code: '<div aria-="foobar" />', errors: [errorMessage('aria-')] },
{
code: '<div aria-labeledby="foobar" />',
errors: [errorMessage('aria-labeledby')],
},
{
code: '<div aria-skldjfaria-klajsd="foobar" />',
errors: [errorMessage('aria-skldjfaria-klajsd')],
},
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,267 @@
/* eslint-env jest */
/**
* @fileoverview Enforce ARIA state and property values are valid.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { aria } from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule, { validityCheck } from '../../../src/rules/aria-proptypes';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = (name) => {
const {
type,
values: permittedValues,
} = aria.get(name.toLowerCase());
switch (type) {
case 'tristate':
return `The value for ${name} must be a boolean or the string "mixed".`;
case 'token':
return `The value for ${name} must be a single token from the following: ${permittedValues}.`;
case 'tokenlist':
return `The value for ${name} must be a list of one or more \
tokens from the following: ${permittedValues}.`;
case 'boolean':
case 'string':
case 'integer':
case 'number':
default:
return `The value for ${name} must be a ${type}.`;
}
};
describe('validityCheck', () => {
it('should false for an unknown expected type', () => {
expect(validityCheck(
null,
null,
)).toBe(false);
});
});
ruleTester.run('aria-proptypes', rule, {
valid: [
// DON'T TEST INVALID ARIA-* PROPS
{ code: '<div aria-foo="true" />' },
{ code: '<div abcaria-foo="true" />' },
// BOOLEAN
{ code: '<div aria-hidden={true} />' },
{ code: '<div aria-hidden="true" />' },
{ code: '<div aria-hidden={"false"} />' },
{ code: '<div aria-hidden={!false} />' },
{ code: '<div aria-hidden />' },
{ code: '<div aria-hidden={false} />' },
{ code: '<div aria-hidden={!true} />' },
{ code: '<div aria-hidden={!"yes"} />' },
{ code: '<div aria-hidden={foo} />' },
{ code: '<div aria-hidden={foo.bar} />' },
{ code: '<div aria-hidden={<div />} />' },
// STRING
{ code: '<div aria-label="Close" />' },
{ code: '<div aria-label={`Close`} />' },
{ code: '<div aria-label={foo} />' },
{ code: '<div aria-label={foo.bar} />' },
{ code: '<input aria-invalid={error ? "true" : "false"} />' },
{ code: '<input aria-invalid={undefined ? "true" : "false"} />' },
// TRISTATE
{ code: '<div aria-checked={true} />' },
{ code: '<div aria-checked="true" />' },
{ code: '<div aria-checked={"false"} />' },
{ code: '<div aria-checked={!false} />' },
{ code: '<div aria-checked />' },
{ code: '<div aria-checked={false} />' },
{ code: '<div aria-checked={!true} />' },
{ code: '<div aria-checked={!"yes"} />' },
{ code: '<div aria-checked={foo} />' },
{ code: '<div aria-checked={foo.bar} />' },
{ code: '<div aria-checked="mixed" />' },
{ code: '<div aria-checked={`mixed`} />' },
// INTEGER
{ code: '<div aria-level={123} />' },
{ code: '<div aria-level={-123} />' },
{ code: '<div aria-level={+123} />' },
{ code: '<div aria-level={~123} />' },
{ code: '<div aria-level={"123"} />' },
{ code: '<div aria-level={`123`} />' },
{ code: '<div aria-level="123" />' },
{ code: '<div aria-level={foo} />' },
{ code: '<div aria-level={foo.bar} />' },
// NUMBER
{ code: '<div aria-valuemax={123} />' },
{ code: '<div aria-valuemax={-123} />' },
{ code: '<div aria-valuemax={+123} />' },
{ code: '<div aria-valuemax={~123} />' },
{ code: '<div aria-valuemax={"123"} />' },
{ code: '<div aria-valuemax={`123`} />' },
{ code: '<div aria-valuemax="123" />' },
{ code: '<div aria-valuemax={foo} />' },
{ code: '<div aria-valuemax={foo.bar} />' },
// TOKEN
{ code: '<div aria-sort="ascending" />' },
{ code: '<div aria-sort="ASCENDING" />' },
{ code: '<div aria-sort={"ascending"} />' },
{ code: '<div aria-sort={`ascending`} />' },
{ code: '<div aria-sort="descending" />' },
{ code: '<div aria-sort={"descending"} />' },
{ code: '<div aria-sort={`descending`} />' },
{ code: '<div aria-sort="none" />' },
{ code: '<div aria-sort={"none"} />' },
{ code: '<div aria-sort={`none`} />' },
{ code: '<div aria-sort="other" />' },
{ code: '<div aria-sort={"other"} />' },
{ code: '<div aria-sort={`other`} />' },
{ code: '<div aria-sort={foo} />' },
{ code: '<div aria-sort={foo.bar} />' },
{ code: '<div aria-invalid={true} />' },
{ code: '<div aria-invalid="true" />' },
{ code: '<div aria-invalid={false} />' },
{ code: '<div aria-invalid="false" />' },
{ code: '<div aria-invalid="grammar" />' },
{ code: '<div aria-invalid="spelling" />' },
// TOKENLIST
{ code: '<div aria-relevant="additions" />' },
{ code: '<div aria-relevant={"additions"} />' },
{ code: '<div aria-relevant={`additions`} />' },
{ code: '<div aria-relevant="additions removals" />' },
{ code: '<div aria-relevant="additions additions" />' },
{ code: '<div aria-relevant={"additions removals"} />' },
{ code: '<div aria-relevant={`additions removals`} />' },
{ code: '<div aria-relevant="additions removals text" />' },
{ code: '<div aria-relevant={"additions removals text"} />' },
{ code: '<div aria-relevant={`additions removals text`} />' },
{ code: '<div aria-relevant="additions removals text all" />' },
{ code: '<div aria-relevant={"additions removals text all"} />' },
{ code: '<div aria-relevant={`removals additions text all`} />' },
{ code: '<div aria-relevant={foo} />' },
{ code: '<div aria-relevant={foo.bar} />' },
].map(parserOptionsMapper),
invalid: [
// BOOLEAN
{
code: '<div aria-hidden={undefined} />',
errors: [errorMessage('aria-hidden')],
},
{ code: '<div aria-hidden="yes" />', errors: [errorMessage('aria-hidden')] },
{ code: '<div aria-hidden="no" />', errors: [errorMessage('aria-hidden')] },
{ code: '<div aria-hidden={1234} />', errors: [errorMessage('aria-hidden')] },
{
code: '<div aria-hidden={`${abc}`} />',
errors: [errorMessage('aria-hidden')],
},
// STRING
{ code: '<div aria-label={undefined} />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label={true} />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label={false} />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label={1234} />', errors: [errorMessage('aria-label')] },
{ code: '<div aria-label={!true} />', errors: [errorMessage('aria-label')] },
// TRISTATE
{
code: '<div aria-checked={undefined} />',
errors: [errorMessage('aria-checked')],
},
{ code: '<div aria-checked="yes" />', errors: [errorMessage('aria-checked')] },
{ code: '<div aria-checked="no" />', errors: [errorMessage('aria-checked')] },
{ code: '<div aria-checked={1234} />', errors: [errorMessage('aria-checked')] },
{
code: '<div aria-checked={`${abc}`} />',
errors: [errorMessage('aria-checked')],
},
// INTEGER
{ code: '<div aria-level={undefined} />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level="yes" />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level="no" />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level={`abc`} />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level={true} />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level={"false"} />', errors: [errorMessage('aria-level')] },
{ code: '<div aria-level={!"false"} />', errors: [errorMessage('aria-level')] },
// NUMBER
{
code: '<div aria-valuemax={undefined} />',
errors: [errorMessage('aria-valuemax')],
},
{ code: '<div aria-valuemax="yes" />', errors: [errorMessage('aria-valuemax')] },
{ code: '<div aria-valuemax="no" />', errors: [errorMessage('aria-valuemax')] },
{
code: '<div aria-valuemax={`abc`} />',
errors: [errorMessage('aria-valuemax')],
},
{
code: '<div aria-valuemax={true} />',
errors: [errorMessage('aria-valuemax')],
},
{ code: '<div aria-valuemax />', errors: [errorMessage('aria-valuemax')] },
{
code: '<div aria-valuemax={"false"} />',
errors: [errorMessage('aria-valuemax')],
},
{
code: '<div aria-valuemax={!"false"} />',
errors: [errorMessage('aria-valuemax')],
},
// TOKEN
{ code: '<div aria-sort="" />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort="descnding" />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort={undefined} />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort={true} />', errors: [errorMessage('aria-sort')] },
{ code: '<div aria-sort={"false"} />', errors: [errorMessage('aria-sort')] },
{
code: '<div aria-sort="ascending descending" />',
errors: [errorMessage('aria-sort')],
},
// TOKENLIST
{ code: '<div aria-relevant="" />', errors: [errorMessage('aria-relevant')] },
{
code: '<div aria-relevant="foobar" />',
errors: [errorMessage('aria-relevant')],
},
{ code: '<div aria-relevant />', errors: [errorMessage('aria-relevant')] },
{
code: '<div aria-relevant={undefined} />',
errors: [errorMessage('aria-relevant')],
},
{
code: '<div aria-relevant={true} />',
errors: [errorMessage('aria-relevant')],
},
{
code: '<div aria-relevant={"false"} />',
errors: [errorMessage('aria-relevant')],
},
{
code: '<div aria-relevant="additions removalss" />',
errors: [errorMessage('aria-relevant')],
},
{
code: '<div aria-relevant="additions removalss " />',
errors: [errorMessage('aria-relevant')],
},
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,77 @@
/* eslint-env jest */
/**
* @fileoverview Enforce aria role attribute is valid.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { roles } from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/aria-role';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = {
message: 'Elements with ARIA roles must use a valid, non-abstract ARIA role.',
type: 'JSXAttribute',
};
const validRoles = [...roles.keys()].filter(
role => roles.get(role).abstract === false,
);
const invalidRoles = [...roles.keys()].filter(
role => roles.get(role).abstract === true,
);
const createTests = roleNames => roleNames.map(role => ({
code: `<div role="${role.toLowerCase()}" />`,
}));
const validTests = createTests(validRoles);
const invalidTests = createTests(invalidRoles).map((test) => {
const invalidTest = Object.assign({}, test);
invalidTest.errors = [errorMessage];
return invalidTest;
});
const ignoreNonDOMSchema = [{
ignoreNonDOM: true,
}];
ruleTester.run('aria-role', rule, {
valid: [
// Variables should pass, as we are only testing literals.
{ code: '<div />' },
{ code: '<div></div>' },
{ code: '<div role={role} />' },
{ code: '<div role={role || "button"} />' },
{ code: '<div role={role || "foobar"} />' },
{ code: '<div role="tabpanel row" />' },
{ code: '<div role="switch" />' },
{ code: '<div role="doc-abstract" />' },
{ code: '<div role="doc-appendix doc-bibliography" />' },
{ code: '<Bar baz />' },
{ code: '<Foo role="bar" />', options: ignoreNonDOMSchema },
].concat(validTests).map(parserOptionsMapper),
invalid: [
{ code: '<div role="foobar" />', errors: [errorMessage] },
{ code: '<div role="datepicker"></div>', errors: [errorMessage] },
{ code: '<div role="range"></div>', errors: [errorMessage] },
{ code: '<div role=""></div>', errors: [errorMessage] },
{ code: '<div role="tabpanel row foobar"></div>', errors: [errorMessage] },
{ code: '<div role="tabpanel row range"></div>', errors: [errorMessage] },
{ code: '<div role="doc-endnotes range"></div>', errors: [errorMessage] },
{ code: '<div role />', errors: [errorMessage] },
{ code: '<div role={null}></div>', errors: [errorMessage] },
{ code: '<Foo role="datepicker" />', errors: [errorMessage] },
].concat(invalidTests).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,68 @@
/* eslint-env jest */
/**
* @fileoverview Enforce that elements that do not support ARIA roles,
* states and properties do not have those attributes.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { dom } from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/aria-unsupported-elements';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = invalidProp => ({
message: `This element does not support ARIA roles, states and properties. \
Try removing the prop '${invalidProp}'.`,
type: 'JSXOpeningElement',
});
const domElements = [...dom.keys()];
// Generate valid test cases
const roleValidityTests = domElements.map((element) => {
const isReserved = dom.get(element).reserved || false;
const role = isReserved ? '' : 'role';
return {
code: `<${element} ${role} />`,
};
});
const ariaValidityTests = domElements.map((element) => {
const isReserved = dom.get(element).reserved || false;
const aria = isReserved ? '' : 'aria-hidden';
return {
code: `<${element} ${aria} />`,
};
});
// Generate invalid test cases.
const invalidRoleValidityTests = domElements
.filter(element => Boolean(dom.get(element).reserved))
.map(reservedElem => ({
code: `<${reservedElem} role {...props} />`,
errors: [errorMessage('role')],
}));
const invalidAriaValidityTests = domElements
.filter(element => Boolean(dom.get(element).reserved))
.map(reservedElem => ({
code: `<${reservedElem} aria-hidden {...props} />`,
errors: [errorMessage('aria-hidden')],
}));
ruleTester.run('aria-unsupported-elements', rule, {
valid: roleValidityTests.concat(ariaValidityTests).map(parserOptionsMapper),
invalid: invalidRoleValidityTests.concat(invalidAriaValidityTests)
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,74 @@
/* eslint-env jest */
/**
* @fileoverview Enforce a clickable non-interactive element has at least 1 keyboard event listener.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/click-events-have-key-events';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = 'Visible, non-interactive elements with click handlers' +
' must have at least one keyboard listener.';
const expectedError = {
message: errorMessage,
type: 'JSXOpeningElement',
};
ruleTester.run('click-events-have-key-events', rule, {
valid: [
{ code: '<div onClick={() => void 0} onKeyDown={foo}/>;' },
{ code: '<div onClick={() => void 0} onKeyUp={foo} />;' },
{ code: '<div onClick={() => void 0} onKeyPress={foo}/>;' },
{ code: '<div onClick={() => void 0} onKeyDown={foo} onKeyUp={bar} />;' },
{ code: '<div onClick={() => void 0} onKeyDown={foo} {...props} />;' },
{ code: '<div className="foo" />;' },
{ code: '<div onClick={() => void 0} aria-hidden />;' },
{ code: '<div onClick={() => void 0} aria-hidden={true} />;' },
{ code: '<div onClick={() => void 0} aria-hidden={false} onKeyDown={foo} />;' },
{
code: '<div onClick={() => void 0} onKeyDown={foo} aria-hidden={undefined} />;',
},
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input onClick={() => void 0} />' },
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<input onClick={() => void 0} type="hidden" />;' },
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<div onClick={() => void 0} />;', errors: [expectedError] },
{
code: '<div onClick={() => void 0} role={undefined} />;',
errors: [expectedError],
},
{ code: '<div onClick={() => void 0} {...props} />;', errors: [expectedError] },
{ code: '<section onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<main onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<article onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<header onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<footer onClick={() => void 0} />;', errors: [expectedError] },
{
code: '<div onClick={() => void 0} aria-hidden={false} />;',
errors: [expectedError],
},
{ code: '<a onClick={() => void 0} />', errors: [expectedError] },
{ code: '<a tabIndex="0" onClick={() => void 0} />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,47 @@
/* eslint-env jest */
/**
* @fileoverview Enforce heading (h1, h2, etc) elements contain accessible content.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/heading-has-content';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Headings must have content and the content must be accessible by a screen reader.',
type: 'JSXOpeningElement',
};
ruleTester.run('heading-has-content', rule, {
valid: [
{ code: '<div />;' },
{ code: '<h1>Foo</h1>' },
{ code: '<h2>Foo</h2>' },
{ code: '<h3>Foo</h3>' },
{ code: '<h4>Foo</h4>' },
{ code: '<h5>Foo</h5>' },
{ code: '<h6>Foo</h6>' },
{ code: '<h6>123</h6>' },
{ code: '<h1><Bar /></h1>' },
{ code: '<h1>{foo}</h1>' },
{ code: '<h1>{foo.bar}</h1>' },
{ code: '<h1 dangerouslySetInnerHTML={{ __html: "foo" }} />' },
{ code: '<h1 children={children} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<h1 />', errors: [expectedError] },
{ code: '<h1><Bar aria-hidden /></h1>', errors: [expectedError] },
{ code: '<h1>{undefined}</h1>', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,173 @@
/* eslint-env jest */
/**
* @fileoverview Enforce links may not point to just #.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/href-no-hash';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Links must not point to "#". Use a more descriptive href or use a button instead.',
type: 'JSXOpeningElement',
};
const components = [{
components: ['Anchor', 'Link'],
}];
const specialLink = [{
specialLink: ['hrefLeft', 'hrefRight'],
}];
const componentsAndSpecialLink = [{
components: ['Anchor'],
specialLink: ['hrefLeft'],
}];
ruleTester.run('href-no-hash', rule, {
valid: [
// DEFAULT ELEMENT 'a' TESTS
{ code: '<Anchor />;' },
{ code: '<a {...props} />' },
{ code: '<a href="foo" />' },
{ code: '<a href={foo} />' },
{ code: '<a href="/foo" />' },
{ code: '<a href={`${undefined}`} />' },
{ code: '<div href="foo" />' },
{ code: '<a href={`${undefined}foo`}/>' },
{ code: '<a href={`#${undefined}foo`}/>' },
{ code: '<a href={`#foo`}/>' },
{ code: '<a href={"foo"}/>' },
{ code: '<a href="#foo" />' },
{ code: '<UX.Layout>test</UX.Layout>' },
{ code: '<a href={this} />' },
// CUSTOM ELEMENT TEST FOR ARRAY OPTION
{ code: '<Anchor />;', options: components },
{ code: '<Anchor {...props} />', options: components },
{ code: '<Anchor href="foo" />', options: components },
{ code: '<Anchor href={foo} />', options: components },
{ code: '<Anchor href="/foo" />', options: components },
{ code: '<Anchor href={`${undefined}`} />', options: components },
{ code: '<div href="foo" />', options: components },
{ code: '<Anchor href={`${undefined}foo`}/>', options: components },
{ code: '<Anchor href={`#${undefined}foo`}/>', options: components },
{ code: '<Anchor href={`#foo`}/>', options: components },
{ code: '<Anchor href={"foo"}/>', options: components },
{ code: '<Anchor href="#foo" />', options: components },
{ code: '<Link />;', options: components },
{ code: '<Link {...props} />', options: components },
{ code: '<Link href="foo" />', options: components },
{ code: '<Link href={foo} />', options: components },
{ code: '<Link href="/foo" />', options: components },
{ code: '<Link href={`${undefined}`} />', options: components },
{ code: '<div href="foo" />', options: components },
{ code: '<Link href={`${undefined}foo`}/>', options: components },
{ code: '<Link href={`#${undefined}foo`}/>', options: components },
{ code: '<Link href={`#foo`}/>', options: components },
{ code: '<Link href={"foo"}/>', options: components },
{ code: '<Link href="#foo" />', options: components },
// CUSTOM PROP TESTS
{ code: '<a />;', options: specialLink },
{ code: '<a {...props} />', options: specialLink },
{ code: '<a hrefLeft="foo" />', options: specialLink },
{ code: '<a hrefLeft={foo} />', options: specialLink },
{ code: '<a hrefLeft="/foo" />', options: specialLink },
{ code: '<a hrefLeft={`${undefined}`} />', options: specialLink },
{ code: '<div hrefLeft="foo" />', options: specialLink },
{ code: '<a hrefLeft={`${undefined}foo`}/>', options: specialLink },
{ code: '<a hrefLeft={`#${undefined}foo`}/>', options: specialLink },
{ code: '<a hrefLeft={`#foo`}/>', options: specialLink },
{ code: '<a hrefLeft={"foo"}/>', options: specialLink },
{ code: '<a hrefLeft="#foo" />', options: specialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: specialLink },
{ code: '<a hrefRight={this} />', options: specialLink },
{ code: '<a />;', options: specialLink },
{ code: '<a {...props} />', options: specialLink },
{ code: '<a hrefRight="foo" />', options: specialLink },
{ code: '<a hrefRight={foo} />', options: specialLink },
{ code: '<a hrefRight="/foo" />', options: specialLink },
{ code: '<a hrefRight={`${undefined}`} />', options: specialLink },
{ code: '<div hrefRight="foo" />', options: specialLink },
{ code: '<a hrefRight={`${undefined}foo`}/>', options: specialLink },
{ code: '<a hrefRight={`#${undefined}foo`}/>', options: specialLink },
{ code: '<a hrefRight={`#foo`}/>', options: specialLink },
{ code: '<a hrefRight={"foo"}/>', options: specialLink },
{ code: '<a hrefRight="#foo" />', options: specialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: specialLink },
{ code: '<a hrefRight={this} />', options: specialLink },
// CUSTOM BOTH COMPONENTS AND SPECIALLINK TESTS
{ code: '<Anchor />;', options: componentsAndSpecialLink },
{ code: '<Anchor {...props} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={foo} />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="/foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={`${undefined}`} />', options: componentsAndSpecialLink },
{ code: '<div hrefLeft="foo" />', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={`${undefined}foo`}/>', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={`#${undefined}foo`}/>', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={`#foo`}/>', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"foo"}/>', options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft="#foo" />', options: componentsAndSpecialLink },
{ code: '<UX.Layout>test</UX.Layout>', options: componentsAndSpecialLink },
].map(parserOptionsMapper),
invalid: [
// DEFAULT ELEMENT 'a' TESTS
{ code: '<a href="#" />', errors: [expectedError] },
{ code: '<a href={"#"} />', errors: [expectedError] },
{ code: '<a href={`#${undefined}`} />', errors: [expectedError] },
// CUSTOM ELEMENT TEST FOR ARRAY OPTION
{ code: '<Link href="#" />', errors: [expectedError], options: components },
{ code: '<Link href={"#"} />', errors: [expectedError], options: components },
{
code: '<Link href={`#${undefined}`} />',
errors: [expectedError],
options: components,
},
{ code: '<Anchor href="#" />', errors: [expectedError], options: components },
{ code: '<Anchor href={"#"} />', errors: [expectedError], options: components },
{
code: '<Anchor href={`#${undefined}`} />',
errors: [expectedError],
options: components,
},
// CUSTOM PROP TESTS
{ code: '<a hrefLeft="#" />', errors: [expectedError], options: specialLink },
{ code: '<a hrefLeft={"#"} />', errors: [expectedError], options: specialLink },
{
code: '<a hrefLeft={`#${undefined}`} />',
errors: [expectedError],
options: specialLink,
},
{ code: '<a hrefRight="#" />', errors: [expectedError], options: specialLink },
{ code: '<a hrefRight={"#"} />', errors: [expectedError], options: specialLink },
{
code: '<a hrefRight={`#${undefined}`} />',
errors: [expectedError],
options: specialLink,
},
// CUSTOM BOTH COMPONENTS AND SPECIALLINK TESTS
{ code: '<Anchor hrefLeft="#" />', errors: [expectedError], options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"#"} />', errors: [expectedError], options: componentsAndSpecialLink },
{
code: '<Anchor hrefLeft={`#${undefined}`} />',
errors: [expectedError],
options: componentsAndSpecialLink,
},
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,40 @@
/* eslint-env jest */
/**
* @fileoverview Enforce html element has lang prop.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/html-has-lang';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: '<html> elements must have the lang prop.',
type: 'JSXOpeningElement',
};
ruleTester.run('html-has-lang', rule, {
valid: [
{ code: '<div />;' },
{ code: '<html lang="en" />' },
{ code: '<html lang="en-US" />' },
{ code: '<html lang={foo} />' },
{ code: '<html lang />' },
{ code: '<HTML />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<html />', errors: [expectedError] },
{ code: '<html {...props} />', errors: [expectedError] },
{ code: '<html lang={undefined} />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,45 @@
/* eslint-env jest */
/**
* @fileoverview Enforce iframe elements have a title attribute.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/iframe-has-title';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: '<iframe> elements must have a unique title property.',
type: 'JSXOpeningElement',
};
ruleTester.run('html-has-lang', rule, {
valid: [
{ code: '<div />;' },
{ code: '<iframe title="Unique title" />' },
{ code: '<iframe title={foo} />' },
{ code: '<FooComponent />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<iframe />', errors: [expectedError] },
{ code: '<iframe {...props} />', errors: [expectedError] },
{ code: '<iframe title={undefined} />', errors: [expectedError] },
{ code: '<iframe title="" />', errors: [expectedError] },
{ code: '<iframe title={false} />', errors: [expectedError] },
{ code: '<iframe title={true} />', errors: [expectedError] },
{ code: "<iframe title={''} />", errors: [expectedError] },
{ code: '<iframe title={``} />', errors: [expectedError] },
{ code: '<iframe title={""} />', errors: [expectedError] },
{ code: '<iframe title={42} />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,117 @@
/* eslint-env jest */
/**
* @fileoverview Enforce img alt attribute does not have the word image, picture, or photo.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/img-redundant-alt';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const array = [{
components: ['Image'],
words: ['Word1', 'Word2'],
}];
const ruleTester = new RuleTester();
const expectedError = {
message: 'Redundant alt attribute. Screen-readers already announce `img` tags as an image. ' +
'You don\'t need to use the words `image`, `photo,` or `picture` ' +
'(or any specified custom words) in the alt prop.',
type: 'JSXOpeningElement',
};
ruleTester.run('img-redundant-alt', rule, {
valid: [
{ code: '<img alt="foo" />;' },
{ code: '<img alt="picture of me taking a photo of an image" aria-hidden />' },
{ code: '<img aria-hidden alt="photo of image" />' },
{ code: '<img ALt="foo" />;' },
{ code: '<img {...this.props} alt="foo" />' },
{ code: '<img {...this.props} alt={"foo"} />' },
{ code: '<img {...this.props} alt={alt} />' },
{ code: '<a />' },
{ code: '<img />' },
{ code: '<IMG />' },
{ code: '<img alt={undefined} />' },
{ code: '<img alt={`this should pass for ${now}`} />' },
{ code: '<img alt={`this should pass for ${photo}`} />' },
{ code: '<img alt={`this should pass for ${image}`} />' },
{ code: '<img alt={`this should pass for ${picture}`} />' },
{ code: '<img alt={`${photo}`} />' },
{ code: '<img alt={`${image}`} />' },
{ code: '<img alt={`${picture}`} />' },
{ code: '<img alt={"undefined"} />' },
{ code: '<img alt={() => {}} />' },
{ code: '<img alt={function(e){}} />' },
{ code: '<img aria-hidden={false} alt="Doing cool things." />' },
{ code: '<UX.Layout>test</UX.Layout>' },
{ code: '<img alt={imageAlt} />' },
{ code: '<img alt />' },
{ code: '<img alt="Photography" />;' },
{ code: '<img alt="ImageMagick" />;' },
].map(parserOptionsMapper),
invalid: [
{ code: '<img alt="Photo of friend." />;', errors: [expectedError] },
{ code: '<img alt="Picture of friend." />;', errors: [expectedError] },
{ code: '<img alt="Image of friend." />;', errors: [expectedError] },
{ code: '<img alt="PhOtO of friend." />;', errors: [expectedError] },
{ code: '<img alt={"photo"} />;', errors: [expectedError] },
{ code: '<img alt="piCTUre of friend." />;', errors: [expectedError] },
{ code: '<img alt="imAGE of friend." />;', errors: [expectedError] },
{
code: '<img alt="photo of cool person" aria-hidden={false} />',
errors: [expectedError],
},
{
code: '<img alt="picture of cool person" aria-hidden={false} />',
errors: [expectedError],
},
{
code: '<img alt="image of cool person" aria-hidden={false} />',
errors: [expectedError],
},
{ code: '<img alt="photo" {...this.props} />', errors: [expectedError] },
{ code: '<img alt="image" {...this.props} />', errors: [expectedError] },
{ code: '<img alt="picture" {...this.props} />', errors: [expectedError] },
{
code: '<img alt={`picture doing ${things}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`photo doing ${things}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`image doing ${things}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`picture doing ${picture}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`photo doing ${photo}`} {...this.props} />',
errors: [expectedError],
},
{
code: '<img alt={`image doing ${image}`} {...this.props} />',
errors: [expectedError],
},
// TESTS FOR ARRAY OPTION TESTS
{ code: '<img alt="Word1" />;', options: array, errors: [expectedError] },
{ code: '<img alt="Word2" />;', options: array, errors: [expectedError] },
{ code: '<Image alt="Word1" />;', options: array, errors: [expectedError] },
{ code: '<Image alt="Word2" />;', options: array, errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,252 @@
/* eslint-env jest */
/**
* @fileoverview Enforce that elements with onClick handlers must be focusable.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import includes from 'array-includes';
import { RuleTester } from 'eslint';
import {
eventHandlers,
eventHandlersByType,
} from 'jsx-ast-utils';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/interactive-supports-focus';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
function template(strings, ...keys) {
return (...values) => keys.reduce(
(acc, k, i) => acc + (values[k] || '') + strings[i + 1],
strings[0],
);
}
const ruleName = 'interactive-supports-focus';
const type = 'JSXOpeningElement';
const codeTemplate = template`<${0} role="${1}" ${2}={() => void 0} />`;
const tabindexTemplate =
template`<${0} role="${1}" ${2}={() => void 0} tabIndex="0" />`;
const tabbableTemplate = template`Elements with the '${0}' interactive role must be tabbable.`;
const focusableTemplate = template`Elements with the '${0}' interactive role must be focusable.`;
const recommendedOptions =
(configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {});
const strictOptions =
(configs.strict.rules[`jsx-a11y/${ruleName}`][1] || {});
const alwaysValid = [
{ code: '<div />' },
{ code: '<div aria-hidden onClick={() => void 0} />' },
{ code: '<div aria-hidden={true == true} onClick={() => void 0} />' },
{ code: '<div aria-hidden={true === true} onClick={() => void 0} />' },
{ code: '<div aria-hidden={hidden !== false} onClick={() => void 0} />' },
{ code: '<div aria-hidden={hidden != false} onClick={() => void 0} />' },
{ code: '<div aria-hidden={1 < 2} onClick={() => void 0} />' },
{ code: '<div aria-hidden={1 <= 2} onClick={() => void 0} />' },
{ code: '<div aria-hidden={2 > 1} onClick={() => void 0} />' },
{ code: '<div aria-hidden={2 >= 1} onClick={() => void 0} />' },
{ code: '<div onClick={() => void 0} />;' },
{ code: '<div onClick={() => void 0} tabIndex={undefined} />;' },
{ code: '<div onClick={() => void 0} tabIndex="bad" />;' },
{ code: '<div onClick={() => void 0} role={undefined} />;' },
{ code: '<div role="section" onClick={() => void 0} />' },
{ code: '<div onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<div onClick={() => void 0} {...props} />;' },
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input type="hidden" onClick={() => void 0} tabIndex="-1" />' },
{ code: '<input type="hidden" onClick={() => void 0} tabIndex={-1} />' },
{ code: '<input onClick={() => void 0} />' },
{ code: '<input onClick={() => void 0} role="combobox" />' },
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<area href="#" onClick={() => void 0} className="foo" />' },
{ code: '<area onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<a onClick="showNextPage();">Next page</a>' },
{ code: '<a onClick="showNextPage();" tabIndex={undefined}>Next page</a>' },
{ code: '<a onClick="showNextPage();" tabIndex="bad">Next page</a>' },
{ code: '<a onClick={() => void 0} />' },
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
{ code: '<a tabIndex={dynamicTabIndex} onClick={() => void 0} />' },
{ code: '<a tabIndex={0} onClick={() => void 0} />' },
{ code: '<a role="button" href="#" onClick={() => void 0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex={0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" role="button" />' },
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<input onClick={() => void 0} type="hidden" />;' },
{ code: '<span onClick="submitForm();">Submit</span>' },
{ code: '<span onClick="submitForm();" tabIndex={undefined}>Submit</span>' },
{ code: '<span onClick="submitForm();" tabIndex="bad">Submit</span>' },
{ code: '<span onClick="doSomething();" tabIndex="0">Click me!</span>' },
{ code: '<span onClick="doSomething();" tabIndex={0}>Click me!</span>' },
{ code: '<span onClick="doSomething();" tabIndex="-1">Click me too!</span>' },
{
code: '<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>',
},
{ code: '<section onClick={() => void 0} />;' },
{ code: '<main onClick={() => void 0} />;' },
{ code: '<article onClick={() => void 0} />;' },
{ code: '<header onClick={() => void 0} />;' },
{ code: '<footer onClick={() => void 0} />;' },
{ code: '<div role="button" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="checkbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="link" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitem" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitemcheckbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="menuitemradio" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="option" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="radio" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="spinbutton" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="switch" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="tab" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="textbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<Input onClick={() => void 0} type="hidden" />;' },
];
const interactiveRoles = [
'button',
'checkbox',
'link',
'gridcell',
'menuitem',
'menuitemcheckbox',
'menuitemradio',
'option',
'radio',
'searchbox',
'slider',
'spinbutton',
'switch',
'tab',
'textbox',
'treeitem',
];
const recommendedRoles = [
'button',
'checkbox',
'link',
'searchbox',
'spinbutton',
'switch',
'textbox',
];
const strictRoles = [
'button',
'checkbox',
'link',
'progressbar',
'searchbox',
'slider',
'spinbutton',
'switch',
'textbox',
];
const staticElements = [
'div',
];
const triggeringHandlers = [
...eventHandlersByType.mouse,
...eventHandlersByType.keyboard,
];
const passReducer = (roles, handlers, messageTemplate) =>
staticElements.reduce((elementAcc, element) =>
elementAcc.concat(roles.reduce((roleAcc, role) =>
roleAcc.concat(handlers
.map(handler => ({
code: messageTemplate(element, role, handler),
}),
),
), []),
), []);
const failReducer = (roles, handlers, messageTemplate) =>
staticElements.reduce((elementAcc, element) =>
elementAcc.concat(roles.reduce((roleAcc, role) =>
roleAcc.concat(handlers
.map(handler => ({
code: codeTemplate(element, role, handler),
errors: [{
type,
message: messageTemplate(role),
}],
}),
),
), []),
), []);
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: [
...alwaysValid,
...passReducer(
interactiveRoles,
eventHandlers.filter(handler => !includes(triggeringHandlers, handler)),
codeTemplate,
),
...passReducer(
interactiveRoles.filter(role => !includes(recommendedRoles, role)),
eventHandlers.filter(handler => includes(triggeringHandlers, handler)),
tabindexTemplate,
),
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: [
...failReducer(recommendedRoles, triggeringHandlers, tabbableTemplate),
...failReducer(
interactiveRoles.filter(role => !includes(recommendedRoles, role)),
triggeringHandlers,
focusableTemplate,
),
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: [
...alwaysValid,
...passReducer(
interactiveRoles,
eventHandlers.filter(handler => !includes(triggeringHandlers, handler)),
codeTemplate,
),
...passReducer(
interactiveRoles.filter(role => !includes(strictRoles, role)),
eventHandlers.filter(handler => includes(triggeringHandlers, handler)),
tabindexTemplate,
),
]
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
invalid: [
...failReducer(strictRoles, triggeringHandlers, tabbableTemplate),
...failReducer(
interactiveRoles.filter(role => !includes(strictRoles, role)),
triggeringHandlers,
focusableTemplate,
),
]
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,125 @@
/* eslint-env jest */
/**
* @fileoverview Enforce label tags have htmlFor attribute.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/label-has-for';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Form label must have associated control',
type: 'JSXOpeningElement',
};
const array = [{
components: ['Label', 'Descriptor'],
}];
const optionsRequiredNesting = [{
required: 'nesting',
}];
const optionsRequiredSome = [{
required: { some: ['nesting', 'id'] },
}];
const optionsRequiredEvery = [{
required: { every: ['nesting', 'id'] },
}];
ruleTester.run('label-has-for', rule, {
valid: [
// DEFAULT ELEMENT 'label' TESTS
{ code: '<label htmlFor="foo" />' },
{ code: '<label htmlFor={"foo"} />' },
{ code: '<label htmlFor={foo} />' },
{ code: '<label htmlFor={`${id}`} />' },
{ code: '<div />' },
{ code: '<label htmlFor="foo">Test!</label>' },
{ code: '<Label />' }, // lower-case convention refers to real HTML elements.
{ code: '<Label htmlFor="foo" />' },
{ code: '<UX.Layout>test</UX.Layout>' },
// CUSTOM ELEMENT ARRAY OPTION TESTS
{ code: '<Label htmlFor="foo" />', options: array },
{ code: '<Label htmlFor={"foo"} />', options: array },
{ code: '<Label htmlFor={foo} />', options: array },
{ code: '<Label htmlFor={`${id}`} />', options: array },
{ code: '<div />', options: array },
{ code: '<Label htmlFor="foo">Test!</Label>', options: array },
{ code: '<Descriptor htmlFor="foo" />', options: array },
{ code: '<Descriptor htmlFor={"foo"} />', options: array },
{ code: '<Descriptor htmlFor={foo} />', options: array },
{ code: '<Descriptor htmlFor={`${id}`} />', options: array },
{ code: '<div />', options: array },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: array },
{ code: '<label><input /></label>', options: optionsRequiredNesting },
{ code: '<label><input /></label>', options: optionsRequiredSome },
{ code: '<label htmlFor="foo" />', options: optionsRequiredSome },
{ code: '<label htmlFor="foo"><input /></label>', options: optionsRequiredEvery },
].map(parserOptionsMapper),
invalid: [
// DEFAULT ELEMENT 'label' TESTS
{ code: '<label id="foo" />', errors: [expectedError] },
{ code: '<label htmlFor={undefined} />', errors: [expectedError] },
{ code: '<label htmlFor={`${undefined}`} />', errors: [expectedError] },
{ code: '<label>First Name</label>', errors: [expectedError] },
{ code: '<label {...props}>Foo</label>', errors: [expectedError] },
{ code: '<label><input /></label>', errors: [expectedError] },
// CUSTOM ELEMENT ARRAY OPTION TESTS
{ code: '<Label id="foo" />', errors: [expectedError], options: array },
{
code: '<Label htmlFor={undefined} />',
errors: [expectedError],
options: array,
},
{
code: '<Label htmlFor={`${undefined}`} />',
errors: [expectedError],
options: array,
},
{ code: '<Label>First Name</Label>', errors: [expectedError], options: array },
{
code: '<Label {...props}>Foo</Label>',
errors: [expectedError],
options: array,
},
{ code: '<Descriptor id="foo" />', errors: [expectedError], options: array },
{
code: '<Descriptor htmlFor={undefined} />',
errors: [expectedError],
options: array,
},
{
code: '<Descriptor htmlFor={`${undefined}`} />',
errors: [expectedError],
options: array,
},
{
code: '<Descriptor>First Name</Descriptor>',
errors: [expectedError],
options: array,
},
{
code: '<Descriptor {...props}>Foo</Descriptor>',
errors: [expectedError],
options: array,
},
{ code: '<label htmlFor="foo" />', errors: [expectedError], options: optionsRequiredNesting },
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredNesting },
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredSome },
{ code: '<label htmlFor="foo" />', errors: [expectedError], options: optionsRequiredEvery },
{ code: '<label><input /></label>', errors: [expectedError], options: optionsRequiredEvery },
{ code: '<label>First Name</label>', errors: [expectedError], options: optionsRequiredEvery },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,42 @@
/* eslint-env jest */
/**
* @fileoverview Enforce lang attribute has a valid value.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/lang';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'lang attribute must have a valid value.',
type: 'JSXAttribute',
};
ruleTester.run('lang', rule, {
valid: [
{ code: '<div />;' },
{ code: '<div foo="bar" />;' },
{ code: '<div lang="foo" />;' },
{ code: '<html lang="en" />' },
{ code: '<html lang="en-US" />' },
{ code: '<html lang={foo} />' },
{ code: '<HTML lang="foo" />' },
{ code: '<Foo lang="bar" />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<html lang="foo" />', errors: [expectedError] },
{ code: '<html lang="zz-LL" />', errors: [expectedError] },
{ code: '<html lang={undefined} />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,103 @@
/* eslint-env jest */
/**
* @fileoverview <audio> and <video> elements must have a <track> for captions.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/media-has-caption';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Media elements such as <audio> and <video> must have a <track> for captions.',
type: 'JSXOpeningElement',
};
const customSchema = [
{
audio: ['Audio'],
video: ['Video'],
track: ['Track'],
},
];
ruleTester.run('media-has-caption', rule, {
valid: [
{ code: '<div />;' },
{ code: '<MyDiv />;' },
{ code: '<audio><track kind="captions" /></audio>' },
{ code: '<audio><track kind="Captions" /></audio>' },
{
code: '<audio><track kind="Captions" /><track kind="subtitles" /></audio>',
},
{ code: '<video><track kind="captions" /></video>' },
{ code: '<video><track kind="Captions" /></video>' },
{
code: '<video><track kind="Captions" /><track kind="subtitles" /></video>',
},
{
code: '<Audio><track kind="captions" /></Audio>',
options: customSchema,
},
{
code: '<audio><Track kind="captions" /></audio>',
options: customSchema,
},
{
code: '<Video><track kind="captions" /></Video>',
options: customSchema,
},
{
code: '<video><Track kind="captions" /></video>',
options: customSchema,
},
{
code: '<Audio><Track kind="captions" /></Audio>',
options: customSchema,
},
{
code: '<Video><Track kind="captions" /></Video>',
options: customSchema,
},
].map(parserOptionsMapper),
invalid: [
{ code: '<audio><track /></audio>', errors: [expectedError] },
{
code: '<audio><track kind="subtitles" /></audio>',
errors: [expectedError],
},
{ code: '<audio />', errors: [expectedError] },
{ code: '<video><track /></video>', errors: [expectedError] },
{
code: '<video><track kind="subtitles" /></video>',
errors: [expectedError],
},
{ code: '<video />', errors: [expectedError] },
{ code: '<audio>Foo</audio>', errors: [expectedError] },
{ code: '<video>Foo</video>', errors: [expectedError] },
{ code: '<Audio />', options: customSchema, errors: [expectedError] },
{ code: '<Video />', options: customSchema, errors: [expectedError] },
{ code: '<audio><Track /></audio>', options: customSchema, errors: [expectedError] },
{ code: '<video><Track /></video>', options: customSchema, errors: [expectedError] },
{
code: '<Audio><Track kind="subtitles" /></Audio>',
options: customSchema,
errors: [expectedError],
},
{
code: '<Video><Track kind="subtitles" /></Video>',
options: customSchema,
errors: [expectedError],
},
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,67 @@
/* eslint-env jest */
/**
* @fileoverview Enforce onmouseover/onmouseout are accompanied
* by onfocus/onblur.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/mouse-events-have-key-events';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const mouseOverError = {
message: 'onMouseOver must be accompanied by onFocus for accessibility.',
type: 'JSXOpeningElement',
};
const mouseOutError = {
message: 'onMouseOut must be accompanied by onBlur for accessibility.',
type: 'JSXOpeningElement',
};
ruleTester.run('mouse-events-have-key-events', rule, {
valid: [
{ code: '<div onMouseOver={() => void 0} onFocus={() => void 0} />;' },
{
code: '<div onMouseOver={() => void 0} onFocus={() => void 0} {...props} />;',
},
{ code: '<div onMouseOver={handleMouseOver} onFocus={handleFocus} />;' },
{
code: '<div onMouseOver={handleMouseOver} onFocus={handleFocus} {...props} />;',
},
{ code: '<div />;' },
{ code: '<div onMouseOut={() => void 0} onBlur={() => void 0} />' },
{ code: '<div onMouseOut={() => void 0} onBlur={() => void 0} {...props} />' },
{ code: '<div onMouseOut={handleMouseOut} onBlur={handleOnBlur} />' },
{ code: '<div onMouseOut={handleMouseOut} onBlur={handleOnBlur} {...props} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<div onMouseOver={() => void 0} />;', errors: [mouseOverError] },
{ code: '<div onMouseOut={() => void 0} />', errors: [mouseOutError] },
{
code: '<div onMouseOver={() => void 0} onFocus={undefined} />;',
errors: [mouseOverError],
},
{
code: '<div onMouseOut={() => void 0} onBlur={undefined} />',
errors: [mouseOutError],
},
{
code: '<div onMouseOver={() => void 0} {...props} />',
errors: [mouseOverError],
},
{
code: '<div onMouseOut={() => void 0} {...props} />',
errors: [mouseOutError],
},
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,50 @@
/* eslint-env jest */
/**
* @fileoverview Enforce no accesskey attribute on element.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-access-key';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'No access key attribute allowed. Inconsistencies ' +
'between keyboard shortcuts and keyboard comments used by screenreader ' +
'and keyboard only users create a11y complications.',
type: 'JSXOpeningElement',
};
ruleTester.run('no-access-key', rule, {
valid: [
{ code: '<div />;' },
{ code: '<div {...props} />' },
{ code: '<div accessKey={undefined} />' },
{ code: '<div accessKey={`${undefined}`} />' },
{ code: '<div accessKey={`${undefined}${undefined}`} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<div accesskey="h" />', errors: [expectedError] },
{ code: '<div accessKey="h" />', errors: [expectedError] },
{ code: '<div accessKey="h" {...props} />', errors: [expectedError] },
{ code: '<div acCesSKeY="y" />', errors: [expectedError] },
{ code: '<div accessKey={"y"} />', errors: [expectedError] },
{ code: '<div accessKey={`${y}`} />', errors: [expectedError] },
{
code: '<div accessKey={`${undefined}y${undefined}`} />',
errors: [expectedError],
},
{ code: '<div accessKey={`This is ${bad}`} />', errors: [expectedError] },
{ code: '<div accessKey={accessKey} />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,50 @@
/* eslint-env jest */
/**
* @fileoverview Enforce autoFocus prop is not used.
* @author Ethan Cohen <@evcohen>
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-autofocus';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'The autoFocus prop should not be used, as it can reduce usability and accessibility for users.',
type: 'JSXAttribute',
};
const ignoreNonDOMSchema = [
{
ignoreNonDOM: true,
},
];
ruleTester.run('no-autofocus', rule, {
valid: [
{ code: '<div />;' },
{ code: '<div autofocus />;' },
{ code: '<input autofocus="true" />;' },
{ code: '<Foo bar />' },
{ code: '<Foo autoFocus />', options: ignoreNonDOMSchema },
].map(parserOptionsMapper),
invalid: [
{ code: '<div autoFocus />', errors: [expectedError] },
{ code: '<div autoFocus={true} />', errors: [expectedError] },
{ code: '<div autoFocus={false} />', errors: [expectedError] },
{ code: '<div autoFocus={undefined} />', errors: [expectedError] },
{ code: '<div autoFocus="true" />', errors: [expectedError] },
{ code: '<div autoFocus="false" />', errors: [expectedError] },
{ code: '<input autoFocus />', errors: [expectedError] },
{ code: '<Foo autoFocus />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,42 @@
/* eslint-env jest */
/**
* @fileoverview Enforce distracting elements are not used.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-distracting-elements';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = element => ({
message: `Do not use <${element}> elements as they can create visual accessibility issues and are deprecated.`,
type: 'JSXOpeningElement',
});
ruleTester.run('no-marquee', rule, {
valid: [
{ code: '<div />;' },
{ code: '<Marquee />' },
{ code: '<div marquee />' },
{ code: '<Blink />' },
{ code: '<div blink />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<marquee />', errors: [expectedError('marquee')] },
{ code: '<marquee {...props} />', errors: [expectedError('marquee')] },
{ code: '<marquee lang={undefined} />', errors: [expectedError('marquee')] },
{ code: '<blink />', errors: [expectedError('blink')] },
{ code: '<blink {...props} />', errors: [expectedError('blink')] },
{ code: '<blink foo={undefined} />', errors: [expectedError('blink')] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,389 @@
/* eslint-env jest */
/**
* @fileoverview Disallow inherently interactive elements to be assigned
* non-interactive roles.
* @author Jesse Beach
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-interactive-element-to-noninteractive-role';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage =
'Interactive elements should not be assigned non-interactive roles.';
const expectedError = {
message: errorMessage,
type: 'JSXAttribute',
};
const ruleName = 'jsx-a11y/no-interactive-element-to-noninteractive-role';
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
/* Interactive elements */
{ code: '<a href="http://x.y.z" role="button" />' },
{ code: '<a href="http://x.y.z" tabIndex="0" role="button" />' },
{ code: '<button className="foo" role="button" />' },
/* All flavors of input */
{ code: '<input role="button" />' },
{ code: '<input type="button" role="button" />' },
{ code: '<input type="checkbox" role="button" />' },
{ code: '<input type="color" role="button" />' },
{ code: '<input type="date" role="button" />' },
{ code: '<input type="datetime" role="button" />' },
{ code: '<input type="datetime-local" role="button" />' },
{ code: '<input type="email" role="button" />' },
{ code: '<input type="file" role="button" />' },
{ code: '<input type="image" role="button" />' },
{ code: '<input type="month" role="button" />' },
{ code: '<input type="number" role="button" />' },
{ code: '<input type="password" role="button" />' },
{ code: '<input type="radio" role="button" />' },
{ code: '<input type="range" role="button" />' },
{ code: '<input type="reset" role="button" />' },
{ code: '<input type="search" role="button" />' },
{ code: '<input type="submit" role="button" />' },
{ code: '<input type="tel" role="button" />' },
{ code: '<input type="text" role="button" />' },
{ code: '<input type="time" role="button" />' },
{ code: '<input type="url" role="button" />' },
{ code: '<input type="week" role="button" />' },
{ code: '<input type="hidden" role="button" />' },
/* End all flavors of input */
{ code: '<menuitem role="button" />;' },
{ code: '<option className="foo" role="button" />' },
{ code: '<select className="foo" role="button" />' },
{ code: '<textarea className="foo" role="button" />' },
{ code: '<tr role="button" />;' },
/* HTML elements with neither an interactive or non-interactive valence (static) */
{ code: '<a role="button" />' },
{ code: '<a role="img" />;' },
{ code: '<a tabIndex="0" role="button" />' },
{ code: '<a tabIndex="0" role="img" />' },
{ code: '<acronym role="button" />;' },
{ code: '<address role="button" />;' },
{ code: '<applet role="button" />;' },
{ code: '<aside role="button" />;' },
{ code: '<audio role="button" />;' },
{ code: '<b role="button" />;' },
{ code: '<base role="button" />;' },
{ code: '<bdi role="button" />;' },
{ code: '<bdo role="button" />;' },
{ code: '<big role="button" />;' },
{ code: '<blink role="button" />;' },
{ code: '<blockquote role="button" />;' },
{ code: '<body role="button" />;' },
{ code: '<br role="button" />;' },
{ code: '<canvas role="button" />;' },
{ code: '<caption role="button" />;' },
{ code: '<center role="button" />;' },
{ code: '<cite role="button" />;' },
{ code: '<code role="button" />;' },
{ code: '<col role="button" />;' },
{ code: '<colgroup role="button" />;' },
{ code: '<content role="button" />;' },
{ code: '<data role="button" />;' },
{ code: '<datalist role="button" />;' },
{ code: '<del role="button" />;' },
{ code: '<details role="button" />;' },
{ code: '<dir role="button" />;' },
{ code: '<div role="button" />;' },
{ code: '<div className="foo" role="button" />;' },
{ code: '<div className="foo" {...props} role="button" />;' },
{ code: '<div aria-hidden role="button" />;' },
{ code: '<div aria-hidden={true} role="button" />;' },
{ code: '<div role="button" />;' },
{ code: '<div role={undefined} role="button" />;' },
{ code: '<div {...props} role="button" />;' },
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} role="button" />;' },
{ code: '<dl role="button" />;' },
{ code: '<em role="button" />;' },
{ code: '<embed role="button" />;' },
{ code: '<figcaption role="button" />;' },
{ code: '<font role="button" />;' },
{ code: '<footer role="button" />;' },
{ code: '<frameset role="button" />;' },
{ code: '<head role="button" />;' },
{ code: '<header role="button" />;' },
{ code: '<hgroup role="button" />;' },
{ code: '<html role="button" />;' },
{ code: '<i role="button" />;' },
{ code: '<iframe role="button" />;' },
{ code: '<ins role="button" />;' },
{ code: '<kbd role="button" />;' },
{ code: '<keygen role="button" />;' },
{ code: '<label role="button" />;' },
{ code: '<legend role="button" />;' },
{ code: '<link role="button" />;' },
{ code: '<map role="button" />;' },
{ code: '<mark role="button" />;' },
{ code: '<marquee role="button" />;' },
{ code: '<menu role="button" />;' },
{ code: '<meta role="button" />;' },
{ code: '<meter role="button" />;' },
{ code: '<noembed role="button" />;' },
{ code: '<noscript role="button" />;' },
{ code: '<object role="button" />;' },
{ code: '<optgroup role="button" />;' },
{ code: '<output role="button" />;' },
{ code: '<p role="button" />;' },
{ code: '<param role="button" />;' },
{ code: '<picture role="button" />;' },
{ code: '<pre role="button" />;' },
{ code: '<progress role="button" />;' },
{ code: '<q role="button" />;' },
{ code: '<rp role="button" />;' },
{ code: '<rt role="button" />;' },
{ code: '<rtc role="button" />;' },
{ code: '<ruby role="button" />;' },
{ code: '<s role="button" />;' },
{ code: '<samp role="button" />;' },
{ code: '<script role="button" />;' },
{ code: '<section role="button" />;' },
{ code: '<small role="button" />;' },
{ code: '<source role="button" />;' },
{ code: '<spacer role="button" />;' },
{ code: '<span role="button" />;' },
{ code: '<strike role="button" />;' },
{ code: '<strong role="button" />;' },
{ code: '<style role="button" />;' },
{ code: '<sub role="button" />;' },
{ code: '<summary role="button" />;' },
{ code: '<sup role="button" />;' },
{ code: '<th role="button" />;' },
{ code: '<time role="button" />;' },
{ code: '<title role="button" />;' },
{ code: '<track role="button" />;' },
{ code: '<tt role="button" />;' },
{ code: '<u role="button" />;' },
{ code: '<var role="button" />;' },
{ code: '<video role="button" />;' },
{ code: '<wbr role="button" />;' },
{ code: '<xmp role="button" />;' },
/* HTML elements attributed with an interactive role */
{ code: '<div role="button" />;' },
{ code: '<div role="checkbox" />;' },
{ code: '<div role="columnheader" />;' },
{ code: '<div role="combobox" />;' },
{ code: '<div role="grid" />;' },
{ code: '<div role="gridcell" />;' },
{ code: '<div role="link" />;' },
{ code: '<div role="listbox" />;' },
{ code: '<div role="menu" />;' },
{ code: '<div role="menubar" />;' },
{ code: '<div role="menuitem" />;' },
{ code: '<div role="menuitemcheckbox" />;' },
{ code: '<div role="menuitemradio" />;' },
{ code: '<div role="option" />;' },
{ code: '<div role="progressbar" />;' },
{ code: '<div role="radio" />;' },
{ code: '<div role="radiogroup" />;' },
{ code: '<div role="row" />;' },
{ code: '<div role="rowheader" />;' },
{ code: '<div role="searchbox" />;' },
{ code: '<div role="slider" />;' },
{ code: '<div role="spinbutton" />;' },
{ code: '<div role="switch" />;' },
{ code: '<div role="tab" />;' },
{ code: '<div role="textbox" />;' },
{ code: '<div role="treeitem" />;' },
/* Presentation is a special case role that indicates intentional static semantics */
{ code: '<div role="presentation" />;' },
/* HTML elements attributed with an abstract role */
{ code: '<div role="command" />;' },
{ code: '<div role="composite" />;' },
{ code: '<div role="input" />;' },
{ code: '<div role="landmark" />;' },
{ code: '<div role="range" />;' },
{ code: '<div role="roletype" />;' },
{ code: '<div role="section" />;' },
{ code: '<div role="sectionhead" />;' },
{ code: '<div role="select" />;' },
{ code: '<div role="structure" />;' },
{ code: '<div role="tablist" />;' },
{ code: '<div role="toolbar" />;' },
{ code: '<div role="tree" />;' },
{ code: '<div role="treegrid" />;' },
{ code: '<div role="widget" />;' },
{ code: '<div role="window" />;' },
/* HTML elements with an inherent, non-interactive role, assigned an
* interactive role. */
{ code: '<main role="button" />;' },
{ code: '<area role="button" />;' },
{ code: '<article role="button" />;' },
{ code: '<article role="button" />;' },
{ code: '<dd role="button" />;' },
{ code: '<dfn role="button" />;' },
{ code: '<dt role="button" />;' },
{ code: '<fieldset role="button" />;' },
{ code: '<figure role="button" />;' },
{ code: '<form role="button" />;' },
{ code: '<frame role="button" />;' },
{ code: '<h1 role="button" />;' },
{ code: '<h2 role="button" />;' },
{ code: '<h3 role="button" />;' },
{ code: '<h4 role="button" />;' },
{ code: '<h5 role="button" />;' },
{ code: '<h6 role="button" />;' },
{ code: '<hr role="button" />;' },
{ code: '<img role="button" />;' },
{ code: '<li role="button" />;' },
{ code: '<li role="presentation" />;' },
{ code: '<nav role="button" />;' },
{ code: '<ol role="button" />;' },
{ code: '<table role="button" />;' },
{ code: '<tbody role="button" />;' },
{ code: '<td role="button" />;' },
{ code: '<tfoot role="button" />;' },
{ code: '<thead role="button" />;' },
{ code: '<ul role="button" />;' },
/* HTML elements attributed with a non-interactive role */
{ code: '<div role="alert" />;' },
{ code: '<div role="alertdialog" />;' },
{ code: '<div role="application" />;' },
{ code: '<div role="article" />;' },
{ code: '<div role="banner" />;' },
{ code: '<div role="cell" />;' },
{ code: '<div role="complementary" />;' },
{ code: '<div role="contentinfo" />;' },
{ code: '<div role="definition" />;' },
{ code: '<div role="dialog" />;' },
{ code: '<div role="directory" />;' },
{ code: '<div role="document" />;' },
{ code: '<div role="feed" />;' },
{ code: '<div role="figure" />;' },
{ code: '<div role="form" />;' },
{ code: '<div role="group" />;' },
{ code: '<div role="heading" />;' },
{ code: '<div role="img" />;' },
{ code: '<div role="list" />;' },
{ code: '<div role="listitem" />;' },
{ code: '<div role="log" />;' },
{ code: '<div role="main" />;' },
{ code: '<div role="marquee" />;' },
{ code: '<div role="math" />;' },
{ code: '<div role="navigation" />;' },
{ code: '<div role="note" />;' },
{ code: '<div role="region" />;' },
{ code: '<div role="rowgroup" />;' },
{ code: '<div role="search" />;' },
{ code: '<div role="separator" />;' },
{ code: '<div role="scrollbar" />;' },
{ code: '<div role="status" />;' },
{ code: '<div role="table" />;' },
{ code: '<div role="tabpanel" />;' },
{ code: '<div role="term" />;' },
{ code: '<div role="timer" />;' },
{ code: '<div role="tooltip" />;' },
/* Namespaced roles are not checked */
{ code: '<div mynamespace:role="term" />' },
{ code: '<input mynamespace:role="img" />' },
];
const neverValid = [
/* Interactive elements */
{ code: '<a href="http://x.y.z" role="img" />', errors: [expectedError] },
{ code: '<a href="http://x.y.z" tabIndex="0" role="img" />', errors: [expectedError] },
/* All flavors of input */
{ code: '<input role="img" />', errors: [expectedError] },
{ code: '<input type="img" role="img" />', errors: [expectedError] },
{ code: '<input type="checkbox" role="img" />', errors: [expectedError] },
{ code: '<input type="color" role="img" />', errors: [expectedError] },
{ code: '<input type="date" role="img" />', errors: [expectedError] },
{ code: '<input type="datetime" role="img" />', errors: [expectedError] },
{ code: '<input type="datetime-local" role="img" />', errors: [expectedError] },
{ code: '<input type="email" role="img" />', errors: [expectedError] },
{ code: '<input type="file" role="img" />', errors: [expectedError] },
{ code: '<input type="hidden" role="img" />', errors: [expectedError] },
{ code: '<input type="image" role="img" />', errors: [expectedError] },
{ code: '<input type="month" role="img" />', errors: [expectedError] },
{ code: '<input type="number" role="img" />', errors: [expectedError] },
{ code: '<input type="password" role="img" />', errors: [expectedError] },
{ code: '<input type="radio" role="img" />', errors: [expectedError] },
{ code: '<input type="range" role="img" />', errors: [expectedError] },
{ code: '<input type="reset" role="img" />', errors: [expectedError] },
{ code: '<input type="search" role="img" />', errors: [expectedError] },
{ code: '<input type="submit" role="img" />', errors: [expectedError] },
{ code: '<input type="tel" role="img" />', errors: [expectedError] },
{ code: '<input type="text" role="img" />', errors: [expectedError] },
{ code: '<input type="time" role="img" />', errors: [expectedError] },
{ code: '<input type="url" role="img" />', errors: [expectedError] },
{ code: '<input type="week" role="img" />', errors: [expectedError] },
/* End all flavors of input */
{ code: '<menuitem role="img" />;', errors: [expectedError] },
{ code: '<option className="foo" role="img" />', errors: [expectedError] },
{ code: '<select className="foo" role="img" />', errors: [expectedError] },
{ code: '<textarea className="foo" role="img" />', errors: [expectedError] },
{ code: '<tr role="img" />;', errors: [expectedError] },
/* Interactive elements */
{ code: '<a href="http://x.y.z" role="listitem" />', errors: [expectedError] },
{ code: '<a href="http://x.y.z" tabIndex="0" role="listitem" />', errors: [expectedError] },
/* All flavors of input */
{ code: '<input role="listitem" />', errors: [expectedError] },
{ code: '<input type="listitem" role="listitem" />', errors: [expectedError] },
{ code: '<input type="checkbox" role="listitem" />', errors: [expectedError] },
{ code: '<input type="color" role="listitem" />', errors: [expectedError] },
{ code: '<input type="date" role="listitem" />', errors: [expectedError] },
{ code: '<input type="datetime" role="listitem" />', errors: [expectedError] },
{ code: '<input type="datetime-local" role="listitem" />', errors: [expectedError] },
{ code: '<input type="email" role="listitem" />', errors: [expectedError] },
{ code: '<input type="file" role="listitem" />', errors: [expectedError] },
{ code: '<input type="image" role="listitem" />', errors: [expectedError] },
{ code: '<input type="month" role="listitem" />', errors: [expectedError] },
{ code: '<input type="number" role="listitem" />', errors: [expectedError] },
{ code: '<input type="password" role="listitem" />', errors: [expectedError] },
{ code: '<input type="radio" role="listitem" />', errors: [expectedError] },
{ code: '<input type="range" role="listitem" />', errors: [expectedError] },
{ code: '<input type="reset" role="listitem" />', errors: [expectedError] },
{ code: '<input type="search" role="listitem" />', errors: [expectedError] },
{ code: '<input type="submit" role="listitem" />', errors: [expectedError] },
{ code: '<input type="tel" role="listitem" />', errors: [expectedError] },
{ code: '<input type="text" role="listitem" />', errors: [expectedError] },
{ code: '<input type="time" role="listitem" />', errors: [expectedError] },
{ code: '<input type="url" role="listitem" />', errors: [expectedError] },
{ code: '<input type="week" role="listitem" />', errors: [expectedError] },
/* End all flavors of input */
{ code: '<menuitem role="listitem" />;', errors: [expectedError] },
{ code: '<option className="foo" role="listitem" />', errors: [expectedError] },
{ code: '<select className="foo" role="listitem" />', errors: [expectedError] },
{ code: '<textarea className="foo" role="listitem" />', errors: [expectedError] },
{ code: '<tr role="listitem" />;', errors: [expectedError] },
];
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: [
...alwaysValid,
{ code: '<tr role="presentation" />;' },
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: [
...neverValid,
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: [
...alwaysValid,
].map(parserOptionsMapper),
invalid: [
...neverValid,
{ code: '<tr role="presentation" />;', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,457 @@
/* eslint-env jest */
/**
* @fileoverview Enforce non-interactive elements have no interactive handlers.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-noninteractive-element-interactions';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage =
'Non-interactive elements should not be assigned mouse or keyboard event listeners.';
const expectedError = {
message: errorMessage,
type: 'JSXOpeningElement',
};
const ruleName = 'no-noninteractive-element-interactions';
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
/* All flavors of input */
{ code: '<input onClick={() => void 0} />' },
{ code: '<input type="button" onClick={() => void 0} />' },
{ code: '<input type="checkbox" onClick={() => void 0} />' },
{ code: '<input type="color" onClick={() => void 0} />' },
{ code: '<input type="date" onClick={() => void 0} />' },
{ code: '<input type="datetime" onClick={() => void 0} />' },
{ code: '<input type="datetime-local" onClick={() => void 0} />' },
{ code: '<input type="email" onClick={() => void 0} />' },
{ code: '<input type="file" onClick={() => void 0} />' },
{ code: '<input type="image" onClick={() => void 0} />' },
{ code: '<input type="month" onClick={() => void 0} />' },
{ code: '<input type="number" onClick={() => void 0} />' },
{ code: '<input type="password" onClick={() => void 0} />' },
{ code: '<input type="radio" onClick={() => void 0} />' },
{ code: '<input type="range" onClick={() => void 0} />' },
{ code: '<input type="reset" onClick={() => void 0} />' },
{ code: '<input type="search" onClick={() => void 0} />' },
{ code: '<input type="submit" onClick={() => void 0} />' },
{ code: '<input type="tel" onClick={() => void 0} />' },
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input type="time" onClick={() => void 0} />' },
{ code: '<input type="url" onClick={() => void 0} />' },
{ code: '<input type="week" onClick={() => void 0} />' },
{ code: '<input type="hidden" onClick={() => void 0} />' },
/* End all flavors of input */
{ code: '<a onClick={() => void 0} />' },
{ code: '<a onClick={() => {}} />;' },
{ code: '<a tabIndex="0" onClick={() => void 0} />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<area onClick={() => {}} />;' },
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<menuitem onClick={() => {}} />;' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<tr onClick={() => {}} />;' },
/* HTML elements with neither an interactive or non-interactive valence (static) */
{ code: '<acronym onClick={() => {}} />;' },
{ code: '<address onClick={() => {}} />;' },
{ code: '<applet onClick={() => {}} />;' },
{ code: '<aside onClick={() => {}} />;' },
{ code: '<audio onClick={() => {}} />;' },
{ code: '<b onClick={() => {}} />;' },
{ code: '<base onClick={() => {}} />;' },
{ code: '<bdi onClick={() => {}} />;' },
{ code: '<bdo onClick={() => {}} />;' },
{ code: '<big onClick={() => {}} />;' },
{ code: '<blink onClick={() => {}} />;' },
{ code: '<body onClick={() => {}} />;' },
{ code: '<canvas onClick={() => {}} />;' },
{ code: '<center onClick={() => {}} />;' },
{ code: '<cite onClick={() => {}} />;' },
{ code: '<code onClick={() => {}} />;' },
{ code: '<col onClick={() => {}} />;' },
{ code: '<colgroup onClick={() => {}} />;' },
{ code: '<content onClick={() => {}} />;' },
{ code: '<data onClick={() => {}} />;' },
{ code: '<datalist onClick={() => {}} />;' },
{ code: '<del onClick={() => {}} />;' },
{ code: '<div />;' },
{ code: '<div className="foo" />;' },
{ code: '<div className="foo" {...props} />;' },
{ code: '<div onClick={() => void 0} aria-hidden />;' },
{ code: '<div onClick={() => void 0} aria-hidden={true} />;' },
{ code: '<div onClick={() => void 0} />;' },
{ code: '<div onClick={() => void 0} role={undefined} />;' },
{ code: '<div onClick={() => void 0} {...props} />;' },
{ code: '<div onClick={null} />;' },
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} />;' },
{ code: '<em onClick={() => {}} />;' },
{ code: '<embed onClick={() => {}} />;' },
{ code: '<font onClick={() => {}} />;' },
{ code: '<frameset onClick={() => {}} />;' },
{ code: '<head onClick={() => {}} />;' },
{ code: '<header onClick={() => {}} />;' },
{ code: '<hgroup onClick={() => {}} />;' },
{ code: '<html onClick={() => {}} />;' },
{ code: '<i onClick={() => {}} />;' },
{ code: '<ins onClick={() => {}} />;' },
{ code: '<kbd onClick={() => {}} />;' },
{ code: '<keygen onClick={() => {}} />;' },
{ code: '<link onClick={() => {}} />;' },
{ code: '<main onClick={null} />;' },
{ code: '<map onClick={() => {}} />;' },
{ code: '<meta onClick={() => {}} />;' },
{ code: '<noembed onClick={() => {}} />;' },
{ code: '<noscript onClick={() => {}} />;' },
{ code: '<object onClick={() => {}} />;' },
{ code: '<optgroup onClick={() => {}} />;' },
{ code: '<output onClick={() => {}} />;' },
{ code: '<param onClick={() => {}} />;' },
{ code: '<picture onClick={() => {}} />;' },
{ code: '<q onClick={() => {}} />;' },
{ code: '<rp onClick={() => {}} />;' },
{ code: '<rt onClick={() => {}} />;' },
{ code: '<rtc onClick={() => {}} />;' },
{ code: '<s onClick={() => {}} />;' },
{ code: '<samp onClick={() => {}} />;' },
{ code: '<script onClick={() => {}} />;' },
{ code: '<section onClick={() => {}} />;' },
{ code: '<small onClick={() => {}} />;' },
{ code: '<source onClick={() => {}} />;' },
{ code: '<spacer onClick={() => {}} />;' },
{ code: '<span onClick={() => {}} />;' },
{ code: '<strike onClick={() => {}} />;' },
{ code: '<strong onClick={() => {}} />;' },
{ code: '<style onClick={() => {}} />;' },
{ code: '<sub onClick={() => {}} />;' },
{ code: '<summary onClick={() => {}} />;' },
{ code: '<sup onClick={() => {}} />;' },
{ code: '<th onClick={() => {}} />;' },
{ code: '<title onClick={() => {}} />;' },
{ code: '<track onClick={() => {}} />;' },
{ code: '<tt onClick={() => {}} />;' },
{ code: '<u onClick={() => {}} />;' },
{ code: '<var onClick={() => {}} />;' },
{ code: '<video onClick={() => {}} />;' },
{ code: '<wbr onClick={() => {}} />;' },
{ code: '<xmp onClick={() => {}} />;' },
/* HTML elements attributed with an interactive role */
{ code: '<div role="button" onClick={() => {}} />;' },
{ code: '<div role="checkbox" onClick={() => {}} />;' },
{ code: '<div role="columnheader" onClick={() => {}} />;' },
{ code: '<div role="combobox" onClick={() => {}} />;' },
{ code: '<div role="grid" onClick={() => {}} />;' },
{ code: '<div role="gridcell" onClick={() => {}} />;' },
{ code: '<div role="link" onClick={() => {}} />;' },
{ code: '<div role="listbox" onClick={() => {}} />;' },
{ code: '<div role="menu" onClick={() => {}} />;' },
{ code: '<div role="menubar" onClick={() => {}} />;' },
{ code: '<div role="menuitem" onClick={() => {}} />;' },
{ code: '<div role="menuitemcheckbox" onClick={() => {}} />;' },
{ code: '<div role="menuitemradio" onClick={() => {}} />;' },
{ code: '<div role="option" onClick={() => {}} />;' },
{ code: '<div role="progressbar" onClick={() => {}} />;' },
{ code: '<div role="radio" onClick={() => {}} />;' },
{ code: '<div role="radiogroup" onClick={() => {}} />;' },
{ code: '<div role="row" onClick={() => {}} />;' },
{ code: '<div role="rowheader" onClick={() => {}} />;' },
{ code: '<div role="searchbox" onClick={() => {}} />;' },
{ code: '<div role="slider" onClick={() => {}} />;' },
{ code: '<div role="spinbutton" onClick={() => {}} />;' },
{ code: '<div role="switch" onClick={() => {}} />;' },
{ code: '<div role="tab" onClick={() => {}} />;' },
{ code: '<div role="textbox" onClick={() => {}} />;' },
{ code: '<div role="treeitem" onClick={() => {}} />;' },
/* Presentation is a special case role that indicates intentional static semantics */
{ code: '<div role="presentation" onClick={() => {}} />;' },
/* HTML elements attributed with an abstract role */
{ code: '<div role="command" onClick={() => {}} />;' },
{ code: '<div role="composite" onClick={() => {}} />;' },
{ code: '<div role="input" onClick={() => {}} />;' },
{ code: '<div role="landmark" onClick={() => {}} />;' },
{ code: '<div role="range" onClick={() => {}} />;' },
{ code: '<div role="roletype" onClick={() => {}} />;' },
{ code: '<div role="section" onClick={() => {}} />;' },
{ code: '<div role="sectionhead" onClick={() => {}} />;' },
{ code: '<div role="select" onClick={() => {}} />;' },
{ code: '<div role="structure" onClick={() => {}} />;' },
{ code: '<div role="tablist" onClick={() => {}} />;' },
{ code: '<div role="toolbar" onClick={() => {}} />;' },
{ code: '<div role="tree" onClick={() => {}} />;' },
{ code: '<div role="treegrid" onClick={() => {}} />;' },
{ code: '<div role="widget" onClick={() => {}} />;' },
{ code: '<div role="window" onClick={() => {}} />;' },
];
const neverValid = [
/* HTML elements with an inherent, non-interactive role */
{ code: '<main onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<article onClick={() => {}} />;', errors: [expectedError] },
{ code: '<blockquote onClick={() => {}} />;', errors: [expectedError] },
{ code: '<br onClick={() => {}} />;', errors: [expectedError] },
{ code: '<caption onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dd onClick={() => {}} />;', errors: [expectedError] },
{ code: '<details onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dfn onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dl onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dir onClick={() => {}} />;', errors: [expectedError] },
{ code: '<dt onClick={() => {}} />;', errors: [expectedError] },
{ code: '<fieldset onClick={() => {}} />;', errors: [expectedError] },
{ code: '<figcaption onClick={() => {}} />;', errors: [expectedError] },
{ code: '<figure onClick={() => {}} />;', errors: [expectedError] },
{ code: '<footer onClick={() => {}} />;', errors: [expectedError] },
{ code: '<form onClick={() => {}} />;', errors: [expectedError] },
{ code: '<frame onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h1 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h2 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h3 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h4 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h5 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<h6 onClick={() => {}} />;', errors: [expectedError] },
{ code: '<hr onClick={() => {}} />;', errors: [expectedError] },
{ code: '<iframe onClick={() => {}} />;', errors: [expectedError] },
{ code: '<img onClick={() => {}} />;', errors: [expectedError] },
{ code: '<label onClick={() => {}} />;', errors: [expectedError] },
{ code: '<legend onClick={() => {}} />;', errors: [expectedError] },
{ code: '<li onClick={() => {}} />;', errors: [expectedError] },
{ code: '<mark onClick={() => {}} />;', errors: [expectedError] },
{ code: '<marquee onClick={() => {}} />;', errors: [expectedError] },
{ code: '<menu onClick={() => {}} />;', errors: [expectedError] },
{ code: '<meter onClick={() => {}} />;', errors: [expectedError] },
{ code: '<nav onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ol onClick={() => {}} />;', errors: [expectedError] },
{ code: '<p onClick={() => {}} />;', errors: [expectedError] },
{ code: '<pre onClick={() => {}} />;', errors: [expectedError] },
{ code: '<progress onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ruby onClick={() => {}} />;', errors: [expectedError] },
{ code: '<table onClick={() => {}} />;', errors: [expectedError] },
{ code: '<tbody onClick={() => {}} />;', errors: [expectedError] },
{ code: '<td onClick={() => {}} />;', errors: [expectedError] },
{ code: '<tfoot onClick={() => {}} />;', errors: [expectedError] },
{ code: '<thead onClick={() => {}} />;', errors: [expectedError] },
{ code: '<time onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ol onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ul onClick={() => {}} />;', errors: [expectedError] },
/* HTML elements attributed with a non-interactive role */
{ code: '<div role="alert" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="alertdialog" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="application" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="banner" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="cell" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="complementary" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="contentinfo" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="definition" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="dialog" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="directory" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="document" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="feed" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="figure" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="form" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="group" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="heading" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="img" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="list" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="listitem" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="log" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="main" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="marquee" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="math" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="navigation" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="note" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="region" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="rowgroup" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="search" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="separator" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="scrollbar" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="status" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="table" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="tabpanel" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="term" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="timer" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="tooltip" onClick={() => {}} />;', errors: [expectedError] },
];
const recommendedOptions =
(configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: [
...alwaysValid,
// All the possible handlers
{ code: '<div role="article" onCopy={() => {}} />;' },
{ code: '<div role="article" onCut={() => {}} />;' },
{ code: '<div role="article" onPaste={() => {}} />;' },
{ code: '<div role="article" onCompositionEnd={() => {}} />;' },
{ code: '<div role="article" onCompositionStart={() => {}} />;' },
{ code: '<div role="article" onCompositionUpdate={() => {}} />;' },
{ code: '<div role="article" onFocus={() => {}} />;' },
{ code: '<div role="article" onBlur={() => {}} />;' },
{ code: '<div role="article" onChange={() => {}} />;' },
{ code: '<div role="article" onInput={() => {}} />;' },
{ code: '<div role="article" onSubmit={() => {}} />;' },
{ code: '<div role="article" onContextMenu={() => {}} />;' },
{ code: '<div role="article" onDblClick={() => {}} />;' },
{ code: '<div role="article" onDoubleClick={() => {}} />;' },
{ code: '<div role="article" onDrag={() => {}} />;' },
{ code: '<div role="article" onDragEnd={() => {}} />;' },
{ code: '<div role="article" onDragEnter={() => {}} />;' },
{ code: '<div role="article" onDragExit={() => {}} />;' },
{ code: '<div role="article" onDragLeave={() => {}} />;' },
{ code: '<div role="article" onDragOver={() => {}} />;' },
{ code: '<div role="article" onDragStart={() => {}} />;' },
{ code: '<div role="article" onDrop={() => {}} />;' },
{ code: '<div role="article" onMouseEnter={() => {}} />;' },
{ code: '<div role="article" onMouseLeave={() => {}} />;' },
{ code: '<div role="article" onMouseMove={() => {}} />;' },
{ code: '<div role="article" onMouseOut={() => {}} />;' },
{ code: '<div role="article" onMouseOver={() => {}} />;' },
{ code: '<div role="article" onSelect={() => {}} />;' },
{ code: '<div role="article" onTouchCancel={() => {}} />;' },
{ code: '<div role="article" onTouchEnd={() => {}} />;' },
{ code: '<div role="article" onTouchMove={() => {}} />;' },
{ code: '<div role="article" onTouchStart={() => {}} />;' },
{ code: '<div role="article" onScroll={() => {}} />;' },
{ code: '<div role="article" onWheel={() => {}} />;' },
{ code: '<div role="article" onAbort={() => {}} />;' },
{ code: '<div role="article" onCanPlay={() => {}} />;' },
{ code: '<div role="article" onCanPlayThrough={() => {}} />;' },
{ code: '<div role="article" onDurationChange={() => {}} />;' },
{ code: '<div role="article" onEmptied={() => {}} />;' },
{ code: '<div role="article" onEncrypted={() => {}} />;' },
{ code: '<div role="article" onEnded={() => {}} />;' },
{ code: '<div role="article" onError={() => {}} />;' },
{ code: '<div role="article" onLoadedData={() => {}} />;' },
{ code: '<div role="article" onLoadedMetadata={() => {}} />;' },
{ code: '<div role="article" onLoadStart={() => {}} />;' },
{ code: '<div role="article" onPause={() => {}} />;' },
{ code: '<div role="article" onPlay={() => {}} />;' },
{ code: '<div role="article" onPlaying={() => {}} />;' },
{ code: '<div role="article" onProgress={() => {}} />;' },
{ code: '<div role="article" onRateChange={() => {}} />;' },
{ code: '<div role="article" onSeeked={() => {}} />;' },
{ code: '<div role="article" onSeeking={() => {}} />;' },
{ code: '<div role="article" onStalled={() => {}} />;' },
{ code: '<div role="article" onSuspend={() => {}} />;' },
{ code: '<div role="article" onTimeUpdate={() => {}} />;' },
{ code: '<div role="article" onVolumeChange={() => {}} />;' },
{ code: '<div role="article" onWaiting={() => {}} />;' },
{ code: '<div role="article" onLoad={() => {}} />;' },
{ code: '<div role="article" onError={() => {}} />;' },
{ code: '<div role="article" onAnimationStart={() => {}} />;' },
{ code: '<div role="article" onAnimationEnd={() => {}} />;' },
{ code: '<div role="article" onAnimationIteration={() => {}} />;' },
{ code: '<div role="article" onTransitionEnd={() => {}} />;' },
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: [
...neverValid,
// All the possible handlers
{ code: '<div role="article" onKeyDown={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onKeyPress={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onKeyUp={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseDown={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseUp={() => {}} />;', errors: [expectedError] },
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: [
...alwaysValid,
].map(parserOptionsMapper),
invalid: [
...neverValid,
// All the possible handlers
{ code: '<div role="article" onCopy={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onCut={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onPaste={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onCompositionEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onCompositionStart={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onCompositionUpdate={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onKeyDown={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onKeyPress={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onKeyUp={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onFocus={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onBlur={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onChange={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onInput={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onSubmit={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onContextMenu={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDblClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDoubleClick={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDrag={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragExit={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragOver={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDragStart={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDrop={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseDown={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseMove={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseOut={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseOver={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseUp={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onSelect={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onTouchCancel={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onTouchEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onTouchMove={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onTouchStart={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onScroll={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onWheel={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onAbort={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onCanPlay={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onCanPlayThrough={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onDurationChange={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onEmptied={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onEncrypted={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onEnded={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onError={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onLoadedData={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onLoadedMetadata={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onLoadStart={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onPause={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onPlay={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onPlaying={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onProgress={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onRateChange={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onSeeked={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onSeeking={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onStalled={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onSuspend={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onTimeUpdate={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onVolumeChange={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onWaiting={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onLoad={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onError={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onAnimationStart={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onAnimationEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onAnimationIteration={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onTransitionEnd={() => {}} />;', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,483 @@
/* eslint-env jest */
/**
* @fileoverview Disallow inherently non-interactive elements to be assigned
* interactive roles.
* @author Jesse Beach
* @author $AUTHOR
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-noninteractive-element-to-interactive-role';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage =
'Non-interactive elements should not be assigned interactive roles.';
const expectedError = {
message: errorMessage,
type: 'JSXAttribute',
};
const ruleName = 'jsx-a11y/no-noninteractive-element-to-interactive-role';
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
/* Interactive elements */
{ code: '<a tabIndex="0" role="button" />' },
{ code: '<a href="http://x.y.z" role="button" />' },
{ code: '<a href="http://x.y.z" tabIndex="0" role="button" />' },
{ code: '<area role="button" />;' },
{ code: '<area role="menuitem" />;' },
{ code: '<button className="foo" role="button" />' },
/* All flavors of input */
{ code: '<input role="button" />' },
{ code: '<input type="button" role="button" />' },
{ code: '<input type="checkbox" role="button" />' },
{ code: '<input type="color" role="button" />' },
{ code: '<input type="date" role="button" />' },
{ code: '<input type="datetime" role="button" />' },
{ code: '<input type="datetime-local" role="button" />' },
{ code: '<input type="email" role="button" />' },
{ code: '<input type="file" role="button" />' },
{ code: '<input type="hidden" role="button" />' },
{ code: '<input type="image" role="button" />' },
{ code: '<input type="month" role="button" />' },
{ code: '<input type="number" role="button" />' },
{ code: '<input type="password" role="button" />' },
{ code: '<input type="radio" role="button" />' },
{ code: '<input type="range" role="button" />' },
{ code: '<input type="reset" role="button" />' },
{ code: '<input type="search" role="button" />' },
{ code: '<input type="submit" role="button" />' },
{ code: '<input type="tel" role="button" />' },
{ code: '<input type="text" role="button" />' },
{ code: '<input type="time" role="button" />' },
{ code: '<input type="url" role="button" />' },
{ code: '<input type="week" role="button" />' },
{ code: '<input type="hidden" role="img" />' },
/* End all flavors of input */
{ code: '<menuitem role="button" />;' },
{ code: '<option className="foo" role="button" />' },
{ code: '<select className="foo" role="button" />' },
{ code: '<textarea className="foo" role="button" />' },
{ code: '<tr role="button" />;' },
{ code: '<tr role="presentation" />;' },
/* Interactive elements */
{ code: '<a tabIndex="0" role="img" />' },
{ code: '<a href="http://x.y.z" role="img" />' },
{ code: '<a href="http://x.y.z" tabIndex="0" role="img" />' },
/* All flavors of input */
{ code: '<input role="img" />' },
{ code: '<input type="img" role="img" />' },
{ code: '<input type="checkbox" role="img" />' },
{ code: '<input type="color" role="img" />' },
{ code: '<input type="date" role="img" />' },
{ code: '<input type="datetime" role="img" />' },
{ code: '<input type="datetime-local" role="img" />' },
{ code: '<input type="email" role="img" />' },
{ code: '<input type="file" role="img" />' },
{ code: '<input type="hidden" role="button" />' },
{ code: '<input type="image" role="img" />' },
{ code: '<input type="month" role="img" />' },
{ code: '<input type="number" role="img" />' },
{ code: '<input type="password" role="img" />' },
{ code: '<input type="radio" role="img" />' },
{ code: '<input type="range" role="img" />' },
{ code: '<input type="reset" role="img" />' },
{ code: '<input type="search" role="img" />' },
{ code: '<input type="submit" role="img" />' },
{ code: '<input type="tel" role="img" />' },
{ code: '<input type="text" role="img" />' },
{ code: '<input type="time" role="img" />' },
{ code: '<input type="url" role="img" />' },
{ code: '<input type="week" role="img" />' },
/* End all flavors of input */
{ code: '<menuitem role="img" />;' },
{ code: '<option className="foo" role="img" />' },
{ code: '<select className="foo" role="img" />' },
{ code: '<textarea className="foo" role="img" />' },
{ code: '<tr role="img" />;' },
/* Interactive elements */
{ code: '<a tabIndex="0" role="listitem" />' },
{ code: '<a href="http://x.y.z" role="listitem" />' },
{ code: '<a href="http://x.y.z" tabIndex="0" role="listitem" />' },
/* All flavors of input */
{ code: '<input role="listitem" />' },
{ code: '<input type="listitem" role="listitem" />' },
{ code: '<input type="checkbox" role="listitem" />' },
{ code: '<input type="color" role="listitem" />' },
{ code: '<input type="date" role="listitem" />' },
{ code: '<input type="datetime" role="listitem" />' },
{ code: '<input type="datetime-local" role="listitem" />' },
{ code: '<input type="email" role="listitem" />' },
{ code: '<input type="file" role="listitem" />' },
{ code: '<input type="image" role="listitem" />' },
{ code: '<input type="month" role="listitem" />' },
{ code: '<input type="number" role="listitem" />' },
{ code: '<input type="password" role="listitem" />' },
{ code: '<input type="radio" role="listitem" />' },
{ code: '<input type="range" role="listitem" />' },
{ code: '<input type="reset" role="listitem" />' },
{ code: '<input type="search" role="listitem" />' },
{ code: '<input type="submit" role="listitem" />' },
{ code: '<input type="tel" role="listitem" />' },
{ code: '<input type="text" role="listitem" />' },
{ code: '<input type="time" role="listitem" />' },
{ code: '<input type="url" role="listitem" />' },
{ code: '<input type="week" role="listitem" />' },
/* End all flavors of input */
{ code: '<menuitem role="listitem" />;' },
{ code: '<option className="foo" role="listitem" />' },
{ code: '<select className="foo" role="listitem" />' },
{ code: '<textarea className="foo" role="listitem" />' },
{ code: '<tr role="listitem" />;' },
/* HTML elements with neither an interactive or non-interactive valence (static) */
{ code: '<acronym role="button" />;' },
{ code: '<address role="button" />;' },
{ code: '<applet role="button" />;' },
{ code: '<aside role="button" />;' },
{ code: '<audio role="button" />;' },
{ code: '<b role="button" />;' },
{ code: '<base role="button" />;' },
{ code: '<bdi role="button" />;' },
{ code: '<bdo role="button" />;' },
{ code: '<big role="button" />;' },
{ code: '<blink role="button" />;' },
{ code: '<body role="button" />;' },
{ code: '<canvas role="button" />;' },
{ code: '<center role="button" />;' },
{ code: '<cite role="button" />;' },
{ code: '<code role="button" />;' },
{ code: '<col role="button" />;' },
{ code: '<colgroup role="button" />;' },
{ code: '<content role="button" />;' },
{ code: '<data role="button" />;' },
{ code: '<datalist role="button" />;' },
{ code: '<del role="button" />;' },
{ code: '<div role="button" />;' },
{ code: '<div className="foo" role="button" />;' },
{ code: '<div className="foo" {...props} role="button" />;' },
{ code: '<div aria-hidden role="button" />;' },
{ code: '<div aria-hidden={true} role="button" />;' },
{ code: '<div role="button" />;' },
{ code: '<div role={undefined} role="button" />;' },
{ code: '<div {...props} role="button" />;' },
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} role="button" />;' },
{ code: '<em role="button" />;' },
{ code: '<embed role="button" />;' },
{ code: '<font role="button" />;' },
{ code: '<frameset role="button" />;' },
{ code: '<head role="button" />;' },
{ code: '<header role="button" />;' },
{ code: '<hgroup role="button" />;' },
{ code: '<html role="button" />;' },
{ code: '<i role="button" />;' },
{ code: '<ins role="button" />;' },
{ code: '<kbd role="button" />;' },
{ code: '<keygen role="button" />;' },
{ code: '<link role="button" />;' },
{ code: '<map role="button" />;' },
{ code: '<meta role="button" />;' },
{ code: '<noembed role="button" />;' },
{ code: '<noscript role="button" />;' },
{ code: '<object role="button" />;' },
{ code: '<optgroup role="button" />;' },
{ code: '<output role="button" />;' },
{ code: '<param role="button" />;' },
{ code: '<picture role="button" />;' },
{ code: '<q role="button" />;' },
{ code: '<rp role="button" />;' },
{ code: '<rt role="button" />;' },
{ code: '<rtc role="button" />;' },
{ code: '<s role="button" />;' },
{ code: '<samp role="button" />;' },
{ code: '<script role="button" />;' },
{ code: '<section role="button" />;' },
{ code: '<small role="button" />;' },
{ code: '<source role="button" />;' },
{ code: '<spacer role="button" />;' },
{ code: '<span role="button" />;' },
{ code: '<strike role="button" />;' },
{ code: '<strong role="button" />;' },
{ code: '<style role="button" />;' },
{ code: '<sub role="button" />;' },
{ code: '<summary role="button" />;' },
{ code: '<sup role="button" />;' },
{ code: '<th role="button" />;' },
{ code: '<title role="button" />;' },
{ code: '<track role="button" />;' },
{ code: '<tt role="button" />;' },
{ code: '<u role="button" />;' },
{ code: '<var role="button" />;' },
{ code: '<video role="button" />;' },
{ code: '<wbr role="button" />;' },
{ code: '<xmp role="button" />;' },
/* HTML elements attributed with an interactive role */
{ code: '<div role="button" />;' },
{ code: '<div role="checkbox" />;' },
{ code: '<div role="columnheader" />;' },
{ code: '<div role="combobox" />;' },
{ code: '<div role="grid" />;' },
{ code: '<div role="gridcell" />;' },
{ code: '<div role="link" />;' },
{ code: '<div role="listbox" />;' },
{ code: '<div role="menu" />;' },
{ code: '<div role="menubar" />;' },
{ code: '<div role="menuitem" />;' },
{ code: '<div role="menuitemcheckbox" />;' },
{ code: '<div role="menuitemradio" />;' },
{ code: '<div role="option" />;' },
{ code: '<div role="progressbar" />;' },
{ code: '<div role="radio" />;' },
{ code: '<div role="radiogroup" />;' },
{ code: '<div role="row" />;' },
{ code: '<div role="rowheader" />;' },
{ code: '<div role="searchbox" />;' },
{ code: '<div role="slider" />;' },
{ code: '<div role="spinbutton" />;' },
{ code: '<div role="switch" />;' },
{ code: '<div role="tab" />;' },
{ code: '<div role="textbox" />;' },
{ code: '<div role="treeitem" />;' },
/* Presentation is a special case role that indicates intentional static semantics */
{ code: '<div role="presentation" />;' },
/* HTML elements attributed with an abstract role */
{ code: '<div role="command" />;' },
{ code: '<div role="composite" />;' },
{ code: '<div role="input" />;' },
{ code: '<div role="landmark" />;' },
{ code: '<div role="range" />;' },
{ code: '<div role="roletype" />;' },
{ code: '<div role="section" />;' },
{ code: '<div role="sectionhead" />;' },
{ code: '<div role="select" />;' },
{ code: '<div role="structure" />;' },
{ code: '<div role="tablist" />;' },
{ code: '<div role="toolbar" />;' },
{ code: '<div role="tree" />;' },
{ code: '<div role="treegrid" />;' },
{ code: '<div role="widget" />;' },
{ code: '<div role="window" />;' },
/* HTML elements with an inherent non-interactive role, assigned an
* interactive role. */
{ code: '<main role="listitem" />;' },
{ code: '<a role="listitem" />' },
{ code: '<a role="listitem" />;' },
{ code: '<a role="button" />' },
{ code: '<a role="button" />;' },
{ code: '<a role="menuitem" />' },
{ code: '<a role="menuitem" />;' },
{ code: '<area role="listitem" />;' },
{ code: '<article role="listitem" />;' },
{ code: '<article role="listitem" />;' },
{ code: '<dd role="listitem" />;' },
{ code: '<dfn role="listitem" />;' },
{ code: '<dt role="listitem" />;' },
{ code: '<fieldset role="listitem" />;' },
{ code: '<figure role="listitem" />;' },
{ code: '<form role="listitem" />;' },
{ code: '<frame role="listitem" />;' },
{ code: '<h1 role="listitem" />;' },
{ code: '<h2 role="listitem" />;' },
{ code: '<h3 role="listitem" />;' },
{ code: '<h4 role="listitem" />;' },
{ code: '<h5 role="listitem" />;' },
{ code: '<h6 role="listitem" />;' },
{ code: '<hr role="listitem" />;' },
{ code: '<img role="listitem" />;' },
{ code: '<li role="listitem" />;' },
{ code: '<li role="presentation" />;' },
{ code: '<nav role="listitem" />;' },
{ code: '<ol role="listitem" />;' },
{ code: '<table role="listitem" />;' },
{ code: '<tbody role="listitem" />;' },
{ code: '<td role="listitem" />;' },
{ code: '<tfoot role="listitem" />;' },
{ code: '<thead role="listitem" />;' },
{ code: '<ul role="listitem" />;' },
/* HTML elements attributed with a non-interactive role */
{ code: '<div role="alert" />;' },
{ code: '<div role="alertdialog" />;' },
{ code: '<div role="application" />;' },
{ code: '<div role="article" />;' },
{ code: '<div role="banner" />;' },
{ code: '<div role="cell" />;' },
{ code: '<div role="complementary" />;' },
{ code: '<div role="contentinfo" />;' },
{ code: '<div role="definition" />;' },
{ code: '<div role="dialog" />;' },
{ code: '<div role="directory" />;' },
{ code: '<div role="document" />;' },
{ code: '<div role="feed" />;' },
{ code: '<div role="figure" />;' },
{ code: '<div role="form" />;' },
{ code: '<div role="group" />;' },
{ code: '<div role="heading" />;' },
{ code: '<div role="img" />;' },
{ code: '<div role="list" />;' },
{ code: '<div role="listitem" />;' },
{ code: '<div role="log" />;' },
{ code: '<div role="main" />;' },
{ code: '<div role="marquee" />;' },
{ code: '<div role="math" />;' },
{ code: '<div role="navigation" />;' },
{ code: '<div role="note" />;' },
{ code: '<div role="region" />;' },
{ code: '<div role="rowgroup" />;' },
{ code: '<div role="search" />;' },
{ code: '<div role="separator" />;' },
{ code: '<div role="scrollbar" />;' },
{ code: '<div role="status" />;' },
{ code: '<div role="table" />;' },
{ code: '<div role="tabpanel" />;' },
{ code: '<div role="term" />;' },
{ code: '<div role="timer" />;' },
{ code: '<div role="tooltip" />;' },
];
const neverValid = [
/* HTML elements with an inherent non-interactive role, assigned an
* interactive role. */
{ code: '<main role="button" />;', errors: [expectedError] },
{ code: '<article role="button" />;', errors: [expectedError] },
{ code: '<article role="button" />;', errors: [expectedError] },
{ code: '<blockquote role="button" />;', errors: [expectedError] },
{ code: '<br role="button" />;', errors: [expectedError] },
{ code: '<caption role="button" />;', errors: [expectedError] },
{ code: '<dd role="button" />;', errors: [expectedError] },
{ code: '<details role="button" />;', errors: [expectedError] },
{ code: '<dir role="button" />;', errors: [expectedError] },
{ code: '<dl role="button" />;', errors: [expectedError] },
{ code: '<dfn role="button" />;', errors: [expectedError] },
{ code: '<dt role="button" />;', errors: [expectedError] },
{ code: '<fieldset role="button" />;', errors: [expectedError] },
{ code: '<figcaption role="button" />;', errors: [expectedError] },
{ code: '<figure role="button" />;', errors: [expectedError] },
{ code: '<footer role="button" />;', errors: [expectedError] },
{ code: '<form role="button" />;', errors: [expectedError] },
{ code: '<frame role="button" />;', errors: [expectedError] },
{ code: '<h1 role="button" />;', errors: [expectedError] },
{ code: '<h2 role="button" />;', errors: [expectedError] },
{ code: '<h3 role="button" />;', errors: [expectedError] },
{ code: '<h4 role="button" />;', errors: [expectedError] },
{ code: '<h5 role="button" />;', errors: [expectedError] },
{ code: '<h6 role="button" />;', errors: [expectedError] },
{ code: '<hr role="button" />;', errors: [expectedError] },
{ code: '<iframe role="button" />;', errors: [expectedError] },
{ code: '<img role="button" />;', errors: [expectedError] },
{ code: '<label role="button" />;', errors: [expectedError] },
{ code: '<legend role="button" />;', errors: [expectedError] },
{ code: '<li role="button" />;', errors: [expectedError] },
{ code: '<mark role="button" />;', errors: [expectedError] },
{ code: '<marquee role="button" />;', errors: [expectedError] },
{ code: '<menu role="button" />;', errors: [expectedError] },
{ code: '<meter role="button" />;', errors: [expectedError] },
{ code: '<nav role="button" />;', errors: [expectedError] },
{ code: '<ol role="button" />;', errors: [expectedError] },
{ code: '<pre role="button" />;', errors: [expectedError] },
{ code: '<progress role="button" />;', errors: [expectedError] },
{ code: '<ruby role="button" />;', errors: [expectedError] },
{ code: '<table role="button" />;', errors: [expectedError] },
{ code: '<tbody role="button" />;', errors: [expectedError] },
{ code: '<td role="button" />;', errors: [expectedError] },
{ code: '<tfoot role="button" />;', errors: [expectedError] },
{ code: '<thead role="button" />;', errors: [expectedError] },
{ code: '<time role="button" />;', errors: [expectedError] },
{ code: '<ul role="button" />;', errors: [expectedError] },
/* HTML elements with an inherent non-interactive role, assigned an
* interactive role. */
{ code: '<main role="menuitem" />;', errors: [expectedError] },
{ code: '<article role="menuitem" />;', errors: [expectedError] },
{ code: '<article role="menuitem" />;', errors: [expectedError] },
{ code: '<dd role="menuitem" />;', errors: [expectedError] },
{ code: '<dfn role="menuitem" />;', errors: [expectedError] },
{ code: '<dt role="menuitem" />;', errors: [expectedError] },
{ code: '<fieldset role="menuitem" />;', errors: [expectedError] },
{ code: '<figure role="menuitem" />;', errors: [expectedError] },
{ code: '<form role="menuitem" />;', errors: [expectedError] },
{ code: '<frame role="menuitem" />;', errors: [expectedError] },
{ code: '<h1 role="menuitem" />;', errors: [expectedError] },
{ code: '<h2 role="menuitem" />;', errors: [expectedError] },
{ code: '<h3 role="menuitem" />;', errors: [expectedError] },
{ code: '<h4 role="menuitem" />;', errors: [expectedError] },
{ code: '<h5 role="menuitem" />;', errors: [expectedError] },
{ code: '<h6 role="menuitem" />;', errors: [expectedError] },
{ code: '<hr role="menuitem" />;', errors: [expectedError] },
{ code: '<img role="menuitem" />;', errors: [expectedError] },
{ code: '<nav role="menuitem" />;', errors: [expectedError] },
{ code: '<ol role="menuitem" />;', errors: [expectedError] },
{ code: '<p role="button" />;', errors: [expectedError] },
{ code: '<table role="menuitem" />;', errors: [expectedError] },
{ code: '<tbody role="menuitem" />;', errors: [expectedError] },
{ code: '<td role="menuitem" />;', errors: [expectedError] },
{ code: '<tfoot role="menuitem" />;', errors: [expectedError] },
{ code: '<thead role="menuitem" />;', errors: [expectedError] },
];
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: [
...alwaysValid,
{ code: '<ul role="menu" />;' },
{ code: '<ul role="menubar" />;' },
{ code: '<ul role="radiogroup" />;' },
{ code: '<ul role="tablist" />;' },
{ code: '<ul role="tree" />;' },
{ code: '<ul role="treegrid" />;' },
{ code: '<ol role="menu" />;' },
{ code: '<ol role="menubar" />;' },
{ code: '<ol role="radiogroup" />;' },
{ code: '<ol role="tablist" />;' },
{ code: '<ol role="tree" />;' },
{ code: '<ol role="treegrid" />;' },
{ code: '<li role="tab" />;' },
{ code: '<li role="menuitem" />;' },
{ code: '<li role="row" />;' },
{ code: '<li role="treeitem" />;' },
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: [
...neverValid,
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: [
...alwaysValid,
].map(parserOptionsMapper),
invalid: [
...neverValid,
{ code: '<ul role="menu" />;', errors: [expectedError] },
{ code: '<ul role="menubar" />;', errors: [expectedError] },
{ code: '<ul role="radiogroup" />;', errors: [expectedError] },
{ code: '<ul role="tablist" />;', errors: [expectedError] },
{ code: '<ul role="tree" />;', errors: [expectedError] },
{ code: '<ul role="treegrid" />;', errors: [expectedError] },
{ code: '<ol role="menu" />;', errors: [expectedError] },
{ code: '<ol role="menubar" />;', errors: [expectedError] },
{ code: '<ol role="radiogroup" />;', errors: [expectedError] },
{ code: '<ol role="tablist" />;', errors: [expectedError] },
{ code: '<ol role="tree" />;', errors: [expectedError] },
{ code: '<ol role="treegrid" />;', errors: [expectedError] },
{ code: '<li role="tab" />;', errors: [expectedError] },
{ code: '<li role="menuitem" />;', errors: [expectedError] },
{ code: '<li role="row" />;', errors: [expectedError] },
{ code: '<li role="treeitem" />;', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,75 @@
/* eslint-env jest */
/**
* @fileoverview Disallow tabindex on static and noninteractive elements
* @author jessebeach
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-noninteractive-tabindex';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const ruleName = 'no-noninteractive-tabindex';
const expectedError = {
message: '`tabIndex` should only be declared on interactive elements.',
type: 'JSXAttribute',
};
const alwaysValid = [
{ code: '<MyButton tabIndex={0} />' },
{ code: '<button />' },
{ code: '<button tabIndex="0" />' },
{ code: '<button tabIndex={0} />' },
{ code: '<div />' },
{ code: '<div tabIndex="-1" />' },
{ code: '<div role="button" tabIndex="0" />' },
{ code: '<div role="article" tabIndex="-1" />' },
{ code: '<article tabIndex="-1" />' },
];
const neverValid = [
{ code: '<div tabIndex="0" />', errors: [expectedError] },
{ code: '<div role="article" tabIndex="0" />', errors: [expectedError] },
{ code: '<article tabIndex="0" />', errors: [expectedError] },
{ code: '<article tabIndex={0} />', errors: [expectedError] },
];
const recommendedOptions = (
configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {}
);
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: [
...alwaysValid,
{ code: '<div role="tabpanel" tabIndex="0" />' },
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: [
...neverValid,
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: [
...alwaysValid,
].map(parserOptionsMapper),
invalid: [
...neverValid,
{ code: '<div role="tabpanel" tabIndex="0" />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,47 @@
/* eslint-env jest */
/**
* @fileoverview Enforce usage of onBlur over onChange on select menus for accessibility.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-onchange';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'onBlur must be used instead of onchange, unless absolutely necessary and it ' +
'causes no negative consequences for keyboard only or screen reader users.',
type: 'JSXOpeningElement',
};
ruleTester.run('no-onchange', rule, {
valid: [
{ code: '<select onBlur={() => {}} />;' },
{ code: '<select onBlur={handleOnBlur} />;' },
{ code: '<option />;' },
{ code: '<option onBlur={() => {}} onChange={() => {}} />;' },
{ code: '<option {...props} />' },
{ code: '<input onChange={() => {}} />;' },
{ code: '<input onChange={handleOnChange} />;' },
{ code: '<input />;' },
{ code: '<input onChange={() => {}} onChange={() => {}} />;' },
{ code: '<input {...props} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<select onChange={() => {}} />;', errors: [expectedError] },
{ code: '<select onChange={handleOnChange} />;', errors: [expectedError] },
{ code: '<option onChange={() => {}} />', errors: [expectedError] },
{ code: '<option onChange={() => {}} {...props} />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,39 @@
/* eslint-env jest */
/**
* @fileoverview Enforce explicit role property is not the
* same as implicit default role property on element.
* @author Ethan Cohen <@evcohen>
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-redundant-roles';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = (element, implicitRole) => ({
message: `The element ${element} has an implicit role of ${implicitRole}. Defining this explicitly is redundant and should be avoided.`,
type: 'JSXOpeningElement',
});
ruleTester.run('no-redundant-roles', rule, {
valid: [
{ code: '<div />;' },
{ code: '<button role="main" />' },
{ code: '<MyComponent role="button" />' },
{ code: '<button role={`${foo}button`} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<button role="button" />', errors: [expectedError('button', 'button')] },
{ code: '<body role="DOCUMENT" />', errors: [expectedError('body', 'document')] },
{ code: '<button role={`${undefined}button`} />', errors: [expectedError('button', 'button')] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,457 @@
/* eslint-env jest */
/**
* @fileoverview Enforce static elements have no interactive handlers.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/no-static-element-interactions';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage =
'Static HTML elements with event handlers require a role.';
const expectedError = {
message: errorMessage,
type: 'JSXOpeningElement',
};
const ruleName = 'no-static-element-interactions';
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
{ code: '<div />;' },
{ code: '<div className="foo" />;' },
{ code: '<div className="foo" {...props} />;' },
{ code: '<div onClick={() => void 0} aria-hidden />;' },
{ code: '<div onClick={() => void 0} aria-hidden={true} />;' },
{ code: '<div onClick={null} />;' },
/* All flavors of input */
{ code: '<input onClick={() => void 0} />' },
{ code: '<input type="button" onClick={() => void 0} />' },
{ code: '<input type="checkbox" onClick={() => void 0} />' },
{ code: '<input type="color" onClick={() => void 0} />' },
{ code: '<input type="date" onClick={() => void 0} />' },
{ code: '<input type="datetime" onClick={() => void 0} />' },
{ code: '<input type="datetime-local" onClick={() => void 0} />' },
{ code: '<input type="email" onClick={() => void 0} />' },
{ code: '<input type="file" onClick={() => void 0} />' },
{ code: '<input type="hidden" onClick={() => void 0} />' },
{ code: '<input type="image" onClick={() => void 0} />' },
{ code: '<input type="month" onClick={() => void 0} />' },
{ code: '<input type="number" onClick={() => void 0} />' },
{ code: '<input type="password" onClick={() => void 0} />' },
{ code: '<input type="radio" onClick={() => void 0} />' },
{ code: '<input type="range" onClick={() => void 0} />' },
{ code: '<input type="reset" onClick={() => void 0} />' },
{ code: '<input type="search" onClick={() => void 0} />' },
{ code: '<input type="submit" onClick={() => void 0} />' },
{ code: '<input type="tel" onClick={() => void 0} />' },
{ code: '<input type="text" onClick={() => void 0} />' },
{ code: '<input type="time" onClick={() => void 0} />' },
{ code: '<input type="url" onClick={() => void 0} />' },
{ code: '<input type="week" onClick={() => void 0} />' },
/* End all flavors of input */
{ code: '<button onClick={() => void 0} className="foo" />' },
{ code: '<menuitem onClick={() => {}} />;' },
{ code: '<option onClick={() => void 0} className="foo" />' },
{ code: '<select onClick={() => void 0} className="foo" />' },
{ code: '<textarea onClick={() => void 0} className="foo" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" />' },
{ code: '<a onClick={() => void 0} href="http://x.y.z" tabIndex="0" />' },
{ code: '<audio onClick={() => {}} />;' },
{ code: '<form onClick={() => {}} />;' },
/* HTML elements attributed with an interactive role */
{ code: '<div role="button" onClick={() => {}} />;' },
{ code: '<div role="checkbox" onClick={() => {}} />;' },
{ code: '<div role="columnheader" onClick={() => {}} />;' },
{ code: '<div role="combobox" onClick={() => {}} />;' },
{ code: '<div role="form" onClick={() => {}} />;' },
{ code: '<div role="gridcell" onClick={() => {}} />;' },
{ code: '<div role="link" onClick={() => {}} />;' },
{ code: '<div role="menuitem" onClick={() => {}} />;' },
{ code: '<div role="menuitemcheckbox" onClick={() => {}} />;' },
{ code: '<div role="menuitemradio" onClick={() => {}} />;' },
{ code: '<div role="option" onClick={() => {}} />;' },
{ code: '<div role="radio" onClick={() => {}} />;' },
{ code: '<div role="rowheader" onClick={() => {}} />;' },
{ code: '<div role="searchbox" onClick={() => {}} />;' },
{ code: '<div role="slider" onClick={() => {}} />;' },
{ code: '<div role="spinbutton" onClick={() => {}} />;' },
{ code: '<div role="switch" onClick={() => {}} />;' },
{ code: '<div role="tab" onClick={() => {}} />;' },
{ code: '<div role="textbox" onClick={() => {}} />;' },
{ code: '<div role="treeitem" onClick={() => {}} />;' },
/* Presentation is a special case role that indicates intentional static semantics */
{ code: '<div role="presentation" onClick={() => {}} />;' },
/* HTML elements with an inherent, non-interactive role */
{ code: '<main onClick={() => void 0} />;' },
{ code: '<article onClick={() => {}} />;' },
{ code: '<article onDblClick={() => void 0} />;' },
{ code: '<blockquote onClick={() => {}} />;' },
{ code: '<br onClick={() => {}} />;' },
{ code: '<canvas onClick={() => {}} />;' },
{ code: '<caption onClick={() => {}} />;' },
{ code: '<details onClick={() => {}} />;' },
{ code: '<dd onClick={() => {}} />;' },
{ code: '<dfn onClick={() => {}} />;' },
{ code: '<dir onClick={() => {}} />;' },
{ code: '<dl onClick={() => {}} />;' },
{ code: '<dt onClick={() => {}} />;' },
{ code: '<embed onClick={() => {}} />;' },
{ code: '<fieldset onClick={() => {}} />;' },
{ code: '<figcaption onClick={() => {}} />;' },
{ code: '<figure onClick={() => {}} />;' },
{ code: '<footer onClick={() => {}} />;' },
{ code: '<frame onClick={() => {}} />;' },
{ code: '<h1 onClick={() => {}} />;' },
{ code: '<h2 onClick={() => {}} />;' },
{ code: '<h3 onClick={() => {}} />;' },
{ code: '<h4 onClick={() => {}} />;' },
{ code: '<h5 onClick={() => {}} />;' },
{ code: '<h6 onClick={() => {}} />;' },
{ code: '<hr onClick={() => {}} />;' },
{ code: '<iframe onClick={() => {}} />;' },
{ code: '<img onClick={() => {}} />;' },
{ code: '<label onClick={() => {}} />;' },
{ code: '<legend onClick={() => {}} />;' },
{ code: '<li onClick={() => {}} />;' },
{ code: '<link onClick={() => {}} />;' },
{ code: '<mark onClick={() => {}} />;' },
{ code: '<marquee onClick={() => {}} />;' },
{ code: '<menu onClick={() => {}} />;' },
{ code: '<meter onClick={() => {}} />;' },
{ code: '<nav onClick={() => {}} />;' },
{ code: '<ol onClick={() => {}} />;' },
{ code: '<p onClick={() => {}} />;' },
{ code: '<pre onClick={() => {}} />;' },
{ code: '<progress onClick={() => {}} />;' },
{ code: '<ruby onClick={() => {}} />;' },
{ code: '<table onClick={() => {}} />;' },
{ code: '<tbody onClick={() => {}} />;' },
{ code: '<tfoot onClick={() => {}} />;' },
{ code: '<th onClick={() => {}} />;' },
{ code: '<thead onClick={() => {}} />;' },
{ code: '<time onClick={() => {}} />;' },
{ code: '<tr onClick={() => {}} />;' },
{ code: '<video onClick={() => {}} />;' },
{ code: '<ul onClick={() => {}} />;' },
/* HTML elements attributed with an abstract role */
{ code: '<div role="command" onClick={() => {}} />;' },
{ code: '<div role="composite" onClick={() => {}} />;' },
{ code: '<div role="input" onClick={() => {}} />;' },
{ code: '<div role="landmark" onClick={() => {}} />;' },
{ code: '<div role="range" onClick={() => {}} />;' },
{ code: '<div role="roletype" onClick={() => {}} />;' },
{ code: '<div role="section" onClick={() => {}} />;' },
{ code: '<div role="sectionhead" onClick={() => {}} />;' },
{ code: '<div role="select" onClick={() => {}} />;' },
{ code: '<div role="structure" onClick={() => {}} />;' },
{ code: '<div role="widget" onClick={() => {}} />;' },
{ code: '<div role="window" onClick={() => {}} />;' },
/* HTML elements attributed with a non-interactive role */
{ code: '<div role="alert" onClick={() => {}} />;' },
{ code: '<div role="alertdialog" onClick={() => {}} />;' },
{ code: '<div role="application" onClick={() => {}} />;' },
{ code: '<div role="article" onClick={() => {}} />;' },
{ code: '<div role="banner" onClick={() => {}} />;' },
{ code: '<div role="cell" onClick={() => {}} />;' },
{ code: '<div role="complementary" onClick={() => {}} />;' },
{ code: '<div role="contentinfo" onClick={() => {}} />;' },
{ code: '<div role="definition" onClick={() => {}} />;' },
{ code: '<div role="dialog" onClick={() => {}} />;' },
{ code: '<div role="directory" onClick={() => {}} />;' },
{ code: '<div role="document" onClick={() => {}} />;' },
{ code: '<div role="feed" onClick={() => {}} />;' },
{ code: '<div role="figure" onClick={() => {}} />;' },
{ code: '<div role="grid" onClick={() => {}} />;' },
{ code: '<div role="group" onClick={() => {}} />;' },
{ code: '<div role="heading" onClick={() => {}} />;' },
{ code: '<div role="img" onClick={() => {}} />;' },
{ code: '<div role="list" onClick={() => {}} />;' },
{ code: '<div role="listbox" onClick={() => {}} />;' },
{ code: '<div role="listitem" onClick={() => {}} />;' },
{ code: '<div role="log" onClick={() => {}} />;' },
{ code: '<div role="main" onClick={() => {}} />;' },
{ code: '<div role="marquee" onClick={() => {}} />;' },
{ code: '<div role="math" onClick={() => {}} />;' },
{ code: '<div role="menu" onClick={() => {}} />;' },
{ code: '<div role="menubar" onClick={() => {}} />;' },
{ code: '<div role="navigation" onClick={() => {}} />;' },
{ code: '<div role="note" onClick={() => {}} />;' },
{ code: '<div role="progressbar" onClick={() => {}} />;' },
{ code: '<div role="radiogroup" onClick={() => {}} />;' },
{ code: '<div role="region" onClick={() => {}} />;' },
{ code: '<div role="row" onClick={() => {}} />;' },
{ code: '<div role="rowgroup" onClick={() => {}} />;' },
{ code: '<div role="search" onClick={() => {}} />;' },
{ code: '<div role="separator" onClick={() => {}} />;' },
{ code: '<div role="scrollbar" onClick={() => {}} />;' },
{ code: '<div role="status" onClick={() => {}} />;' },
{ code: '<div role="table" onClick={() => {}} />;' },
{ code: '<div role="tablist" onClick={() => {}} />;' },
{ code: '<div role="tabpanel" onClick={() => {}} />;' },
{ code: '<td onClick={() => {}} />;' },
{ code: '<div role="term" onClick={() => {}} />;' },
{ code: '<div role="timer" onClick={() => {}} />;' },
{ code: '<div role="toolbar" onClick={() => {}} />;' },
{ code: '<div role="tooltip" onClick={() => {}} />;' },
{ code: '<div role="tree" onClick={() => {}} />;' },
{ code: '<div role="treegrid" onClick={() => {}} />;' },
];
const neverValid = [
{ code: '<div onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<div onClick={() => void 0} role={undefined} />;', errors: [expectedError] },
{ code: '<div onClick={() => void 0} {...props} />;', errors: [expectedError] },
{ code: '<div onKeyUp={() => void 0} aria-hidden={false} />;', errors: [expectedError] },
/* Static elements; no inherent role */
{ code: '<a onClick={() => void 0} />', errors: [expectedError] },
{ code: '<a onClick={() => {}} />;', errors: [expectedError] },
{ code: '<a tabIndex="0" onClick={() => void 0} />', errors: [expectedError] },
{ code: '<area onClick={() => {}} />;', errors: [expectedError] },
{ code: '<acronym onClick={() => {}} />;', errors: [expectedError] },
{ code: '<address onClick={() => {}} />;', errors: [expectedError] },
{ code: '<applet onClick={() => {}} />;', errors: [expectedError] },
{ code: '<aside onClick={() => {}} />;', errors: [expectedError] },
{ code: '<b onClick={() => {}} />;', errors: [expectedError] },
{ code: '<base onClick={() => {}} />;', errors: [expectedError] },
{ code: '<bdi onClick={() => {}} />;', errors: [expectedError] },
{ code: '<bdo onClick={() => {}} />;', errors: [expectedError] },
{ code: '<big onClick={() => {}} />;', errors: [expectedError] },
{ code: '<blink onClick={() => {}} />;', errors: [expectedError] },
{ code: '<body onClick={() => {}} />;', errors: [expectedError] },
{ code: '<center onClick={() => {}} />;', errors: [expectedError] },
{ code: '<cite onClick={() => {}} />;', errors: [expectedError] },
{ code: '<code onClick={() => {}} />;', errors: [expectedError] },
{ code: '<col onClick={() => {}} />;', errors: [expectedError] },
{ code: '<colgroup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<content onClick={() => {}} />;', errors: [expectedError] },
{ code: '<data onClick={() => {}} />;', errors: [expectedError] },
{ code: '<datalist onClick={() => {}} />;', errors: [expectedError] },
{ code: '<del onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div onClick={() => {}} />;', errors: [expectedError] },
{ code: '<em onClick={() => {}} />;', errors: [expectedError] },
{ code: '<font onClick={() => {}} />;', errors: [expectedError] },
{ code: '<frameset onClick={() => {}} />;', errors: [expectedError] },
{ code: '<head onClick={() => {}} />;', errors: [expectedError] },
{ code: '<header onClick={() => {}} />;', errors: [expectedError] },
{ code: '<hgroup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<html onClick={() => {}} />;', errors: [expectedError] },
{ code: '<i onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ins onClick={() => {}} />;', errors: [expectedError] },
{ code: '<kbd onClick={() => {}} />;', errors: [expectedError] },
{ code: '<keygen onClick={() => {}} />;', errors: [expectedError] },
{ code: '<map onClick={() => {}} />;', errors: [expectedError] },
{ code: '<meta onClick={() => {}} />;', errors: [expectedError] },
{ code: '<noembed onClick={() => {}} />;', errors: [expectedError] },
{ code: '<noscript onClick={() => {}} />;', errors: [expectedError] },
{ code: '<object onClick={() => {}} />;', errors: [expectedError] },
{ code: '<optgroup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<output onClick={() => {}} />;', errors: [expectedError] },
{ code: '<param onClick={() => {}} />;', errors: [expectedError] },
{ code: '<picture onClick={() => {}} />;', errors: [expectedError] },
{ code: '<q onClick={() => {}} />;', errors: [expectedError] },
{ code: '<rp onClick={() => {}} />;', errors: [expectedError] },
{ code: '<rt onClick={() => {}} />;', errors: [expectedError] },
{ code: '<rtc onClick={() => {}} />;', errors: [expectedError] },
{ code: '<s onClick={() => {}} />;', errors: [expectedError] },
{ code: '<samp onClick={() => {}} />;', errors: [expectedError] },
{ code: '<script onClick={() => {}} />;', errors: [expectedError] },
{ code: '<section onClick={() => {}} />;', errors: [expectedError] },
{ code: '<small onClick={() => {}} />;', errors: [expectedError] },
{ code: '<source onClick={() => {}} />;', errors: [expectedError] },
{ code: '<spacer onClick={() => {}} />;', errors: [expectedError] },
{ code: '<span onClick={() => {}} />;', errors: [expectedError] },
{ code: '<strike onClick={() => {}} />;', errors: [expectedError] },
{ code: '<strong onClick={() => {}} />;', errors: [expectedError] },
{ code: '<style onClick={() => {}} />;', errors: [expectedError] },
{ code: '<sub onClick={() => {}} />;', errors: [expectedError] },
{ code: '<summary onClick={() => {}} />;', errors: [expectedError] },
{ code: '<sup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<title onClick={() => {}} />;', errors: [expectedError] },
{ code: '<track onClick={() => {}} />;', errors: [expectedError] },
{ code: '<tt onClick={() => {}} />;', errors: [expectedError] },
{ code: '<u onClick={() => {}} />;', errors: [expectedError] },
{ code: '<var onClick={() => {}} />;', errors: [expectedError] },
{ code: '<wbr onClick={() => {}} />;', errors: [expectedError] },
{ code: '<xmp onClick={() => {}} />;', errors: [expectedError] },
];
const recommendedOptions =
(configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: [
...alwaysValid,
// All the possible handlers
{ code: '<div onCopy={() => {}} />;' },
{ code: '<div onCut={() => {}} />;' },
{ code: '<div onPaste={() => {}} />;' },
{ code: '<div onCompositionEnd={() => {}} />;' },
{ code: '<div onCompositionStart={() => {}} />;' },
{ code: '<div onCompositionUpdate={() => {}} />;' },
{ code: '<div onFocus={() => {}} />;' },
{ code: '<div onBlur={() => {}} />;' },
{ code: '<div onChange={() => {}} />;' },
{ code: '<div onInput={() => {}} />;' },
{ code: '<div onSubmit={() => {}} />;' },
{ code: '<div onContextMenu={() => {}} />;' },
{ code: '<div onDblClick={() => {}} />;' },
{ code: '<div onDoubleClick={() => {}} />;' },
{ code: '<div onDrag={() => {}} />;' },
{ code: '<div onDragEnd={() => {}} />;' },
{ code: '<div onDragEnter={() => {}} />;' },
{ code: '<div onDragExit={() => {}} />;' },
{ code: '<div onDragLeave={() => {}} />;' },
{ code: '<div onDragOver={() => {}} />;' },
{ code: '<div onDragStart={() => {}} />;' },
{ code: '<div onDrop={() => {}} />;' },
{ code: '<div onMouseEnter={() => {}} />;' },
{ code: '<div onMouseLeave={() => {}} />;' },
{ code: '<div onMouseMove={() => {}} />;' },
{ code: '<div onMouseOut={() => {}} />;' },
{ code: '<div onMouseOver={() => {}} />;' },
{ code: '<div onSelect={() => {}} />;' },
{ code: '<div onTouchCancel={() => {}} />;' },
{ code: '<div onTouchEnd={() => {}} />;' },
{ code: '<div onTouchMove={() => {}} />;' },
{ code: '<div onTouchStart={() => {}} />;' },
{ code: '<div onScroll={() => {}} />;' },
{ code: '<div onWheel={() => {}} />;' },
{ code: '<div onAbort={() => {}} />;' },
{ code: '<div onCanPlay={() => {}} />;' },
{ code: '<div onCanPlayThrough={() => {}} />;' },
{ code: '<div onDurationChange={() => {}} />;' },
{ code: '<div onEmptied={() => {}} />;' },
{ code: '<div onEncrypted={() => {}} />;' },
{ code: '<div onEnded={() => {}} />;' },
{ code: '<div onError={() => {}} />;' },
{ code: '<div onLoadedData={() => {}} />;' },
{ code: '<div onLoadedMetadata={() => {}} />;' },
{ code: '<div onLoadStart={() => {}} />;' },
{ code: '<div onPause={() => {}} />;' },
{ code: '<div onPlay={() => {}} />;' },
{ code: '<div onPlaying={() => {}} />;' },
{ code: '<div onProgress={() => {}} />;' },
{ code: '<div onRateChange={() => {}} />;' },
{ code: '<div onSeeked={() => {}} />;' },
{ code: '<div onSeeking={() => {}} />;' },
{ code: '<div onStalled={() => {}} />;' },
{ code: '<div onSuspend={() => {}} />;' },
{ code: '<div onTimeUpdate={() => {}} />;' },
{ code: '<div onVolumeChange={() => {}} />;' },
{ code: '<div onWaiting={() => {}} />;' },
{ code: '<div onLoad={() => {}} />;' },
{ code: '<div onError={() => {}} />;' },
{ code: '<div onAnimationStart={() => {}} />;' },
{ code: '<div onAnimationEnd={() => {}} />;' },
{ code: '<div onAnimationIteration={() => {}} />;' },
{ code: '<div onTransitionEnd={() => {}} />;' },
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: [
...neverValid,
// All the possible handlers
{ code: '<div onKeyDown={() => {}} />;', errors: [expectedError] },
{ code: '<div onKeyPress={() => {}} />;', errors: [expectedError] },
{ code: '<div onKeyUp={() => {}} />;', errors: [expectedError] },
{ code: '<div onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseDown={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseUp={() => {}} />;', errors: [expectedError] },
]
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: [
...alwaysValid,
].map(parserOptionsMapper),
invalid: [
...neverValid,
// All the possible handlers
{ code: '<div onCopy={() => {}} />;', errors: [expectedError] },
{ code: '<div onCut={() => {}} />;', errors: [expectedError] },
{ code: '<div onPaste={() => {}} />;', errors: [expectedError] },
{ code: '<div onCompositionEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div onCompositionStart={() => {}} />;', errors: [expectedError] },
{ code: '<div onCompositionUpdate={() => {}} />;', errors: [expectedError] },
{ code: '<div onKeyDown={() => {}} />;', errors: [expectedError] },
{ code: '<div onKeyPress={() => {}} />;', errors: [expectedError] },
{ code: '<div onKeyUp={() => {}} />;', errors: [expectedError] },
{ code: '<div onFocus={() => {}} />;', errors: [expectedError] },
{ code: '<div onBlur={() => {}} />;', errors: [expectedError] },
{ code: '<div onChange={() => {}} />;', errors: [expectedError] },
{ code: '<div onInput={() => {}} />;', errors: [expectedError] },
{ code: '<div onSubmit={() => {}} />;', errors: [expectedError] },
{ code: '<div onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div onContextMenu={() => {}} />;', errors: [expectedError] },
{ code: '<div onDblClick={() => {}} />;', errors: [expectedError] },
{ code: '<div onDoubleClick={() => {}} />;', errors: [expectedError] },
{ code: '<div onDrag={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragExit={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragOver={() => {}} />;', errors: [expectedError] },
{ code: '<div onDragStart={() => {}} />;', errors: [expectedError] },
{ code: '<div onDrop={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseDown={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseMove={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseOut={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseOver={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseUp={() => {}} />;', errors: [expectedError] },
{ code: '<div onSelect={() => {}} />;', errors: [expectedError] },
{ code: '<div onTouchCancel={() => {}} />;', errors: [expectedError] },
{ code: '<div onTouchEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div onTouchMove={() => {}} />;', errors: [expectedError] },
{ code: '<div onTouchStart={() => {}} />;', errors: [expectedError] },
{ code: '<div onScroll={() => {}} />;', errors: [expectedError] },
{ code: '<div onWheel={() => {}} />;', errors: [expectedError] },
{ code: '<div onAbort={() => {}} />;', errors: [expectedError] },
{ code: '<div onCanPlay={() => {}} />;', errors: [expectedError] },
{ code: '<div onCanPlayThrough={() => {}} />;', errors: [expectedError] },
{ code: '<div onDurationChange={() => {}} />;', errors: [expectedError] },
{ code: '<div onEmptied={() => {}} />;', errors: [expectedError] },
{ code: '<div onEncrypted={() => {}} />;', errors: [expectedError] },
{ code: '<div onEnded={() => {}} />;', errors: [expectedError] },
{ code: '<div onError={() => {}} />;', errors: [expectedError] },
{ code: '<div onLoadedData={() => {}} />;', errors: [expectedError] },
{ code: '<div onLoadedMetadata={() => {}} />;', errors: [expectedError] },
{ code: '<div onLoadStart={() => {}} />;', errors: [expectedError] },
{ code: '<div onPause={() => {}} />;', errors: [expectedError] },
{ code: '<div onPlay={() => {}} />;', errors: [expectedError] },
{ code: '<div onPlaying={() => {}} />;', errors: [expectedError] },
{ code: '<div onProgress={() => {}} />;', errors: [expectedError] },
{ code: '<div onRateChange={() => {}} />;', errors: [expectedError] },
{ code: '<div onSeeked={() => {}} />;', errors: [expectedError] },
{ code: '<div onSeeking={() => {}} />;', errors: [expectedError] },
{ code: '<div onStalled={() => {}} />;', errors: [expectedError] },
{ code: '<div onSuspend={() => {}} />;', errors: [expectedError] },
{ code: '<div onTimeUpdate={() => {}} />;', errors: [expectedError] },
{ code: '<div onVolumeChange={() => {}} />;', errors: [expectedError] },
{ code: '<div onWaiting={() => {}} />;', errors: [expectedError] },
{ code: '<div onLoad={() => {}} />;', errors: [expectedError] },
{ code: '<div onError={() => {}} />;', errors: [expectedError] },
{ code: '<div onAnimationStart={() => {}} />;', errors: [expectedError] },
{ code: '<div onAnimationEnd={() => {}} />;', errors: [expectedError] },
{ code: '<div onAnimationIteration={() => {}} />;', errors: [expectedError] },
{ code: '<div onTransitionEnd={() => {}} />;', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,138 @@
/* eslint-env jest */
/**
* @fileoverview Enforce that elements with ARIA roles must
* have all required attributes for that role.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { roles } from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/role-has-required-aria-props';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = (role) => {
const requiredProps = Object.keys(roles.get(role).requiredProps);
return {
message: `Elements with the ARIA role "${role}" must have the following ` +
`attributes defined: ${requiredProps}`,
type: 'JSXAttribute',
};
};
// Create basic test cases using all valid role types.
const basicValidityTests = [...roles.keys()].map((role) => {
const {
requiredProps: requiredPropKeyValues,
} = roles.get(role);
const requiredProps = Object.keys(requiredPropKeyValues);
const propChain = requiredProps.join(' ');
return {
code: `<div role="${role.toLowerCase()}" ${propChain} />`,
};
});
ruleTester.run('role-has-required-aria-props', rule, {
valid: [
{ code: '<Bar baz />' },
// Variables should pass, as we are only testing literals.
{ code: '<div />' },
{ code: '<div></div>' },
{ code: '<div role={role} />' },
{ code: '<div role={role || "button"} />' },
{ code: '<div role={role || "foobar"} />' },
{ code: '<div role="row" />' },
{ code: '<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>' },
].concat(basicValidityTests).map(parserOptionsMapper),
invalid: [
// SLIDER
{ code: '<div role="slider" />', errors: [errorMessage('slider')] },
{
code: '<div role="slider" aria-valuemax />',
errors: [errorMessage('slider')],
},
{
code: '<div role="slider" aria-valuemax aria-valuemin />',
errors: [errorMessage('slider')],
},
{
code: '<div role="slider" aria-valuemax aria-valuenow />',
errors: [errorMessage('slider')],
},
{
code: '<div role="slider" aria-valuemin aria-valuenow />',
errors: [errorMessage('slider')],
},
// SPINBUTTON
{ code: '<div role="spinbutton" />', errors: [errorMessage('spinbutton')] },
{
code: '<div role="spinbutton" aria-valuemax />',
errors: [errorMessage('spinbutton')],
},
{
code: '<div role="spinbutton" aria-valuemax aria-valuemin />',
errors: [errorMessage('spinbutton')],
},
{
code: '<div role="spinbutton" aria-valuemax aria-valuenow />',
errors: [errorMessage('spinbutton')],
},
{
code: '<div role="spinbutton" aria-valuemin aria-valuenow />',
errors: [errorMessage('spinbutton')],
},
// CHECKBOX
{ code: '<div role="checkbox" />', errors: [errorMessage('checkbox')] },
{ code: '<div role="checkbox" checked />', errors: [errorMessage('checkbox')] },
{
code: '<div role="checkbox" aria-chcked />',
errors: [errorMessage('checkbox')],
},
{
code: '<span role="checkbox" aria-labelledby="foo" tabindex="0"></span>',
errors: [errorMessage('checkbox')],
},
// COMBOBOX
{ code: '<div role="combobox" />', errors: [errorMessage('combobox')] },
{ code: '<div role="combobox" expanded />', errors: [errorMessage('combobox')] },
{
code: '<div role="combobox" aria-expandd />',
errors: [errorMessage('combobox')],
},
// SCROLLBAR
{ code: '<div role="scrollbar" />', errors: [errorMessage('scrollbar')] },
{
code: '<div role="scrollbar" aria-valuemax />',
errors: [errorMessage('scrollbar')],
},
{
code: '<div role="scrollbar" aria-valuemax aria-valuemin />',
errors: [errorMessage('scrollbar')],
},
{
code: '<div role="scrollbar" aria-valuemax aria-valuenow />',
errors: [errorMessage('scrollbar')],
},
{
code: '<div role="scrollbar" aria-valuemin aria-valuenow />',
errors: [errorMessage('scrollbar')],
},
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,457 @@
/* eslint-env jest */
/**
* @fileoverview Enforce that an element does not have an unsupported ARIA attribute.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import {
aria,
roles,
} from 'aria-query';
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/role-supports-aria-props';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const generateErrorMessage = (attr, role, tag, isImplicit) => {
if (isImplicit) {
return `The attribute ${attr} is not supported by the role ${role}. \
This role is implicit on the element ${tag}.`;
}
return `The attribute ${attr} is not supported by the role ${role}.`;
};
const errorMessage = (attr, role, tag, isImplicit) => ({
message: generateErrorMessage(attr, role, tag, isImplicit),
type: 'JSXOpeningElement',
});
const nonAbstractRoles = [...roles.keys()].filter(role => roles.get(role).abstract === false);
const createTests = rolesNames => rolesNames.reduce((tests, role) => {
const {
props: propKeyValues,
} = roles.get(role);
const validPropsForRole = Object.keys(propKeyValues);
const invalidPropsForRole = [...aria.keys()]
.map(attribute => attribute.toLowerCase())
.filter(attribute => validPropsForRole.indexOf(attribute) === -1);
const normalRole = role.toLowerCase();
const allTests = [];
allTests[0] = tests[0].concat(validPropsForRole.map(prop => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
})));
allTests[1] = tests[1].concat(invalidPropsForRole.map(prop => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
errors: [errorMessage(prop.toLowerCase(), normalRole, 'div', false)],
})));
return allTests;
}, [[], []]);
const [validTests, invalidTests] = createTests(nonAbstractRoles);
ruleTester.run('role-supports-aria-props', rule, {
valid: [
{ code: '<Foo bar />' },
{ code: '<div />' },
{ code: '<div id="main" />' },
{ code: '<div role />' },
{ code: '<div role="presentation" {...props} />' },
{ code: '<Foo.Bar baz={true} />' },
// IMPLICIT ROLE TESTS
// A TESTS - implicit role is `link`
{ code: '<a href="#" aria-expanded />' },
{ code: '<a href="#" aria-atomic />' },
{ code: '<a href="#" aria-busy />' },
{ code: '<a href="#" aria-controls />' },
{ code: '<a href="#" aria-describedby />' },
{ code: '<a href="#" aria-disabled />' },
{ code: '<a href="#" aria-dropeffect />' },
{ code: '<a href="#" aria-flowto />' },
{ code: '<a href="#" aria-grabbed />' },
{ code: '<a href="#" aria-haspopup />' },
{ code: '<a href="#" aria-hidden />' },
{ code: '<a href="#" aria-invalid />' },
{ code: '<a href="#" aria-label />' },
{ code: '<a href="#" aria-labelledby />' },
{ code: '<a href="#" aria-live />' },
{ code: '<a href="#" aria-owns />' },
{ code: '<a href="#" aria-relevant />' },
// this will have global
{ code: '<a aria-checked />' },
// AREA TESTS - implicit role is `link`
{ code: '<area href="#" aria-expanded />' },
{ code: '<area href="#" aria-atomic />' },
{ code: '<area href="#" aria-busy />' },
{ code: '<area href="#" aria-controls />' },
{ code: '<area href="#" aria-describedby />' },
{ code: '<area href="#" aria-disabled />' },
{ code: '<area href="#" aria-dropeffect />' },
{ code: '<area href="#" aria-flowto />' },
{ code: '<area href="#" aria-grabbed />' },
{ code: '<area href="#" aria-haspopup />' },
{ code: '<area href="#" aria-hidden />' },
{ code: '<area href="#" aria-invalid />' },
{ code: '<area href="#" aria-label />' },
{ code: '<area href="#" aria-labelledby />' },
{ code: '<area href="#" aria-live />' },
{ code: '<area href="#" aria-owns />' },
{ code: '<area href="#" aria-relevant />' },
// this will have global
{ code: '<area aria-checked />' },
// LINK TESTS - implicit role is `link`
{ code: '<link href="#" aria-expanded />' },
{ code: '<link href="#" aria-atomic />' },
{ code: '<link href="#" aria-busy />' },
{ code: '<link href="#" aria-controls />' },
{ code: '<link href="#" aria-describedby />' },
{ code: '<link href="#" aria-disabled />' },
{ code: '<link href="#" aria-dropeffect />' },
{ code: '<link href="#" aria-flowto />' },
{ code: '<link href="#" aria-grabbed />' },
{ code: '<link href="#" aria-haspopup />' },
{ code: '<link href="#" aria-hidden />' },
{ code: '<link href="#" aria-invalid />' },
{ code: '<link href="#" aria-label />' },
{ code: '<link href="#" aria-labelledby />' },
{ code: '<link href="#" aria-live />' },
{ code: '<link href="#" aria-owns />' },
{ code: '<link href="#" aria-relevant />' },
// this will have global
{ code: '<link aria-checked />' },
// IMG TESTS - implicit role is `presentation`
{ code: '<img alt="" aria-atomic />' },
{ code: '<img alt="" aria-busy />' },
{ code: '<img alt="" aria-controls />' },
{ code: '<img alt="" aria-describedby />' },
{ code: '<img alt="" aria-disabled />' },
{ code: '<img alt="" aria-dropeffect />' },
{ code: '<img alt="" aria-flowto />' },
{ code: '<img alt="" aria-grabbed />' },
{ code: '<img alt="" aria-haspopup />' },
{ code: '<img alt="" aria-hidden />' },
{ code: '<img alt="" aria-invalid />' },
{ code: '<img alt="" aria-label />' },
{ code: '<img alt="" aria-labelledby />' },
{ code: '<img alt="" aria-live />' },
{ code: '<img alt="" aria-owns />' },
{ code: '<img alt="" aria-relevant />' },
// this will have role of `img`
{ code: '<img alt="foobar" aria-busy />' },
// MENU TESTS - implicit role is `toolbar` when `type="toolbar"`
{ code: '<menu type="toolbar" aria-activedescendant />' },
{ code: '<menu type="toolbar" aria-expanded />' },
{ code: '<menu type="toolbar" aria-atomic />' },
{ code: '<menu type="toolbar" aria-busy />' },
{ code: '<menu type="toolbar" aria-controls />' },
{ code: '<menu type="toolbar" aria-describedby />' },
{ code: '<menu type="toolbar" aria-disabled />' },
{ code: '<menu type="toolbar" aria-dropeffect />' },
{ code: '<menu type="toolbar" aria-flowto />' },
{ code: '<menu type="toolbar" aria-grabbed />' },
{ code: '<menu type="toolbar" aria-haspopup />' },
{ code: '<menu type="toolbar" aria-hidden />' },
{ code: '<menu type="toolbar" aria-invalid />' },
{ code: '<menu type="toolbar" aria-label />' },
{ code: '<menu type="toolbar" aria-labelledby />' },
{ code: '<menu type="toolbar" aria-live />' },
{ code: '<menu type="toolbar" aria-owns />' },
{ code: '<menu type="toolbar" aria-relevant />' },
// this will have global
{ code: '<menu aria-checked />' },
// MENUITEM TESTS
// when `type="command`, the implicit role is `menuitem`
{ code: '<menuitem type="command" aria-atomic />' },
{ code: '<menuitem type="command" aria-busy />' },
{ code: '<menuitem type="command" aria-controls />' },
{ code: '<menuitem type="command" aria-describedby />' },
{ code: '<menuitem type="command" aria-disabled />' },
{ code: '<menuitem type="command" aria-dropeffect />' },
{ code: '<menuitem type="command" aria-flowto />' },
{ code: '<menuitem type="command" aria-grabbed />' },
{ code: '<menuitem type="command" aria-haspopup />' },
{ code: '<menuitem type="command" aria-hidden />' },
{ code: '<menuitem type="command" aria-invalid />' },
{ code: '<menuitem type="command" aria-label />' },
{ code: '<menuitem type="command" aria-labelledby />' },
{ code: '<menuitem type="command" aria-live />' },
{ code: '<menuitem type="command" aria-owns />' },
{ code: '<menuitem type="command" aria-relevant />' },
// when `type="checkbox`, the implicit role is `menuitemcheckbox`
{ code: '<menuitem type="checkbox" aria-checked />' },
{ code: '<menuitem type="checkbox" aria-atomic />' },
{ code: '<menuitem type="checkbox" aria-busy />' },
{ code: '<menuitem type="checkbox" aria-controls />' },
{ code: '<menuitem type="checkbox" aria-describedby />' },
{ code: '<menuitem type="checkbox" aria-disabled />' },
{ code: '<menuitem type="checkbox" aria-dropeffect />' },
{ code: '<menuitem type="checkbox" aria-flowto />' },
{ code: '<menuitem type="checkbox" aria-grabbed />' },
{ code: '<menuitem type="checkbox" aria-haspopup />' },
{ code: '<menuitem type="checkbox" aria-hidden />' },
{ code: '<menuitem type="checkbox" aria-invalid />' },
{ code: '<menuitem type="checkbox" aria-label />' },
{ code: '<menuitem type="checkbox" aria-labelledby />' },
{ code: '<menuitem type="checkbox" aria-live />' },
{ code: '<menuitem type="checkbox" aria-owns />' },
{ code: '<menuitem type="checkbox" aria-relevant />' },
// when `type="radio`, the implicit role is `menuitemradio`
{ code: '<menuitem type="radio" aria-checked />' },
{ code: '<menuitem type="radio" aria-atomic />' },
{ code: '<menuitem type="radio" aria-busy />' },
{ code: '<menuitem type="radio" aria-controls />' },
{ code: '<menuitem type="radio" aria-describedby />' },
{ code: '<menuitem type="radio" aria-disabled />' },
{ code: '<menuitem type="radio" aria-dropeffect />' },
{ code: '<menuitem type="radio" aria-flowto />' },
{ code: '<menuitem type="radio" aria-grabbed />' },
{ code: '<menuitem type="radio" aria-haspopup />' },
{ code: '<menuitem type="radio" aria-hidden />' },
{ code: '<menuitem type="radio" aria-invalid />' },
{ code: '<menuitem type="radio" aria-label />' },
{ code: '<menuitem type="radio" aria-labelledby />' },
{ code: '<menuitem type="radio" aria-live />' },
{ code: '<menuitem type="radio" aria-owns />' },
{ code: '<menuitem type="radio" aria-relevant />' },
{ code: '<menuitem type="radio" aria-posinset />' },
{ code: '<menuitem type="radio" aria-selected />' },
{ code: '<menuitem type="radio" aria-setsize />' },
// these will have global
{ code: '<menuitem aria-checked />' },
{ code: '<menuitem type="foo" aria-checked />' },
// INPUT TESTS
// when `type="button"`, the implicit role is `button`
{ code: '<input type="button" aria-expanded />' },
{ code: '<input type="button" aria-pressed />' },
{ code: '<input type="button" aria-atomic />' },
{ code: '<input type="button" aria-busy />' },
{ code: '<input type="button" aria-controls />' },
{ code: '<input type="button" aria-describedby />' },
{ code: '<input type="button" aria-disabled />' },
{ code: '<input type="button" aria-dropeffect />' },
{ code: '<input type="button" aria-flowto />' },
{ code: '<input type="button" aria-grabbed />' },
{ code: '<input type="button" aria-haspopup />' },
{ code: '<input type="button" aria-hidden />' },
{ code: '<input type="button" aria-invalid />' },
{ code: '<input type="button" aria-label />' },
{ code: '<input type="button" aria-labelledby />' },
{ code: '<input type="button" aria-live />' },
{ code: '<input type="button" aria-owns />' },
{ code: '<input type="button" aria-relevant />' },
// when `type="image"`, the implicit role is `button`
{ code: '<input type="image" aria-expanded />' },
{ code: '<input type="image" aria-pressed />' },
{ code: '<input type="image" aria-atomic />' },
{ code: '<input type="image" aria-busy />' },
{ code: '<input type="image" aria-controls />' },
{ code: '<input type="image" aria-describedby />' },
{ code: '<input type="image" aria-disabled />' },
{ code: '<input type="image" aria-dropeffect />' },
{ code: '<input type="image" aria-flowto />' },
{ code: '<input type="image" aria-grabbed />' },
{ code: '<input type="image" aria-haspopup />' },
{ code: '<input type="image" aria-hidden />' },
{ code: '<input type="image" aria-invalid />' },
{ code: '<input type="image" aria-label />' },
{ code: '<input type="image" aria-labelledby />' },
{ code: '<input type="image" aria-live />' },
{ code: '<input type="image" aria-owns />' },
{ code: '<input type="image" aria-relevant />' },
// when `type="reset"`, the implicit role is `button`
{ code: '<input type="reset" aria-expanded />' },
{ code: '<input type="reset" aria-pressed />' },
{ code: '<input type="reset" aria-atomic />' },
{ code: '<input type="reset" aria-busy />' },
{ code: '<input type="reset" aria-controls />' },
{ code: '<input type="reset" aria-describedby />' },
{ code: '<input type="reset" aria-disabled />' },
{ code: '<input type="reset" aria-dropeffect />' },
{ code: '<input type="reset" aria-flowto />' },
{ code: '<input type="reset" aria-grabbed />' },
{ code: '<input type="reset" aria-haspopup />' },
{ code: '<input type="reset" aria-hidden />' },
{ code: '<input type="reset" aria-invalid />' },
{ code: '<input type="reset" aria-label />' },
{ code: '<input type="reset" aria-labelledby />' },
{ code: '<input type="reset" aria-live />' },
{ code: '<input type="reset" aria-owns />' },
{ code: '<input type="reset" aria-relevant />' },
// when `type="submit"`, the implicit role is `button`
{ code: '<input type="submit" aria-expanded />' },
{ code: '<input type="submit" aria-pressed />' },
{ code: '<input type="submit" aria-atomic />' },
{ code: '<input type="submit" aria-busy />' },
{ code: '<input type="submit" aria-controls />' },
{ code: '<input type="submit" aria-describedby />' },
{ code: '<input type="submit" aria-disabled />' },
{ code: '<input type="submit" aria-dropeffect />' },
{ code: '<input type="submit" aria-flowto />' },
{ code: '<input type="submit" aria-grabbed />' },
{ code: '<input type="submit" aria-haspopup />' },
{ code: '<input type="submit" aria-hidden />' },
{ code: '<input type="submit" aria-invalid />' },
{ code: '<input type="submit" aria-label />' },
{ code: '<input type="submit" aria-labelledby />' },
{ code: '<input type="submit" aria-live />' },
{ code: '<input type="submit" aria-owns />' },
{ code: '<input type="submit" aria-relevant />' },
// when `type="checkbox"`, the implicit role is `checkbox`
{ code: '<input type="checkbox" aria-checked />' },
{ code: '<input type="checkbox" aria-atomic />' },
{ code: '<input type="checkbox" aria-busy />' },
{ code: '<input type="checkbox" aria-controls />' },
{ code: '<input type="checkbox" aria-describedby />' },
{ code: '<input type="checkbox" aria-disabled />' },
{ code: '<input type="checkbox" aria-dropeffect />' },
{ code: '<input type="checkbox" aria-flowto />' },
{ code: '<input type="checkbox" aria-grabbed />' },
{ code: '<input type="checkbox" aria-haspopup />' },
{ code: '<input type="checkbox" aria-hidden />' },
{ code: '<input type="checkbox" aria-invalid />' },
{ code: '<input type="checkbox" aria-label />' },
{ code: '<input type="checkbox" aria-labelledby />' },
{ code: '<input type="checkbox" aria-live />' },
{ code: '<input type="checkbox" aria-owns />' },
{ code: '<input type="checkbox" aria-relevant />' },
// when `type="radio"`, the implicit role is `radio`
{ code: '<input type="radio" aria-checked />' },
{ code: '<input type="radio" aria-atomic />' },
{ code: '<input type="radio" aria-busy />' },
{ code: '<input type="radio" aria-controls />' },
{ code: '<input type="radio" aria-describedby />' },
{ code: '<input type="radio" aria-disabled />' },
{ code: '<input type="radio" aria-dropeffect />' },
{ code: '<input type="radio" aria-flowto />' },
{ code: '<input type="radio" aria-grabbed />' },
{ code: '<input type="radio" aria-haspopup />' },
{ code: '<input type="radio" aria-hidden />' },
{ code: '<input type="radio" aria-invalid />' },
{ code: '<input type="radio" aria-label />' },
{ code: '<input type="radio" aria-labelledby />' },
{ code: '<input type="radio" aria-live />' },
{ code: '<input type="radio" aria-owns />' },
{ code: '<input type="radio" aria-relevant />' },
{ code: '<input type="radio" aria-posinset />' },
{ code: '<input type="radio" aria-selected />' },
{ code: '<input type="radio" aria-setsize />' },
// when `type="range"`, the implicit role is `slider`
{ code: '<input type="range" aria-valuemax />' },
{ code: '<input type="range" aria-valuemin />' },
{ code: '<input type="range" aria-valuenow />' },
{ code: '<input type="range" aria-orientation />' },
{ code: '<input type="range" aria-atomic />' },
{ code: '<input type="range" aria-busy />' },
{ code: '<input type="range" aria-controls />' },
{ code: '<input type="range" aria-describedby />' },
{ code: '<input type="range" aria-disabled />' },
{ code: '<input type="range" aria-dropeffect />' },
{ code: '<input type="range" aria-flowto />' },
{ code: '<input type="range" aria-grabbed />' },
{ code: '<input type="range" aria-haspopup />' },
{ code: '<input type="range" aria-hidden />' },
{ code: '<input type="range" aria-invalid />' },
{ code: '<input type="range" aria-label />' },
{ code: '<input type="range" aria-labelledby />' },
{ code: '<input type="range" aria-live />' },
{ code: '<input type="range" aria-owns />' },
{ code: '<input type="range" aria-relevant />' },
{ code: '<input type="range" aria-valuetext />' },
// these will have role of `textbox`,
{ code: '<input type="email" aria-disabled />' },
{ code: '<input type="password" aria-disabled />' },
{ code: '<input type="search" aria-disabled />' },
{ code: '<input type="tel" aria-disabled />' },
{ code: '<input type="url" aria-disabled />' },
{ code: '<input aria-disabled />' },
// OTHER TESTS
{ code: '<aside aria-expanded />' },
{ code: '<article aria-expanded />' },
{ code: '<body aria-expanded />' },
{ code: '<button aria-pressed />' },
{ code: '<datalist aria-expanded />' },
{ code: '<details aria-expanded />' },
{ code: '<dialog aria-expanded />' },
{ code: '<dl aria-expanded />' },
{ code: '<form aria-hidden />' },
{ code: '<h1 aria-hidden />' },
{ code: '<h2 aria-hidden />' },
{ code: '<h3 aria-hidden />' },
{ code: '<h4 aria-hidden />' },
{ code: '<h5 aria-hidden />' },
{ code: '<h6 aria-hidden />' },
{ code: '<hr aria-hidden />' },
{ code: '<li aria-expanded />' },
{ code: '<meter aria-atomic />' },
{ code: '<nav aria-expanded />' },
{ code: '<ol aria-expanded />' },
{ code: '<option aria-atomic />' },
{ code: '<output aria-expanded />' },
{ code: '<progress aria-atomic />' },
{ code: '<section aria-expanded />' },
{ code: '<select aria-expanded />' },
{ code: '<tbody aria-expanded />' },
{ code: '<textarea aria-hidden />' },
{ code: '<tfoot aria-expanded />' },
{ code: '<thead aria-expanded />' },
{ code: '<ul aria-expanded />' },
].concat(validTests).map(parserOptionsMapper),
invalid: [
// implicit basic checks
{
code: '<a href="#" aria-checked />',
errors: [errorMessage('aria-checked', 'link', 'a', true)],
},
{
code: '<area href="#" aria-checked />',
errors: [errorMessage('aria-checked', 'link', 'area', true)],
},
{
code: '<link href="#" aria-checked />',
errors: [errorMessage('aria-checked', 'link', 'link', true)],
},
{
code: '<img alt="" aria-checked />',
errors: [errorMessage('aria-checked', 'presentation', 'img', true)],
},
{
code: '<menu type="toolbar" aria-checked />',
errors: [errorMessage('aria-checked', 'toolbar', 'menu', true)],
},
{
code: '<aside aria-checked />',
errors: [errorMessage('aria-checked', 'complementary', 'aside', true)],
},
].concat(invalidTests).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,39 @@
/* eslint-env jest */
/**
* @fileoverview Enforce scope prop is only used on <th> elements.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/scope';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'The scope prop can only be used on <th> elements.',
type: 'JSXAttribute',
};
ruleTester.run('scope', rule, {
valid: [
{ code: '<div />;' },
{ code: '<div foo />;' },
{ code: '<th scope />' },
{ code: '<th scope="row" />' },
{ code: '<th scope={foo} />' },
{ code: '<th scope={"col"} {...props} />' },
{ code: '<Foo scope="bar" {...props} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<div scope />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,55 @@
/* eslint-env jest */
/**
* @fileoverview Enforce tabIndex value is not greater than zero.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import rule from '../../../src/rules/tabindex-no-positive';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'Avoid positive integer values for tabIndex.',
type: 'JSXAttribute',
};
ruleTester.run('tabindex-no-positive', rule, {
valid: [
{ code: '<div />;' },
{ code: '<div {...props} />' },
{ code: '<div id="main" />' },
{ code: '<div tabIndex={undefined} />' },
{ code: '<div tabIndex={`${undefined}`} />' },
{ code: '<div tabIndex={`${undefined}${undefined}`} />' },
{ code: '<div tabIndex={0} />' },
{ code: '<div tabIndex={-1} />' },
{ code: '<div tabIndex={null} />' },
{ code: '<div tabIndex={bar()} />' },
{ code: '<div tabIndex={bar} />' },
{ code: '<div tabIndex={"foobar"} />' },
{ code: '<div tabIndex="0" />' },
{ code: '<div tabIndex="-1" />' },
{ code: '<div tabIndex="-5" />' },
{ code: '<div tabIndex="-5.5" />' },
{ code: '<div tabIndex={-5.5} />' },
{ code: '<div tabIndex={-5} />' },
].map(parserOptionsMapper),
invalid: [
{ code: '<div tabIndex="1" />', errors: [expectedError] },
{ code: '<div tabIndex={1} />', errors: [expectedError] },
{ code: '<div tabIndex={"1"} />', errors: [expectedError] },
{ code: '<div tabIndex={`1`} />', errors: [expectedError] },
{ code: '<div tabIndex={1.589} />', errors: [expectedError] },
].map(parserOptionsMapper),
});

View File

@@ -0,0 +1,114 @@
/* eslint-env mocha */
import expect from 'expect';
import attributesComparator from '../../../src/util/attributesComparator';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
describe('attributesComparator', () => {
describe('base attributes', () => {
let baseAttributes;
let attributes;
describe('are undefined', () => {
describe('and attributes are undefined', () => {
it('should return true', () => {
expect(attributesComparator()).toBe(true);
});
});
});
describe('are empty', () => {
beforeEach(() => {
baseAttributes = [];
});
describe('and attributes', () => {
describe('are empty', () => {
attributes = [];
it('should return true', () => {
expect(attributesComparator(baseAttributes, attributes))
.toBe(true);
});
});
describe('have values', () => {
attributes = [
JSXAttributeMock('foo', 0),
JSXAttributeMock('bar', 'baz'),
];
it('should return true', () => {
expect(attributesComparator(baseAttributes, attributes))
.toBe(true);
});
});
});
});
describe('have values', () => {
beforeEach(() => {
baseAttributes = [
{
name: 'biz',
value: 1,
}, {
name: 'fizz',
value: 'pop',
}, {
name: 'fuzz',
value: 'lolz',
},
];
});
describe('and attributes', () => {
describe('are empty', () => {
attributes = [];
it('should return false', () => {
expect(attributesComparator(baseAttributes, attributes))
.toBe(false);
});
});
describe('have values', () => {
describe('and the values are the different', () => {
it('should return false', () => {
attributes = [
JSXAttributeMock('biz', 2),
JSXAttributeMock('ziff', 'opo'),
JSXAttributeMock('far', 'lolz'),
];
expect(attributesComparator(baseAttributes, attributes))
.toBe(false);
});
});
describe('and the values are a subset', () => {
it('should return true', () => {
attributes = [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('goo', 'gazz'),
];
expect(attributesComparator(baseAttributes, attributes))
.toBe(false);
});
});
describe('and the values are the same', () => {
it('should return true', () => {
attributes = [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('fuzz', 'lolz'),
];
expect(attributesComparator(baseAttributes, attributes))
.toBe(true);
});
});
describe('and the values are a superset', () => {
it('should return true', () => {
attributes = [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('fuzz', 'lolz'),
JSXAttributeMock('dar', 'tee'),
];
expect(attributesComparator(baseAttributes, attributes))
.toBe(true);
});
});
});
});
});
});
});

View File

@@ -0,0 +1,49 @@
/* eslint-env jest */
import assert from 'assert';
import getSuggestion from '../../../src/util/getSuggestion';
describe('spell check suggestion API', () => {
it('should return no suggestions given empty word and no dictionary', () => {
const word = '';
const expected = [];
const actual = getSuggestion(word);
assert.deepEqual(expected, actual);
});
it('should return no suggestions given real word and no dictionary', () => {
const word = 'foo';
const expected = [];
const actual = getSuggestion(word);
assert.deepEqual(expected, actual);
});
it('should return correct suggestion given real word and a dictionary', () => {
const word = 'fo';
const dictionary = ['foo', 'bar', 'baz'];
const expected = ['foo'];
const actual = getSuggestion(word, dictionary);
assert.deepEqual(expected, actual);
});
it('should return multiple correct suggestions given real word and a dictionary', () => {
const word = 'theer';
const dictionary = ['there', 'their', 'foo', 'bar'];
const expected = ['there', 'their'];
const actual = getSuggestion(word, dictionary);
assert.deepEqual(expected, actual);
});
it('should return correct # of suggestions given the limit argument', () => {
const word = 'theer';
const dictionary = ['there', 'their', 'foo', 'bar'];
const limit = 1;
const expected = 1;
const actual = getSuggestion(word, dictionary, limit).length;
assert.deepEqual(expected, actual);
});
});

View File

@@ -0,0 +1,131 @@
/* eslint-env jest */
import getTabIndex from '../../../src/util/getTabIndex';
import IdentifierMock from '../../../__mocks__/IdentifierMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
describe('getTabIndex', () => {
describe('tabIndex is defined', () => {
describe('as a number ', () => {
describe('zero', () => {
it('should return zero', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', 0),
),
).toBe(0);
});
});
describe('positive integer', () => {
it('should return the integer', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', 1),
),
).toBe(1);
});
});
describe('negative integer', () => {
it('should return the integer', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', -1),
),
).toBe(-1);
});
});
describe('float', () => {
it('should return undefined', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', 9.1),
),
).toBeUndefined();
});
});
});
describe('as a string', () => {
describe('empty', () => {
it('should return undefined', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', ''),
),
).toBeUndefined();
});
});
describe('which converts to a number', () => {
it('should return an integer', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', '0'),
),
).toBe(0);
});
});
describe('which is NaN', () => {
it('should return undefined', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', '0a'),
),
).toBeUndefined();
});
});
});
describe('as a boolean', () => {
describe('true', () => {
it('should return undefined', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', true),
),
).toBeUndefined();
});
});
describe('false', () => {
it('should return undefined', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', false),
),
).toBeUndefined();
});
});
});
describe('as an expression', () => {
describe('function expression', () => {
it('should return the correct type', () => {
const attr = function mockFn() { return 0; };
expect(
typeof getTabIndex(
JSXAttributeMock('tabIndex', attr),
),
).toEqual('function');
});
});
describe('variable expression', () => {
it('should return the Identifier name', () => {
const name = 'identName';
expect(
getTabIndex(
JSXAttributeMock(
'tabIndex',
IdentifierMock(name),
true,
),
),
).toEqual(name);
});
});
});
});
describe('tabIndex is not defined', () => {
it('should return undefined', () => {
expect(
getTabIndex(
JSXAttributeMock('tabIndex', undefined),
),
).toBeUndefined();
});
});
});

View File

@@ -0,0 +1,82 @@
/* eslint-env jest */
import hasAccessibleChild from '../../../src/util/hasAccessibleChild';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock';
describe('hasAccessibleChild', () => {
describe('has no children and does not set dangerouslySetInnerHTML', () => {
it('returns false', () => {
expect(
hasAccessibleChild(JSXElementMock('div', [])),
).toBe(false);
});
});
describe('has no children and sets dangerouslySetInnerHTML', () => {
it('Returns true', () => {
const prop = JSXAttributeMock('dangerouslySetInnerHTML', true);
const element = JSXElementMock('div', [prop], []);
expect(hasAccessibleChild(element)).toBe(true);
});
});
describe('has children', () => {
it('Returns true for a Literal child', () => {
const child = {
type: 'Literal',
value: 'foo',
};
const element = JSXElementMock('div', [], [child]);
expect(hasAccessibleChild(element)).toBe(true);
});
it('Returns true for visible child JSXElement', () => {
const child = JSXElementMock('div', []);
const element = JSXElementMock('div', [], [child]);
expect(hasAccessibleChild(element)).toBe(true);
});
it('Returns false for hidden child JSXElement', () => {
const ariaHiddenAttr = JSXAttributeMock('aria-hidden', true);
const child = JSXElementMock('div', [ariaHiddenAttr]);
const element = JSXElementMock('div', [], [child]);
expect(hasAccessibleChild(element)).toBe(false);
});
it('Returns true for defined JSXExpressionContainer', () => {
const expression = {
type: 'Identifier',
name: 'foo',
};
const child = JSXExpressionContainerMock(expression);
const element = JSXElementMock('div', [], [child]);
expect(hasAccessibleChild(element)).toBe(true);
});
it('Returns false for undefined JSXExpressionContainer', () => {
const expression = {
type: 'Identifier',
name: 'undefined',
};
const child = JSXExpressionContainerMock(expression);
const element = JSXElementMock('div', [], [child]);
expect(hasAccessibleChild(element)).toBe(false);
});
it('Returns false for unknown child type', () => {
const child = {
type: 'Unknown',
};
const element = JSXElementMock('div', [], [child]);
expect(hasAccessibleChild(element)).toBe(false);
});
it('Returns true with children passed as a prop', () => {
const children = JSXAttributeMock('children', true);
const element = JSXElementMock('div', [children], []);
expect(hasAccessibleChild(element)).toBe(true);
});
});
});

View File

@@ -0,0 +1,44 @@
/* eslint-env mocha */
import expect from 'expect';
import { elementType } from 'jsx-ast-utils';
import isAbstractRole from '../../../src/util/isAbstractRole';
import {
genElementSymbol,
genAbstractRoleElements,
genNonAbstractRoleElements,
} from '../../../__mocks__/genInteractives';
describe('isAbstractRole', () => {
describe('JSX Components (no tagName)', () => {
it('should NOT identify them as abstract role elements', () => {
expect(isAbstractRole(undefined, []))
.toBe(false);
});
});
describe('elements with an abstract role', () => {
genAbstractRoleElements().forEach(
({ openingElement }) => {
const attributes = openingElement.attributes;
it(`should identify \`${genElementSymbol(openingElement)}\` as an abstract role element`, () => {
expect(isAbstractRole(
elementType(openingElement),
attributes,
)).toBe(true);
});
},
);
});
describe('elements with a non-abstract role', () => {
genNonAbstractRoleElements().forEach(
({ openingElement }) => {
const attributes = openingElement.attributes;
it(`should NOT identify \`${genElementSymbol(openingElement)}\` as an abstract role element`, () => {
expect(isAbstractRole(
elementType(openingElement),
attributes,
)).toBe(false);
});
},
);
});
});

View File

@@ -0,0 +1,81 @@
/* eslint-env mocha */
import expect from 'expect';
import { elementType } from 'jsx-ast-utils';
import isInteractiveElement from '../../../src/util/isInteractiveElement';
import {
genElementSymbol,
genIndeterminantInteractiveElements,
genInteractiveElements,
genInteractiveRoleElements,
genNonInteractiveElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
describe('isInteractiveElement', () => {
describe('JSX Components (no tagName)', () => {
it('should identify them as interactive elements', () => {
expect(isInteractiveElement(undefined, []))
.toBe(false);
});
});
describe('interactive elements', () => {
genInteractiveElements().forEach(
({ openingElement }) => {
it(`should identify \`${genElementSymbol(openingElement)}\` as an interactive element`, () => {
expect(isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(true);
});
},
);
});
describe('interactive role elements', () => {
genInteractiveRoleElements().forEach(
({ openingElement }) => {
it(`should NOT identify \`${genElementSymbol(openingElement)}\` as an interactive element`, () => {
expect(isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(false);
});
},
);
});
describe('non-interactive elements', () => {
genNonInteractiveElements().forEach(
({ openingElement }) => {
it(`should NOT identify \`${genElementSymbol(openingElement)}\` as an interactive element`, () => {
expect(isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(false);
});
},
);
});
describe('non-interactive role elements', () => {
genNonInteractiveRoleElements().forEach(
({ openingElement }) => {
it(`should NOT identify \`${genElementSymbol(openingElement)}\` as an interactive element`, () => {
expect(isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(false);
});
},
);
});
describe('indeterminate elements', () => {
genIndeterminantInteractiveElements().forEach(
({ openingElement }) => {
it(`should NOT identify \`${openingElement.name.name}\` as an interactive element`, () => {
expect(isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(false);
});
},
);
});
});

View File

@@ -0,0 +1,49 @@
/* eslint-env mocha */
import expect from 'expect';
import { elementType } from 'jsx-ast-utils';
import isInteractiveRole from '../../../src/util/isInteractiveRole';
import {
genElementSymbol,
genInteractiveRoleElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
describe('isInteractiveRole', () => {
describe('JSX Components (no tagName)', () => {
it('should identify them as interactive role elements', () => {
expect(isInteractiveRole(undefined, []))
.toBe(false);
});
});
describe('elements with a non-interactive role', () => {
genNonInteractiveRoleElements().forEach(
({ openingElement }) => {
const attributes = openingElement.attributes;
it(`should not identify \`${genElementSymbol(openingElement)}\` as an interactive role element`, () => {
expect(isInteractiveRole(
elementType(openingElement),
attributes,
)).toBe(false);
});
},
);
});
describe('elements without a role', () => {
it('should not identify them as interactive role elements', () => {
expect(isInteractiveRole('div', [])).toBe(false);
});
});
describe('elements with an interactive role', () => {
genInteractiveRoleElements().forEach(
({ openingElement }) => {
const attributes = openingElement.attributes;
it(`should identify \`${genElementSymbol(openingElement)}\` as an interactive role element`, () => {
expect(isInteractiveRole(
elementType(openingElement),
attributes,
)).toBe(true);
});
},
);
});
});

View File

@@ -0,0 +1,81 @@
/* eslint-env mocha */
import expect from 'expect';
import { elementType } from 'jsx-ast-utils';
import isNonInteractiveElement from '../../../src/util/isNonInteractiveElement';
import {
genElementSymbol,
genIndeterminantInteractiveElements,
genInteractiveElements,
genInteractiveRoleElements,
genNonInteractiveElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
describe('isNonInteractiveElement', () => {
describe('JSX Components (no tagName)', () => {
it('should identify them as interactive elements', () => {
expect(isNonInteractiveElement(undefined, []))
.toBe(false);
});
});
describe('non-interactive elements', () => {
genNonInteractiveElements().forEach(
({ openingElement }) => {
it(`should identify \`${genElementSymbol(openingElement)}\` as a non-interactive element`, () => {
expect(isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(true);
});
},
);
});
describe('non-interactive role elements', () => {
genNonInteractiveRoleElements().forEach(
({ openingElement }) => {
it(`should NOT identify \`${genElementSymbol(openingElement)}\` as a non-interactive element`, () => {
expect(isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(false);
});
},
);
});
describe('interactive elements', () => {
genInteractiveElements().forEach(
({ openingElement }) => {
it(`should NOT identify \`${genElementSymbol(openingElement)}\` as a non-interactive element`, () => {
expect(isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(false);
});
},
);
});
describe('interactive role elements', () => {
genInteractiveRoleElements().forEach(
({ openingElement }) => {
it(`should NOT identify \`${genElementSymbol(openingElement)}\` as a non-interactive element`, () => {
expect(isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(false);
});
},
);
});
describe('indeterminate elements', () => {
genIndeterminantInteractiveElements().forEach(
({ openingElement }) => {
it(`should NOT identify \`${openingElement.name.name}\` as a non-interactive element`, () => {
expect(isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
)).toBe(false);
});
},
);
});
});

View File

@@ -0,0 +1,49 @@
/* eslint-env mocha */
import expect from 'expect';
import { elementType } from 'jsx-ast-utils';
import isNonInteractiveRole from '../../../src/util/isNonInteractiveRole';
import {
genElementSymbol,
genInteractiveRoleElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
describe('isNonInteractiveRole', () => {
describe('JSX Components (no tagName)', () => {
it('should identify them as interactive role elements', () => {
expect(isNonInteractiveRole(undefined, []))
.toBe(false);
});
});
describe('elements with a non-interactive role', () => {
genNonInteractiveRoleElements().forEach(
({ openingElement }) => {
const attributes = openingElement.attributes;
it(`should identify \`${genElementSymbol(openingElement)}\` as non-interactive role element`, () => {
expect(isNonInteractiveRole(
elementType(openingElement),
attributes,
)).toBe(true);
});
},
);
});
describe('elements without a role', () => {
it('should not identify them as non-interactive role elements', () => {
expect(isNonInteractiveRole('div', [])).toBe(false);
});
});
describe('elements with an interactive role', () => {
genInteractiveRoleElements().forEach(
({ openingElement }) => {
const attributes = openingElement.attributes;
it(`should NOT identify \`${genElementSymbol(openingElement)}\` as a non-interactive role element`, () => {
expect(isNonInteractiveRole(
elementType(openingElement),
attributes,
)).toBe(false);
});
},
);
});
});

View File

@@ -0,0 +1,45 @@
/* eslint-env mocha */
import expect from 'expect';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
describe('parserOptionsMapper', () => {
it('should return an test case object', () => {
const testCase = {
code: '<div />',
errors: [],
options: {},
};
expect(parserOptionsMapper(testCase)).toEqual({
code: '<div />',
errors: [],
options: {},
parserOptions: {
ecmaVersion: 6,
ecmaFeatures: {
jsx: true,
},
},
});
});
it('should allow for overriding parserOptions', () => {
const testCase = {
code: '<div />',
errors: [],
options: {},
parserOptions: {
ecmaVersion: 5,
},
};
expect(parserOptionsMapper(testCase)).toEqual({
code: '<div />',
errors: [],
options: {},
parserOptions: {
ecmaVersion: 5,
ecmaFeatures: {
jsx: true,
},
},
});
});
});

View File

@@ -0,0 +1,16 @@
/* eslint-env jest */
import assert from 'assert';
import { generateObjSchema, arraySchema } from '../../../src/util/schemas';
describe('schemas', () => {
it('should generate an object schema with correct properties', () => {
const schema = generateObjSchema({
foo: 'bar',
baz: arraySchema,
});
const properties = schema.properties || {};
assert.deepEqual(properties.foo, 'bar');
assert.deepEqual(properties.baz.type, 'array');
});
});

View File

@@ -0,0 +1,23 @@
# accessible-emoji
Emojis have become a common way of communicating content to the end user. To a person using a screenreader, however, he/she may not be aware that this content is there at all. By wrapping the emoji in a `<span>`, giving it the `role="img"`, and providing a useful description in `aria-label`, the screenreader will treat the emoji as an image in the accessibility tree with an accessible name for the end user.
#### Resources
1. [Lèonie Watson](http://tink.uk/accessible-emoji/)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<span role="img" aria-label="Snowman">&#9731;</span>
<span role="img" aria-label="Panda">🐼</span>
<span role="img" aria-labelledby="panda1">🐼</span>
```
### Fail
```jsx
<span>🐼</span>
<i role="img" aria-label="Panda">🐼</i>
```

View File

@@ -0,0 +1,143 @@
# alt-text
Enforce that all elements that require alternative text have meaningful information to relay back to the end user. This is a critical component of accessibility for screenreader users in order for them to understand the content's purpose on the page. By default, this rule checks for alternative text on the following elements: `<img>`, `<area>`, `<input type="image">`, and `<object>`.
#### Resources
1. [aXe object-alt](https://dequeuniversity.com/rules/axe/2.1/object-alt)
2. [aXe image-alt](https://dequeuniversity.com/rules/axe/2.1/image-alt)
3. [aXe input-image-alt](https://dequeuniversity.com/rules/axe/2.1/input-image-alt)
4. [aXe area-alt](https://dequeuniversity.com/rules/axe/2.1/area-alt)
## How to resolve
### `<img>`
An `<img>` must have the `alt` prop set with meaningful text or as an empty string to indicate that it is an image for decoration.
For images that are being used as icons for a button or control, the `alt` prop should be set to an empty string (`alt=""`).
```js
<button>
<img src="icon.png" alt="" />
Save
</button>
```
The content of an `alt` attribute is used to calculate the accessible label of an element, whereas the text content is used to produce a label for the element. For this reason, adding a label to an icon can produce a confusing or duplicated label on a control that already has appropriate text content.
### `<object>`
Add alternative text to all embedded `<object>` elements using either inner text, setting the `title` prop, or using the `aria-label` or `aria-labelledby` props.
### `<input type="image">`
All `<input type="image">` elements must have a non-empty `alt` prop set with a meaningful description of the image or have the `aria-label` or `aria-labelledby` props set.
### `<area>`
All clickable `<area>` elements within an image map have an `alt`, `aria-label` or `aria-labelledby` prop that describes the purpose of the link.
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/img-has-alt": [ 2, {
"elements": [ "img", "object", "area", "input[type=\"image\"]" ],
"img": ["Image"],
"object": ["Object"],
"area": ["Area"],
"input[type=\"image\"]": ["InputImage"]
}],
}
}
```
The `elements` option is a whitelist for DOM elements to check for alternative text. If an element is removed from the default set of elements (noted above), any custom components for that component will also be ignored. In order to indicate any custom wrapper components that should be checked, you can map the DOM element to an array of JSX custom components. This is a good use case when you have a wrapper component that simply renders an `img` element, for instance (like in React):
```jsx
// Image.js
const Image = props => {
const {
alt,
...otherProps
} = props;
return (
<img alt={alt} {...otherProps} />
);
}
...
// Header.js (for example)
...
return (
<header>
<Image alt="Logo" src="logo.jpg" />
</header>
);
```
Note that passing props as spread attribute without explicitly the necessary accessibility props defined will cause this rule to fail. Explicitly pass down the set of props needed for rule to pass. Use `Image` component above as a reference for destructuring and applying the prop. **It is a good thing to explicitly pass props that you expect to be passed for self-documentation.** For example:
#### Bad
```jsx
function Foo(props) {
return <img {...props} />
}
```
#### Good
```jsx
function Foo({ alt, ...props}) {
return <img alt={alt} {...props} />
}
// OR
function Foo(props) {
const {
alt,
...otherProps
} = props;
return <img alt={alt} {...otherProps} />
}
```
### Succeed
```jsx
<img src="foo" alt="Foo eating a sandwich." />
<img src="foo" alt={"Foo eating a sandwich."} />
<img src="foo" alt={altText} />
<img src="foo" alt={`${person} smiling`} />
<img src="foo" alt="" />
<object aria-label="foo" />
<object aria-labelledby="id1" />
<object>Meaningful description</object>
<object title="An object" />
<area aria-label="foo" />
<area aria-labelledby="id1" />
<area alt="This is descriptive!" />
<input type="image" alt="This is descriptive!" />
<input type="image" aria-label="foo" />
<input type="image" aria-labelledby="id1" />
```
### Fail
```jsx
<img src="foo" />
<img {...props} />
<img {...props} alt /> // Has no value
<img {...props} alt={undefined} /> // Has no value
<img {...props} alt={`${undefined}`} /> // Has no value
<img src="foo" role="presentation" /> // Avoid ARIA if it can be achieved without
<img src="foo" role="none" /> // Avoid ARIA if it can be achieved without
<object {...props} />
<area {...props} />
<input type="image" {...props} />
```

View File

@@ -0,0 +1,53 @@
# anchor-has-content
Enforce that anchors have content and that the content is accessible to screen readers. Accessible means that it is not hidden using the `aria-hidden` prop. Refer to the references to learn about why this is important.
#### References
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/link-name)
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/anchor-has-content": [ 2, {
"components": [ "Anchor" ],
}],
}
}
```
For the `components` option, these strings determine which JSX elements (**always including** `<a>`) should be checked for having content. This is a good use case when you have a wrapper component that simply renders an `a` element (like in React):
```js
// Anchor.js
const Anchor = props => {
return (
<a {...props}>{ props.children }</a>
);
}
...
// CreateAccount.js (for example)
...
return (
<Anchor>Create Account</Anchor>
);
```
### Succeed
```jsx
<a>Anchor Content!</a>
<a><TextWrapper /><a>
<a dangerouslySetInnerHTML={{ __html: 'foo' }} />
```
### Fail
```jsx
<a />
<a><TextWrapper aria-hidden /></a>
```

View File

@@ -0,0 +1,183 @@
# anchor-is-valid
The HTML `<a>` element, with a valid `href` attribute, is formally defined as representing a **hyperlink**. That is, a link between one HTML document and another, or between one location inside an HTML document and another location inside the same document.
In fact, the interactive, underlined `<a>` element has become so synonymous with web navigation that this expectation has become entrenched inside browsers, assistive technologies such as screen readers and in how people generally expect the internet to behave. In short, anchors should navigate.
The use of JavaScript frameworks and libraries, like _React_, has made it very easy to add or subtract functionality from the standard HTML elements. This has led to _anchors_ often being used in applications based on how they look and function instead of what they represent.
Whilst it is possible, for example, to turn the `<a>` element into a fully functional `<button>` element with ARIA, the native user agent implementations of HTML elements are to be preferred over custom ARIA solutions.
## How do I resolve this error?
### Case: I want to perform an action and need a clickable UI element
The native user agent implementations of the `<a>` and `<button>` elements not only differ in how they look and how they act when activated, but also in how the user is expected to interact with them. Both are perfectly clickable when using a mouse, but keyboard users expect `<a>` to activate on `enter` only and `<button>` to activate on _both_ `enter` and `space`.
This is exacerbated by the expectation sighted users have of how _buttons_ and _anchors_ work based on their appearance. Therefore we find that using _anchors_ as _buttons_ can easily create confusion without a relatively complicated ARIA and CSS implementation that only serves to create an element HTML already offers and browsers already implement fully accessibly.
We are aware that sometimes _anchors_ are used instead of _buttons_ to achieve a specific visual design. When using the `<button>` element this can still be achieved with styling but, due to the meaning many people attach to the standard underlined `<a>` due its appearance, please reconsider this in the design.
Consider the following:
```jsx
<a href="javascript:void(0)" onClick={foo} >Perform action</a>
<a href="#" onClick={foo} >Perform action</a>
<a onClick={foo} >Perform action</a>
```
All these _anchor_ implementations indicate that the element is only used to execute JavaScript code. All the above should be replaced with:
```jsx
<button onClick={foo} >Perform action</button>
```
### Case: I want navigable links
An `<a>` element without an `href` attribute no longer functions as a hyperlink. That means that it can no longer accept keyboard focus or be clicked on. The documentation for [no-noninteractive-tabindex](no-noninteractive-tabindex.md) explores this further. Preferably use another element (such as `div` or `span`) for display of text.
To properly function as a hyperlink, the `href` attribute should be present and also contain a valid _URL_. _JavaScript_ strings, empty values or using only **#** are not considered valid `href` values.
Valid `href` attributes values are:
```jsx
<a href="/some/valid/uri" >Navigate to page</a>
<a href="/some/valid/uri#top" >Navigate to page and location</a>
<a href="#top" >Navigate to internal page location</a>
```
### Case: I need the HTML to be interactive, don't I need to use an a tag for that?
An `<a>` tag is not inherently interactive. Without an href attribute, it really is no different to a `<div>`.
Let's look at an example that is not accessible by all users:
```jsx
<a
className={'thing'}
onMouseEnter={() => this.setState({showSomething: true})}>
{label}
</a>
```
If you need to create an interface element that the user can click on, consider using a button:
```jsx
<button
className={'thing'}
onClick={() => this.setState({showSomething: true})}>
{label}
</button>
```
If you want to navigate while providing the user with extra functionality, for example in the `onMouseEnter` event, use an anchor with an `href` attribute containing a URL or path as its value.
```jsx
<a
href={someValidPath}
className={'thing'}
onMouseEnter={() => this.setState({showSomething: true})}>
{label}
</a>
```
If you need to create an interface element that the user can mouse over or mouse out of, consider using a div element. In this case, you may need to apply a role of presentation or an interactive role. Interactive ARIA roles include `button`, `link`, `checkbox`, `menuitem`, `menuitemcheckbox`, `menuitemradio`, `option`, `radio`, `searchbox`, `switch` and `textbox`.
```jsx
<div
role="menuitem"
className={'thing'}
onClick={() => this.setState({showSomething: true})}>
onMouseEnter={() => this.setState({showSomething: true})}>
{label}
</div>
```
In the example immediately above an `onClick` event handler was added to provide the same experience mouse users enjoy to keyboard-only and touch-screen users. Never fully rely on mouse events alone to expose functionality.
### References
1. [WebAIM - Introduction to Links and Hypertext](http://webaim.org/techniques/hypertext/)
1. [Links vs. Buttons in Modern Web Applications](https://marcysutton.com/links-vs-buttons-in-modern-web-applications/)
1. [Using ARIA - Notes on ARIA use in HTML](https://www.w3.org/TR/using-aria/#NOTES)
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/anchor-is-valid": [ "error", {
"components": [ "Link" ],
"specialLink": [ "hrefLeft", "hrefRight" ],
"aspects": [ "noHref", "invalidHref", "preferButton" ]
}]
}
}
```
For the `components` option, these strings determine which JSX elements (**always including** `<a>`) should be checked for the props designated in the `specialLink` options (**always including** `href`). This is a good use case when you have a wrapper component that simply renders an `<a>` element (like in React):
```js
// Link.js
const Link = props => <a {...props}>A link</a>;
...
// NavBar.js (for example)
...
return (
<nav>
<Link href="/home" />
</nav>
);
```
For the `aspects` option, these strings determine which sub-rules are run. This allows omission of certain error types in restrictive environments.
- `noHref`: Checks whether an anchor contains an `href` attribute.
- `invalidHref`: Checks if a given `href` value is valid.
- `preferButton`: Checks if anchors have been used as buttons.
The option can be used on its own or with the `components` and `specialLink` options.
If omitted, all sub-rule aspects will be run by default. This is the recommended configuration for all cases except where the rule becomes unusable due to well founded restrictions.
The option must contain at least one `aspect`.
### Succeed
```jsx
<a href="https://github.com" />
<a href="#section" />
<a href="foo" />
<a href="/foo/bar" />
<a href={someValidPath} />
<a href="https://github.com" onClick={foo} />
<a href="#section" onClick={foo} />
<a href="foo" onClick={foo} />
<a href="/foo/bar" onClick={foo} />
<a href={someValidPath} onClick={foo} />
```
### Fail
Anchors should be a button:
```jsx
<a onClick={foo} />
<a href="#" onClick={foo} />
<a href={"#"} onClick={foo} />
<a href={`#`} onClick={foo} />
<a href="javascript:void(0)" onClick={foo} />
<a href={"javascript:void(0)"} onClick={foo} />
<a href={`javascript:void(0)`} onClick={foo} />
```
Missing `href` attribute:
```jsx
<a />
<a href={undefined} />
<a href={null} />
```
Invalid `href` attribute:
```jsx
<a href="#" />
<a href={"#"} />
<a href={`#`} />
<a href="javascript:void(0)" />
<a href={"javascript:void(0)"} />
<a href={`javascript:void(0)`} />
```

View File

@@ -0,0 +1,46 @@
# aria-activedescendant-has-tabindex
`aria-activedescendant` is used to manage focus within a [composite widget](https://www.w3.org/TR/wai-aria/roles#composite_header).
The element with the attribute `aria-activedescendant` retains the active document
focus; it indicates which of its child elements has secondary focus by assigning
the ID of that element to the value of `aria-activedescendant`. This pattern is
used to build a widget like a search typeahead select list. The search input box
retains document focus so that the user can type in the input. If the down arrow
key is pressed and a search suggestion is highlighted, the ID of the suggestion
element will be applied as the value of `aria-activedescendant` on the input
element.
Because an element with `aria-activedescendant` must be tabbable, it must either
have an inherent `tabIndex` of zero or declare a `tabIndex` of zero with the `tabIndex`
attribute.
#### References
1. [MDN](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-activedescendant_attribute)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<CustomComponent />
<CustomComponent aria-activedescendant={someID} />
<CustomComponent aria-activedescendant={someID} tabIndex={0} />
<CustomComponent aria-activedescendant={someID} tabIndex={-1} />
<div />
<input />
<div tabIndex={0} />
<div aria-activedescendant={someID} tabIndex={0} />
<div aria-activedescendant={someID} tabIndex="0" />
<div aria-activedescendant={someID} tabIndex={1} />
<input aria-activedescendant={someID} />
<input aria-activedescendant={someID} tabIndex={0} />
```
### Fail
```jsx
<div aria-activedescendant={someID} />
<div aria-activedescendant={someID} tabIndex={-1} />
<div aria-activedescendant={someID} tabIndex="-1" />
<input aria-activedescendant={someID} tabIndex={-1} />
```

View File

@@ -0,0 +1,22 @@
# aria-props
Elements cannot use an invalid ARIA attribute. This will fail if it finds an `aria-*` property that is not listed in [WAI-ARIA States and Properties spec](https://www.w3.org/TR/wai-aria/states_and_properties#state_prop_def).
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<!-- Good: Labeled using correctly spelled aria-labelledby -->
<div id="address_label">Enter your address</div>
<input aria-labelledby="address_label">
```
### Fail
```jsx
<!-- Bad: Labeled using incorrectly spelled aria-labeledby -->
<div id="address_label">Enter your address</div>
<input aria-labeledby="address_label">
```

View File

@@ -0,0 +1,24 @@
# aria-proptypes
ARIA state and property values must be valid.
#### References
1. [Spec](https://www.w3.org/TR/wai-aria/states_and_properties)
2. [AX_ARIA_04](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_04)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<!-- Good: the aria-hidden state is of type true/false -->
<span aria-hidden="true">foo</span>
```
### Fail
```jsx
<!-- Bad: the aria-hidden state is of type true/false -->
<span aria-hidden="yes">foo</span>
```

View File

@@ -0,0 +1,39 @@
# aria-role
Elements with ARIA roles must use a valid, non-abstract ARIA role. A reference to role defintions can be found at [WAI-ARIA](https://www.w3.org/TR/wai-aria/roles#role_definitions) site.
[AX_ARIA_01](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_01)
[DPUB-ARIA roles](https://www.w3.org/TR/dpub-aria-1.0/)
## Rule details
This rule takes one optional object argument of type object:
```
{
"rules": {
"jsx-a11y/aria-role": [ 2, {
"ignoreNonDOM": true
}],
}
}
```
For the `ignoreNonDOM` option, this determines if developer created components are checked.
### Succeed
```jsx
<div role="button"></div> <!-- Good: "button" is a valid ARIA role -->
<div role={role}></div> <!-- Good: role is a variable & cannot be determined until runtime. -->
<div></div> <!-- Good: No ARIA role -->
<Foo role={role}></Foo> <!-- Good: ignoreNonDOM is set to true -->
```
### Fail
```jsx
<div role="datepicker"></div> <!-- Bad: "datepicker" is not an ARIA role -->
<div role="range"></div> <!-- Bad: "range" is an _abstract_ ARIA role -->
<div role=""></div> <!-- Bad: An empty ARIA role is not allowed -->
<Foo role={role}></Foo> <!-- Bad: ignoreNonDOM is set to false or not set -->
```

View File

@@ -0,0 +1,23 @@
# aria-unsupported-elements
Certain reserved DOM elements do not support ARIA roles, states and properties. This is often because they are not visible, for example `meta`, `html`, `script`, `style`. This rule enforces that these DOM elements do not contain the `role` and/or `aria-*` props.
#### References
1. [AX_ARIA_12](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_12)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<!-- Good: the meta element should not be given any ARIA attributes -->
<meta charset="UTF-8" />
```
### Fail
```jsx
<!-- Bad: the meta element should not be given any ARIA attributes -->
<meta charset="UTF-8" aria-hidden="false" />
```

View File

@@ -0,0 +1,19 @@
# click-events-have-key-events
Enforce `onClick` is accompanied by at least one of the following: `onKeyUp`, `onKeyDown`, `onKeyPress`. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and screenreader users.
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<div onClick={() => {}} onKeyDown={this.handleKeyDown} />
<div onClick={() => {}} onKeyUp={this.handleKeyUp} />
<div onClick={() => {}} onKeyPress={this.handleKeyPress} />
```
### Fail
```jsx
<div onClick={() => {}} />
```

View File

@@ -0,0 +1,60 @@
# heading-has-content
Enforce that heading elements (`h1`, `h2`, etc.) have content and that the content is accessible to screen readers. Accessible means that it is not hidden using the `aria-hidden` prop. Refer to the references to learn about why this is important.
#### References
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/empty-heading)
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/heading-has-content": [ 2, {
"components": [ "MyHeading" ],
}],
}
}
```
For the `components` option, these strings determine which JSX elements (**always including** `<h1>` thru `<h6>`) should be checked for having content. This is a good use case when you have a wrapper component that simply renders an `h1` element (like in React):
```js
// Header.js
const Header = props => {
return (
<h1 {...props}>{ props.children }</h1>
);
}
...
// CreateAccount.js (for example)
...
return (
<Header>Create Account</Header>
);
```
#### Bad
```jsx
function Foo(props) {
return <label {...props} />
}
```
### Succeed
```jsx
<h1>Heading Content!</h1>
<h1><TextWrapper /><h1>
<h1 dangerouslySetInnerHTML={{ __html: 'foo' }} />
```
### Fail
```jsx
<h1 />
<h1><TextWrapper aria-hidden />
```

View File

@@ -0,0 +1,51 @@
# href-no-hash
Enforce an anchor element's href prop value is not just #. You should use something more descriptive, or use a button instead.
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/href-no-hash": [ 2, {
"components": [ "Link" ],
"specialLink": [ "hrefLeft", "hrefRight" ]
}],
}
}
```
For the `components` option, these strings determine which JSX elements (**always including** `<a>`) should be checked for the props designated in the `specialLink` options (**always including** `href`). This is a good use case when you have a wrapper component that simply renders an `a` element (like in React):
```js
// Link.js
const Link = props => <a {...props}>A link</a>;
...
// NavBar.js (for example)
...
return (
<nav>
<Link href="/home" />
</nav>
);
```
### Succeed
```jsx
<a href="https://github.com" />
<a href="#section" />
<a href="foo" />
<a href={undefined} /> // This check will pass, but WTF?
```
### Fail
```jsx
<a href="#" />
<a href={"#"} />
<a href={`#`} />
```

View File

@@ -0,0 +1,23 @@
# html-has-lang
<html> elements must have the lang prop.
#### References
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/html-lang)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<html lang="en">
<html lang="en-US">
<html lang={language}>
```
### Fail
```jsx
<html>
```

View File

@@ -0,0 +1,29 @@
# iframe-has-title
`<iframe>` elements must have a unique title property to indicate its content to the user.
#### References
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/frame-title)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<iframe title="This is a unique title" />
<iframe title={uniqueTitle} />
```
### Fail
```jsx
<iframe />
<iframe {...props} />
<iframe title="" />
<iframe title={''} />
<iframe title={``} />
<iframe title={undefined} />
<iframe title={false} />
<iframe title={true} />
<iframe title={42} />
```

View File

@@ -0,0 +1,38 @@
# img-redundant-alt
Enforce img alt attribute does not contain the word image, picture, or photo. Screenreaders already announce `img` elements as an image. There is no need to use words such as *image*, *photo*, and/or *picture*.
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/img-redundant-alt": [ 2, {
"components": [ "Image" ],
"words": [ "Bild", "Foto" ],
}],
}
}
```
For the `components` option, these strings determine which JSX elements (**always including** `<img>`) should be checked for having redundant words in the `alt` prop value . This is a good use case when you have a wrapper component that simply renders an `img` element (like in React).
For the `words` option, these strings can be used to specify custom words that should be checked for in the alt prop, including `image`, `photo`, and `picture`. Useful for specifying words in other languages.
The rule will first check if `aria-hidden` is true to determine whether to enforce the rule. If the image is hidden, then rule will always succeed.
### Succeed
```jsx
<img src="foo" alt="Foo eating a sandwich." />
<img src="bar" aria-hidden alt="Picture of me taking a photo of an image" /> // Will pass because it is hidden.
<img src="baz" alt={`Baz taking a ${photo}`} /> // This is valid since photo is a variable name.
```
### Fail
```jsx
<img src="foo" alt="Photo of foo being weird." />
<img src="bar" alt="Image of me at a bar!" />
<img src="baz" alt="Picture of baz fixing a bug." />
```

View File

@@ -0,0 +1,143 @@
# interactive-supports-focus
Elements with an interactive role and interaction handlers (mouse or key press) must be focusable.
## How do I resolve this error?
### Case: I got the error "Elements with the '${role}' interactive role must be tabbable". How can I fix this?
This element is a stand-alone control like a button, a link or a form element. A user should be able to reach this element by pressing the tab key on their keyboard.
Add the `tabIndex` property to your component. A value of zero indicates that this element can be tabbed to.
```
<div
role="button"
tabIndex={0} />
```
-- or --
Replace the component with one that renders semantic html element like `<button>`, `<a href>` or `<input>` -- whichever fits your purpose.
Generally buttons, links and form elements should be reachable via tab key presses. An element that can be tabbed to is said to be in the _tab ring_.
### Case: I got the error "Elements with the '${role}' interactive role must be focusable". How can I fix this?
This element is part of a group of buttons, links, menu items, etc. Or this element is part of a composite widget. Composite widgets prescribe standard [keyboard interaction patterns](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav). Within a group of similar elements -- like a button bar -- or within a composite widget, elements that can be focused are given a tabindex of -1. This makes the element *focusable* but not *tabbable*. Generally one item in a group should have a tabindex of zero so that a user can tab to the component. Once an element in the component has focus, your key management behaviors will control traversal within the component's pieces. As the UI author, you will need to implement the key handling behaviors such as listening for traversal key (up/down/left/right) presses and moving the page focus between the focusable elements in your widget.
```
<div role="menu">
<div role="menuitem" tabIndex="0">Open</div>
<div role="menuitem" tabIndex="-1">Save</div>
<div role="menuitem" tabIndex="-1">Close</div>
</div>
```
In the example above, the first item in the group can be tabbed to. The developer provides the ability to traverse to the subsequent items via the up/down/left/right arrow keys. Traversing via arrow keys is not provided by the browser or the assistive technology. See [Fundamental Keyboard Navigation Conventions](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav) for information about established traversal behaviors for various UI widgets.
### Case: This element is not a button, link, menuitem, etc. It is catching bubbled events from elements that it contains
If your element is catching bubbled click or key events from descendant elements, then the proper role for this element is `presentation`.
```
<div
onClick={onClickHandler}
role="presentation">
<button>Save</button>
</div>
```
Marking an element with the role `presentation` indicates to assistive technology that this element should be ignored; it exists to support the web application and is not meant for humans to interact with directly.
### References
1. [AX_FOCUS_02](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_focus_02)
1. [Mozilla Developer Network - ARIA Techniques](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Keyboard_and_focus)
1. [Fundamental Keyboard Navigation Conventions](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav)
1. [WAI-ARIA Authoring Practices Guide - Design Patterns and Widgets](https://www.w3.org/TR/wai-aria-practices-1.1/#aria_ex)
## Rule details
This rule takes an options object with the key `tabbable`. The value is an array of interactive ARIA roles that should be considered tabbable, not just focusable. Any interactive role not included in this list will be flagged as needing to be focusable (tabindex of -1).
```
'jsx-a11y/interactive-supports-focus': [
'error',
{
tabbable: [
'button',
'checkbox',
'link',
'searchbox',
'spinbutton',
'switch',
'textbox',
],
},
]
```
The recommended options list interactive roles that act as form elements. Generally, elements with a role like `menuitem` are a part of a composite widget. Focus in a composite widget is controlled and moved programmatically to satisfy the prescribed [keyboard interaction pattern](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav) for the widget.
The list of possible values includes:
```
[
'button',
'checkbox',
'columnheader',
'combobox',
'grid',
'gridcell',
'link',
'listbox',
'menu',
'menubar',
'menuitem',
'menuitemcheckbox',
'menuitemradio',
'option',
'progressbar',
'radio',
'radiogroup',
'row',
'rowheader',
'searchbox',
'slider',
'spinbutton',
'switch',
'tab',
'tablist',
'textbox',
'toolbar',
'tree',
'treegrid',
'treeitem',
'doc-backlink',
'doc-biblioref',
'doc-glossref',
'doc-noteref',
]
```
### Succeed
```jsx
<!-- Good: div with onClick attribute is hidden from screen reader -->
<div aria-hidden onClick={() => void 0} />
<!-- Good: span with onClick attribute is in the tab order -->
<span onClick="doSomething();" tabIndex="0" role="button">Click me!</span>
<!-- Good: span with onClick attribute may be focused programmatically -->
<span onClick="doSomething();" tabIndex="-1" role="menuitem">Click me too!</span>
<!-- Good: anchor element with href is inherently focusable -->
<a href="javascript:void(0);" onClick="doSomething();">Click ALL the things!</a>
<!-- Good: buttons are inherently focusable -->
<button onClick="doSomething();">Click the button :)</button>
```
### Fail
```jsx
<!-- Bad: span with onClick attribute has no tabindex -->
<span onclick="submitForm();" role="button">Submit</span>
<!-- Bad: anchor element without href is not focusable -->
<a onclick="showNextPage();" role="button">Next page</a>
```

View File

@@ -0,0 +1,101 @@
# label-has-for
Enforce label tags have associated control.
There are two supported ways to associate a label with a control:
- nesting: by wrapping a control in a label tag
- id: by using the prop `htmlFor` as in `htmlFor=[ID of control]`
To fully cover 100% of assistive devices, you're encouraged to validate for both nesting and id.
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/label-has-for": [ 2, {
"components": [ "Label" ],
"required": {
"every": [ "nesting", "id" ]
}
}],
}
}
```
For the `components` option, these strings determine which JSX elements (**always including** `<label>`) should be checked for having `htmlFor` prop. This is a good use case when you have a wrapper component that simply renders a `label` element (like in React):
```js
// Label.js
const Label = props => {
const {
htmlFor,
...otherProps
} = props;
return (
<label htmlFor={htmlFor} {...otherProps} />
);
}
...
// CreateAccount.js (for example)
...
return (
<form>
<input id="firstName" type="text" />
<Label htmlFor="firstName">First Name</Label>
</form>
);
```
The `required` option (defaults to `"required": "id"`) determines which checks are activated. You're allowed to pass in one of the following types:
- string: must be one of the acceptable strings (`"nesting"` or `"id"`)
- object, must have one of the following properties:
- some: an array of acceptable strings, will pass if ANY of the requested checks passed
- every: an array of acceptable strings, will pass if ALL of the requested checks passed
Note that passing props as spread attribute without `htmlFor` explicitly defined will cause this rule to fail. Explicitly pass down `htmlFor` prop for rule to pass. The prop must have an actual value to pass. Use `Label` component above as a reference. **It is a good thing to explicitly pass props that you expect to be passed for self-documentation.** For example:
#### Bad
```jsx
function Foo(props) {
return <label {...props} />
}
```
#### Good
```jsx
function Foo({ htmlFor, ...props}) {
return <label htmlFor={htmlFor} {...props} />
}
// OR
function Foo(props) {
const {
htmlFor,
...otherProps
} = props;
return <label htmlFor={htmlFor} {...otherProps} />
}
```
### Succeed
```jsx
<input type="text" id="firstName" />
<label htmlFor="firstName">First Name</label>
```
### Fail
```jsx
<input type="text" id="firstName" />
<label>First Name</label>
```

View File

@@ -0,0 +1,25 @@
# lang
The `lang` prop on the `<html>` element must have a valid value based on ISO country and language codes.
#### References
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/valid-lang)
2. [ISO Language Codes](http://www.w3schools.com/tags/ref_language_codes.asp)
3. [ISO Country Codes](http://www.w3schools.com/tags/ref_country_codes.asp)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<html lang="en">
<html lang="en-US">
```
### Fail
```jsx
<html>
<html lang="foo">
```

View File

@@ -0,0 +1,40 @@
# media-has-caption
Providing captions for media is essential for deaf users to follow along. Captions should be a transcription or translation of the dialogue, sound effects, relevant musical cues, and other relevant audio information. Not only is this important for accessibility, but can also be useful for all users in the case that the media is unavailable (similar to `alt` text on an image when an image is unable to load).
The captions should contain all important and relevant information to understand the corresponding media. This may mean that the captions are not a 1:1 mapping of the dialogue in the media content.
### References
1.[aXe](https://dequeuniversity.com/rules/axe/2.1/audio-caption)
1.[aXe](https://dequeuniversity.com/rules/axe/2.1/video-caption)
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/media-has-caption": [ 2, {
"audio": [ "Audio" ],
"video": [ "Video" ],
"track": [ "Track" ],
}],
}
}
```
For the `audio`, `video`, and `track` options, these strings determine which JSX elements (**always including** their corresponding DOM element) should be used for this rule. This is a good use case when you have a wrapper component that simply renders an `audio`, `video`, or `track` element (like in React):
### Succeed
```jsx
<audio><track kind="captions" {...props} /></audio>
<video><track kind="captions" {...props} /></video>
```
### Fail
```jsx
<audio {...props} />
<video {...props} />
```

View File

@@ -0,0 +1,25 @@
# mouse-events-have-key-events
Enforce onmouseover/onmouseout are accompanied by onfocus/onblur. Coding for the keyboard is important for users with physical disabilities who cannot use a mouse, AT compatibility, and screenreader users.
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<div onMouseOver={ () => void 0 } onFocus={ () => void 0 } />
<div onMouseOut={ () => void 0 } onBlur={ () => void 0 } />
<div onMouseOver={ () => void 0 } onFocus={ () => void 0 } {...otherProps} />
<div onMouseOut={ () => void 0 } onBlur={ () => void 0 } {...otherProps} />
```
### Fail
In example 3 and 4 below, even if otherProps contains onBlur and/or onFocus, this rule will still fail. Props should be passed down explicitly for rule to pass.
```jsx
<div onMouseOver={ () => void 0 } />
<div onMouseOut={ () => void 0 } />
<div onMouseOver={ () => void 0 } {...otherProps} />
<div onMouseOut={ () => void 0 } {...otherProps} />
```

View File

@@ -0,0 +1,17 @@
# no-access-key
Enforce no accessKey prop on element. Access keys are HTML elements that allow web developers to assign keyboard shortcuts to elements. Inconsistencies between keyboard shortcuts and keyboard commands used by screenreader and keyboard only users create accessibility complications so to avoid complications, access keys should not be used.
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<div />
```
### Fail
```jsx
<div accessKey="h" />
```

View File

@@ -0,0 +1,35 @@
# no-autofocus
Enforce that autoFocus prop is not used on elements. Autofocusing elements can cause usability issues for sighted and non-sighted users, alike.
#### References
1. [w3c](https://w3c.github.io/html/sec-forms.html#autofocusing-a-form-control-the-autofocus-attribute)
## Rule details
This rule takes one optional object argument of type object:
```
{
"rules": {
"jsx-a11y/no-autofocus": [ 2, {
"ignoreNonDOM": true
}],
}
}
```
For the `ignoreNonDOM` option, this determines if developer created components are checked.
### Succeed
```jsx
<div />
```
### Fail
```jsx
<div autoFocus />
<div autoFocus="true" />
<div autoFocus="false" />
<div autoFocus={undefined} />
```

View File

@@ -0,0 +1,34 @@
# no-distracting-elements
Enforces that no distracting elements are used. Elements that can be visually distracting can cause accessibility issues with visually impaired users. Such elements are most likely deprecated, and should be avoided. By default, the following elements are visually distracting: `<marquee>` and `<blink>`.
#### References
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/marquee)
2. [Deque University](https://dequeuniversity.com/rules/axe/1.1/blink)
## Rule details
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/no-distracting-elements": [ 2, {
"elements": [ "marquee", "blink" ],
}],
}
}
```
For the `elements` option, these strings determine which JSX elements should be checked for usage. This shouldn't need to be configured unless you have a seriously compelling use case for these elements. You cannot add any additional elements than what is offered, as the schema is only valid with the provided enumerated list. If you have another element that you think may cause a11y issues due to visual impairment, please feel free to file an issue or send a PR!
### Succeed
```jsx
<div />
```
### Fail
```jsx
<marquee />
<blink />
```

View File

@@ -0,0 +1,63 @@
# no-interactive-element-to-noninteractive-role
Interactive HTML elements indicate _controls_ in the user interface. Interactive elements include `<a href>`, `<button>`, `<input>`, `<select>`, `<textarea>`.
Non-interactive HTML elements and non-interactive ARIA roles indicate _content_ and _containers_ in the user interface. Non-interactive elements include `<main>`, `<area>`, `<h1>` (,`<h2>`, etc), `<img>`, `<li>`, `<ul>` and `<ol>`.
[WAI-ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#usage_intro) should not be used to convert an interactive element to a non-interactive element. Non-interactive ARIA roles include `article`, `banner`, `complementary`, `img`, `listitem`, `main`, `region` and `tooltip`.
## How do I resolve this error?
### Case: The element should be a container, like an article
Wrap your interactive element in a `<div>` with the desired role.
```
<div role="article">
<button>Save</button>
</div>
```
### Case: The element should be content, like an image
Put the content inside your interactive element.
```
<div
role="button"
onClick={() => {}}
onKeyPress={() => {}}
tabIndex="0">
<div role="img" aria-label="Save" />
</div>
```
### References
1. [WAI-ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#usage_intro)
1. [WAI-ARIA Authoring Practices Guide - Design Patterns and Widgets](https://www.w3.org/TR/wai-aria-practices-1.1/#aria_ex)
1. [Fundamental Keyboard Navigation Conventions](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav)
1. [Mozilla Developer Network - ARIA Techniques](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Keyboard_and_focus)
## Rule details
The recommended options for this rule allow the `tr` element to be given a role of `presentation` (or its semantic equivalent `none`). Under normal circumstances, an element with an interactive role should not be semantically neutralized with `presentation` (or `none`).
Options are provided as an object keyed by HTML element name; the value is an array of interactive roles that are allowed on the specified element.
```
{
'no-interactive-element-to-noninteractive-role': [
'error',
{
tr: ['none', 'presentation'],
},
]
}
```
Under the recommended options, the following code is valid. It would be invalid under the strict rules.
```
<tr role="presentation" />
```

View File

@@ -0,0 +1,135 @@
# no-noninteractive-element-handlers
Non-interactive HTML elements and non-interactive ARIA roles indicate _content_ and _containers_ in the user interface. A non-interactive element does not support event handlers (mouse and key handlers). Non-interactive elements include `<main>`, `<area>`, `<h1>` (,`<h2>`, etc), `<img>`, `<li>`, `<ul>` and `<ol>`. Non-interactive [WAI-ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#usage_intro) include `article`, `banner`, `complementary`, `img`, `listitem`, `main`, `region` and `tooltip`.
## How do I resolve this error?
### Case: This element acts like a button, link, menuitem, etc.
Move the event handler function to an inner element that is either a semantically interactive element (`<button>`, `<a href>`) or that has an interactive role. This leaves the _content_ or _container_ semantic value of this element intact.
Common interactive roles include:
1. `button`
1. `link`
1. `checkbox`
1. `menuitem`
1. `menuitemcheckbox`
1. `menuitemradio`
1. `option`
1. `radio`
1. `searchbox`
1. `switch`
1. `textbox`
Note: Adding a role to your element does **not** add behavior. When a semantic HTML element like `<button>` is used, then it will also respond to Enter key presses when it has focus. The developer is responsible for providing the expected behavior of an element that the role suggests it would have: focusability and key press support.
see [WAI-ARIA Authoring Practices Guide - Design Patterns and Widgets](https://www.w3.org/TR/wai-aria-practices-1.1/#aria_ex).
### Case: This element is catching bubbled events from elements that it contains
Move the event handler function to an inner element like `<div>` and give that element a role of `presentation`. This leaves the _content_ or _container_ semantic value of this element intact.
```
<div role="article">
<div
onClick="onClickHandler"
onKeyPress={onKeyPressHandler}
role="presentation">
{this.props.children}
</div>
</div>
```
Marking an element with the role `presentation` indicates to assistive technology that this element should be ignored; it exists to support the web application and is not meant for humans to interact with directly.
### Case: This is a heading that expands/collapses content on the package
Headers often double as expand/collapse controls for the content they headline. An accordion component is a common example of this pattern. Rather than assign the interaction handling code to the heading itself, put a button inside the heading instead. This pattern retains the role of the heading and the role of the button.
```jsx
<h3>
<button onClick={this._expandSection}>News</button>
</h3>
<ul id="articles-list">
<li>...</li>
</ul>
```
### Case: This element is a table cell
Table cells (and tables in general) are meant to contain data. ARIA provides us with a construct called a [Grid](http://w3c.github.io/aria-practices/#grid) that is essentially a 2 dimensional logical container for content and interactive elements.
You have two options in this case.
#### Option 1, move the interactive content inside the table cells
For instance, move the button inside the cell:
```
<table>
<tr>
<td><button>Sort</button></td>
</tr>
</table>
```
This preserves the table cell semantics and the button semantics; the two are not conflated on the cell.
#### Option 2, convert the table into an ARIA grid
If you're user interface has a table-like layout, but is filled with interactive components in the cells, consider converting the table into a grid.
```
<table role="grid">
<tr>
<td role="gridcell" onClick={this.sort}>Sort</td>
</tr>
</table>
```
You can also put the interactive content inside the grid cell. This maintains the semantic distinction between the cell and the interaction content, although a grid cell can be interactive.
### References
1. [WAI-ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#usage_intro)
1. [WAI-ARIA Authoring Practices Guide - Design Patterns and Widgets](https://www.w3.org/TR/wai-aria-practices-1.1/#aria_ex)
1. [Fundamental Keyboard Navigation Conventions](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav)
1. [Mozilla Developer Network - ARIA Techniques](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Keyboard_and_focus)
## Rule details
You may configure which handler props should be taken into account when applying this rule. The recommended configuration includes the following 6 handlers.
```javascript
'jsx-a11y/no-noninteractive-element-interactions': [
'error',
{
handlers: [
'onClick',
'onMouseDown',
'onMouseUp',
'onKeyPress',
'onKeyDown',
'onKeyUp',
],
},
],
```
Adjust the list of handler prop names in the handlers array to increase or decrease the coverage surface of this rule in your codebase.
### Succeed
```jsx
<div onClick={() => void 0} role="button" />
<div onClick={() => void 0} role="presentation" />
<input type="text" onClick={() => void 0} /> // Interactive element does not require role.
<button onClick={() => void 0} className="foo" /> // button is interactive.
<div onClick={() => void 0} role="button" aria-hidden /> // This is hidden from screenreader.
<Input onClick={() => void 0} type="hidden" /> // This is a higher-level DOM component
```
### Fail
```jsx
<li onClick={() => void 0} />
<div onClick={() => void 0} role="listitem" />
```

View File

@@ -0,0 +1,68 @@
# no-noninteractive-element-to-interactive-role
Non-interactive HTML elements indicate _content_ and _containers_ in the user interface. Non-interactive elements include `<main>`, `<area>`, `<h1>` (,`<h2>`, etc), `<img>`, `<li>`, `<ul>` and `<ol>`.
Interactive HTML elements indicate _controls_ in the user interface. Interactive elements include `<a href>`, `<button>`, `<input>`, `<select>`, `<textarea>`.
[WAI-ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#usage_intro) should not be used to convert a non-interactive element to an interactive element. Interactive ARIA roles include `button`, `link`, `checkbox`, `menuitem`, `menuitemcheckbox`, `menuitemradio`, `option`, `radio`, `searchbox`, `switch` and `textbox`.
## How do I resolve this error?
### Case: This element should be a control, like a button
Put the control inside the non-interactive container element.
```
<li>
<div
role="button"
onClick={() => {}}
onKeyPress={() => {}}>
Save
</div>
</li>
```
Or wrap the content inside your interactive element.
```
<div
role="button"
onClick={() => {}}
onKeyPress={() => {}}
tabIndex="0">
<img src="some/file.png" alt="Save" />
</div>
```
### References
1. [WAI-ARIA roles](https://www.w3.org/TR/wai-aria-1.1/#usage_intro)
1. [WAI-ARIA Authoring Practices Guide - Design Patterns and Widgets](https://www.w3.org/TR/wai-aria-practices-1.1/#aria_ex)
1. [Fundamental Keyboard Navigation Conventions](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav)
1. [Mozilla Developer Network - ARIA Techniques](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Keyboard_and_focus)
## Rule details
The recommended options for this rule allow several common interactive roles to be applied to a non-interactive element. The options are provided as an object keyed by HTML element name; the value is an array of interactive roles that are allowed on the specified element.
```
{
'no-noninteractive-element-to-interactive-role': [
'error',
{
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],
},
}
```
Under the recommended options, the following code is valid. It would be invalid under the strict rules.
```
<ul role="menu" />
```

View File

@@ -0,0 +1,87 @@
# no-noninteractive-tabindex
Tab key navigation should be limited to elements on the page that can be interacted with. Thus it is not necessary to add a tabindex to items in an unordered list, for example, to make them navigable through assistive technology. These applications already afford page traversal mechanisms based on the HTML of the page. Generally, we should try to reduce the size of the page's tab ring rather than increasing it.
## How do I resolve this error?
### Case: I am using an `<a>` tag. Isn't that interactive?
The `<a>` tag is tricky. Consider the following:
```
<a>Edit</a>
<a href="#">Edit</a>
<a role="button">Edit</a>
```
The bare `<a>` tag is an _anchor_. It has no semantic AX API mapping in either ARIA or the AXObject model. It's as meaningful as `<div>`, which is to say it has no meaning. An `<a>` tag with an `href` attribute has an inherent role of `link`. An `<a>` tag with an explicit role obtains the designated role. In the example above, this role is `button`.
### Case: I am using "semantic" HTML. Isn't that interactive?
If we take a step back into the field of linguistics for a moment, let's consider what it means for something to be "semantic". Nothing, in and of itself, has meaning. Meaning is constructed through dialogue. A speaker intends a meaning and a listener/observer interprets a meaning. Each participant constructs their own meaning through dialogue. There is no intrinsic or isolated meaning outside of interaction. Thus, we must ask, given that we have a "speaker" who communicates via "semantic" HTML, who is listening/observing?
In our case, the observer is the Accessibility (AX) API. Browsers interpret HTML (inflected at times by ARIA) to construct a meaning (AX Tree) of the page. Whatever the semantic HTML intends has only the force of suggestion to the AX API. Therefore, we have inconsistencies. For example, there is not yet an ARIA role for `text` or `label` and thus no way to change a `<label>` into plain text or a `<span>` into a label via ARIA. '<div>' has an AXObject correpondant `DivRole`, but no such object maps to `<span>`.
What this lint rule endeavors to do is apply the AX API understanding of the semantics of an HTML document back onto your code. The concept of interactivity boils down to whether a user can do something with the indicated or focused component.
Common interactive roles include:
1. `button`
1. `link`
1. `checkbox`
1. `menuitem`
1. `menuitemcheckbox`
1. `menuitemradio`
1. `option`
1. `radio`
1. `searchbox`
1. `switch`
1. `textbox`
Endeavor to limit tabbable elements to those that a user can act upon.
### Case: Shouldn't I add a tabindex so that users can navigate to this item?
It is not necessary to put a tabindex on an `<article>`, for instance or on `<li>` items; assistive technologies provide affordances to users to find and traverse these containers. Most elements that require a tabindex -- `<a href>`, `<button>`, `<input>`, `<textarea>` -- have it already.
Your application might require an exception to this rule in the case of an element that captures incoming tab traversal for a composite widget. In that case, turn off this rule on a per instance basis. This is an uncommon case.
### References
1. [Fundamental Keyboard Navigation Conventions](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav)
## Rule details
The recommended options for this rule allow `tabIndex` on elements with the noninteractive `tabpanel` role. Adding `tabIndex` to a tabpanel is a recommended practice in some instances.
```javascript
'jsx-a11y/no-noninteractive-tabindex': [
'error',
{
tags: [],
roles: ['tabpanel'],
},
]
```
### Succeed
```jsx
<div />
<MyButton tabIndex={0} />
<button />
<button tabIndex="0" />
<button tabIndex={0} />
<div />
<div tabIndex="-1" />
<div role="button" tabIndex="0" />
<div role="article" tabIndex="-1" />
<article tabIndex="-1" />
```
### Fail
```jsx
<div tabIndex="0" />
<div role="article" tabIndex="0" />
<article tabIndex="0" />
<article tabIndex={0} />
```

View File

@@ -0,0 +1,27 @@
# no-onchange
Enforce usage of `onBlur` over/in parallel with `onChange` on select menu elements for accessibility. `onBlur` **should** be used instead of `onChange`, unless absolutely necessary and it causes no negative consequences for keyboard only or screen reader users. `onBlur` is a more declarative action by the user: for instance in a dropdown, using the arrow keys to toggle between options will trigger the `onChange` event in some browsers. Regardless, when a change of context results from an `onBlur` event or an `onChange` event, the user should be notified of the change unless it occurs below the currently focused element.
#### References
1. [onChange Event Accessibility Issues](http://cita.disability.uiuc.edu/html-best-practices/auto/onchange.php)
2. [onChange Select Menu](http://www.themaninblue.com/writing/perspective/2004/10/19/)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<select onBlur={updateModel}>
<option/>
</select>
<select>
<option onBlur={handleOnBlur} onChange={handleOnChange} />
</select>
```
### Fail
```jsx
<select onChange={updateModel} />
```

View File

@@ -0,0 +1,23 @@
# no-redundant-roles
Some HTML elements have native semantics that are implemented by the browser. This includes default/implicit ARIA roles. Setting an ARIA role that matches its default/implicit role is redundant since it is already set by the browser.
#### References
1. [w3](https://www.w3.org/TR/html5/dom.html#aria-role-attribute)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<div />
<button role="presentation" />
<MyComponent role="main" />
```
### Fail
```jsx
<button role="button" />
<img role="img" src="foo.jpg" />
```

View File

@@ -0,0 +1,93 @@
# no-static-element-interactions
Static HTML elements do not have semantic meaning. This is clear in the case of `<div>` and `<span>`. It is less so clear in the case of elements that _seem_ semantic, but that do not have a semantic mapping in the accessibility layer. For example `<a>`, `<big>`, `<blockquote>`, `<footer>`, `<picture>`, `<strike>` and `<time>` -- to name a few -- have no semantic layer mapping. They are as void of meaning as `<div>`.
The [WAI-ARIA `role` attribute](https://www.w3.org/TR/wai-aria-1.1/#usage_intro) confers a semantic mapping to an element. The semantic value can then be expressed to a user via assistive technology.
In order to add interactivity such as a mouse or key event listener to a static element, that element must be given a role value as well.
## How do I resolve this error?
### Case: This element acts like a button, link, menuitem, etc.
Indicate the element's role with the `role` attribute:
```
<div
onClick={onClickHandler}
onKeyPress={onKeyPressHandler}
role="button"
tabIndex="0">
Save
</div>
```
Common interactive roles include:
1. `button`
1. `link`
1. `checkbox`
1. `menuitem`
1. `menuitemcheckbox`
1. `menuitemradio`
1. `option`
1. `radio`
1. `searchbox`
1. `switch`
1. `textbox`
Note: Adding a role to your element does **not** add behavior. When a semantic HTML element like `<button>` is used, then it will also respond to Enter key presses when it has focus. The developer is responsible for providing the expected behavior of an element that the role suggests it would have: focusability and key press support.
### Case: This element is not a button, link, menuitem, etc. It is catching bubbled events from elements that it contains
If your element is catching bubbled click or key events from descendant elements, then the proper role for this element is `presentation`.
```
<div
onClick="onClickHandler"
role="presentation">
<button>Save</button>
</div>
```
Marking an element with the role `presentation` indicates to assistive technology that this element should be ignored; it exists to support the web application and is not meant for humans to interact with directly.
### References
1. [WAI-ARIA `role` attribute](https://www.w3.org/TR/wai-aria-1.1/#usage_intro)
1. [WAI-ARIA Authoring Practices Guide - Design Patterns and Widgets](https://www.w3.org/TR/wai-aria-practices-1.1/#aria_ex)
1. [Fundamental Keyboard Navigation Conventions](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_generalnav)
1. [Mozilla Developer Network - ARIA Techniques](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role#Keyboard_and_focus)
## Rule details
You may configure which handler props should be taken into account when applying this rule. The recommended configuration includes the following 6 handlers.
```javascript
'jsx-a11y/no-static-element-interactions': [
'error',
{
handlers: [
'onClick',
'onMouseDown',
'onMouseUp',
'onKeyPress',
'onKeyDown',
'onKeyUp',
],
},
],
```
Adjust the list of handler prop names in the handlers array to increase or decrease the coverage surface of this rule in your codebase.
### Succeed
```jsx
<button onClick={() => {}} className="foo" />
<div className="foo" onClick={() => {}} role="button" />
<input type="text" onClick={() => {}} />
```
### Fail
```jsx
<div onClick={() => {}} />
```

View File

@@ -0,0 +1,24 @@
# role-has-required-aria-props
Elements with ARIA roles must have all required attributes for that role.
#### References
1. [Spec](https://www.w3.org/TR/wai-aria/roles)
2. [AX_ARIA_03](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_03)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<!-- Good: the checkbox role requires the aria-checked state -->
<span role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0"></span>
```
### Fail
```jsx
<!-- Bad: the checkbox role requires the aria-checked state -->
<span role="checkbox" aria-labelledby="foo" tabindex="0"></span>
```

View File

@@ -0,0 +1,32 @@
# role-supports-aria-props
Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`. Many ARIA attributes (states and properties) can only be used on elements with particular roles. Some elements have implicit roles, such as `<a href="#" />`, which will resolve to `role="link"`.
#### References
1. [AX_ARIA_10](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_10)
2. [Supported States & Properties](https://www.w3.org/TR/wai-aria/roles#supportedState)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<!-- Good: the radiogroup role does support the aria-required property -->
<ul role="radiogroup" aria-required aria-labelledby="foo">
<li tabIndex="-1" role="radio" aria-checked="false">Rainbow Trout</li>
<li tabIndex="-1" role="radio" aria-checked="false">Brook Trout</li>
<li tabIndex="0" role="radio" aria-checked="true">Lake Trout</li>
</ul>
```
### Fail
```jsx
<!-- Bad: the radio role does not support the aria-required property -->
<ul role="radiogroup" aria-labelledby="foo">
<li aria-required tabIndex="-1" role="radio" aria-checked="false">Rainbow Trout</li>
<li aria-required tabIndex="-1" role="radio" aria-checked="false">Brook Trout</li>
<li aria-required tabIndex="0" role="radio" aria-checked="true">Lake Trout</li>
</ul>
```

View File

@@ -0,0 +1,22 @@
# scope
The `scope` scope should be used only on `<th>` elements.
#### References
1. [Deque University](https://dequeuniversity.com/rules/axe/1.1/scope)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<th scope="col" />
<th scope={scope} />
```
### Fail
```jsx
<div scope />
```

View File

@@ -0,0 +1,26 @@
# tabindex-no-positive
Avoid positive tabIndex property values to synchronize the flow of the page with keyboard tab order.
#### References
1. [AX_FOCUS_03](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_focus_03)
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<span tabIndex="0">foo</span>
<span tabIndex="-1">bar</span>
<span tabIndex={0}>baz</span>
```
### Fail
```jsx
<span tabIndex="5">foo</span>
<span tabIndex="3">bar</span>
<span tabIndex="1">baz</span>
<span tabIndex="2">never really sure what goes after baz</span>
```

View File

@@ -0,0 +1,11 @@
/*
* @flow
*/
import type {
JSXAttribute,
JSXOpeningElement,
} from 'ast-types-flow';
export type ESLintJSXAttribute = {
parent: JSXOpeningElement
} & JSXAttribute;

View File

@@ -0,0 +1,12 @@
/*
* @flow
*/
export type ESLintReport = {
node: any,
message: string,
};
export type ESLintContext = {
options: Array<Object>,
report: (ESLintReport) => void,
};

View File

@@ -0,0 +1,147 @@
'use strict';
/* eslint-disable global-require */
module.exports = {
rules: {
'accessible-emoji': require('./rules/accessible-emoji'),
'alt-text': require('./rules/alt-text'),
'anchor-has-content': require('./rules/anchor-has-content'),
'anchor-is-valid': require('./rules/anchor-is-valid'),
'aria-activedescendant-has-tabindex': require('./rules/aria-activedescendant-has-tabindex'),
'aria-props': require('./rules/aria-props'),
'aria-proptypes': require('./rules/aria-proptypes'),
'aria-role': require('./rules/aria-role'),
'aria-unsupported-elements': require('./rules/aria-unsupported-elements'),
'click-events-have-key-events': require('./rules/click-events-have-key-events'),
'heading-has-content': require('./rules/heading-has-content'),
'href-no-hash': require('./rules/href-no-hash'),
'html-has-lang': require('./rules/html-has-lang'),
'iframe-has-title': require('./rules/iframe-has-title'),
'img-redundant-alt': require('./rules/img-redundant-alt'),
'interactive-supports-focus': require('./rules/interactive-supports-focus'),
'label-has-for': require('./rules/label-has-for'),
lang: require('./rules/lang'),
'media-has-caption': require('./rules/media-has-caption'),
'mouse-events-have-key-events': require('./rules/mouse-events-have-key-events'),
'no-access-key': require('./rules/no-access-key'),
'no-autofocus': require('./rules/no-autofocus'),
'no-distracting-elements': require('./rules/no-distracting-elements'),
'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'),
'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'),
'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'),
'no-noninteractive-tabindex': require('./rules/no-noninteractive-tabindex'),
'no-onchange': require('./rules/no-onchange'),
'no-redundant-roles': require('./rules/no-redundant-roles'),
'no-static-element-interactions': require('./rules/no-static-element-interactions'),
'role-has-required-aria-props': require('./rules/role-has-required-aria-props'),
'role-supports-aria-props': require('./rules/role-supports-aria-props'),
scope: require('./rules/scope'),
'tabindex-no-positive': require('./rules/tabindex-no-positive')
},
configs: {
recommended: {
parserOptions: {
ecmaFeatures: {
jsx: true
}
},
rules: {
'jsx-a11y/accessible-emoji': 'error',
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
'jsx-a11y/aria-props': 'error',
'jsx-a11y/aria-proptypes': 'error',
'jsx-a11y/aria-role': 'error',
'jsx-a11y/aria-unsupported-elements': 'error',
'jsx-a11y/click-events-have-key-events': 'error',
'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/href-no-hash': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/iframe-has-title': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/interactive-supports-focus': ['error', {
tabbable: ['button', 'checkbox', 'link', 'searchbox', 'spinbutton', 'switch', 'textbox']
}],
'jsx-a11y/label-has-for': 'error',
'jsx-a11y/media-has-caption': 'error',
'jsx-a11y/mouse-events-have-key-events': 'error',
'jsx-a11y/no-access-key': 'error',
'jsx-a11y/no-autofocus': 'error',
'jsx-a11y/no-distracting-elements': 'error',
'jsx-a11y/no-interactive-element-to-noninteractive-role': ['error', {
tr: ['none', 'presentation']
}],
'jsx-a11y/no-noninteractive-element-interactions': ['error', {
handlers: ['onClick', 'onMouseDown', 'onMouseUp', 'onKeyPress', 'onKeyDown', 'onKeyUp']
}],
'jsx-a11y/no-noninteractive-element-to-interactive-role': ['error', {
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell']
}],
'jsx-a11y/no-noninteractive-tabindex': ['error', {
tags: [],
roles: ['tabpanel']
}],
'jsx-a11y/no-onchange': 'error',
'jsx-a11y/no-redundant-roles': 'error',
'jsx-a11y/no-static-element-interactions': ['error', {
handlers: ['onClick', 'onMouseDown', 'onMouseUp', 'onKeyPress', 'onKeyDown', 'onKeyUp']
}],
'jsx-a11y/role-has-required-aria-props': 'error',
'jsx-a11y/role-supports-aria-props': 'error',
'jsx-a11y/scope': 'error',
'jsx-a11y/tabindex-no-positive': 'error'
}
},
strict: {
parserOptions: {
ecmaFeatures: {
jsx: true
}
},
rules: {
'jsx-a11y/accessible-emoji': 'error',
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/anchor-is-valid': 'error',
'jsx-a11y/aria-activedescendant-has-tabindex': 'error',
'jsx-a11y/aria-props': 'error',
'jsx-a11y/aria-proptypes': 'error',
'jsx-a11y/aria-role': 'error',
'jsx-a11y/aria-unsupported-elements': 'error',
'jsx-a11y/click-events-have-key-events': 'error',
'jsx-a11y/heading-has-content': 'error',
'jsx-a11y/href-no-hash': 'error',
'jsx-a11y/html-has-lang': 'error',
'jsx-a11y/iframe-has-title': 'error',
'jsx-a11y/img-redundant-alt': 'error',
'jsx-a11y/interactive-supports-focus': ['error', {
tabbable: ['button', 'checkbox', 'link', 'progressbar', 'searchbox', 'slider', 'spinbutton', 'switch', 'textbox']
}],
'jsx-a11y/label-has-for': 'error',
'jsx-a11y/media-has-caption': 'error',
'jsx-a11y/mouse-events-have-key-events': 'error',
'jsx-a11y/no-access-key': 'error',
'jsx-a11y/no-autofocus': 'error',
'jsx-a11y/no-distracting-elements': 'error',
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
'jsx-a11y/no-noninteractive-element-interactions': 'error',
'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error',
'jsx-a11y/no-noninteractive-tabindex': 'error',
'jsx-a11y/no-onchange': 'error',
'jsx-a11y/no-redundant-roles': 'error',
'jsx-a11y/no-static-element-interactions': 'error',
'jsx-a11y/role-has-required-aria-props': 'error',
'jsx-a11y/role-supports-aria-props': 'error',
'jsx-a11y/scope': 'error',
'jsx-a11y/tabindex-no-positive': 'error'
}
}
}
};

View File

@@ -0,0 +1,54 @@
'use strict';
var _emojiRegex = require('emoji-regex');
var _emojiRegex2 = _interopRequireDefault(_emojiRegex);
var _jsxAstUtils = require('jsx-ast-utils');
var _schemas = require('../util/schemas');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var errorMessage = 'Emojis should be wrapped in <span>, have role="img", and have an accessible description with aria-label or aria-labelledby.'; /**
* @fileoverview Enforce emojis are wrapped in <span> and provide screenreader access.
* @author Ethan Cohen
*/
// ----------------------------------------------------------------------------
// Rule Definition
// ----------------------------------------------------------------------------
var schema = (0, _schemas.generateObjSchema)();
module.exports = {
meta: {
docs: {},
schema: [schema]
},
create: function create(context) {
return {
JSXOpeningElement: function JSXOpeningElement(node) {
var literalChildValue = node.parent.children.find(function (child) {
return child.type === 'Literal';
});
if (literalChildValue && (0, _emojiRegex2.default)().test(literalChildValue.value)) {
var rolePropValue = (0, _jsxAstUtils.getLiteralPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'role'));
var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
var hasLabel = ariaLabelProp !== undefined || arialLabelledByProp !== undefined;
var isSpan = (0, _jsxAstUtils.elementType)(node) === 'span';
if (hasLabel === false || rolePropValue !== 'img' || isSpan === false) {
context.report({
node: node,
message: errorMessage
});
}
}
}
};
}
};

View File

@@ -0,0 +1,209 @@
'use strict';
var _jsxAstUtils = require('jsx-ast-utils');
var _schemas = require('../util/schemas');
var _hasAccessibleChild = require('../util/hasAccessibleChild');
var _hasAccessibleChild2 = _interopRequireDefault(_hasAccessibleChild);
var _isPresentationRole = require('../util/isPresentationRole');
var _isPresentationRole2 = _interopRequireDefault(_isPresentationRole);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } /**
* @fileoverview Enforce all elements that require alternative text have it.
* @author Ethan Cohen
*/
// ----------------------------------------------------------------------------
// Rule Definition
// ----------------------------------------------------------------------------
var DEFAULT_ELEMENTS = ['img', 'object', 'area', 'input[type="image"]'];
var schema = (0, _schemas.generateObjSchema)({
elements: _schemas.arraySchema,
img: _schemas.arraySchema,
object: _schemas.arraySchema,
area: _schemas.arraySchema,
'input[type="image"]': _schemas.arraySchema
});
var ruleByElement = {
img: function img(context, node) {
var nodeType = (0, _jsxAstUtils.elementType)(node);
var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt');
// Missing alt prop error.
if (altProp === undefined) {
if ((0, _isPresentationRole2.default)(nodeType, node.attributes)) {
context.report({
node: node,
message: 'Prefer alt="" over a presentational role. First rule of aria is to not use aria if it can be achieved via native HTML.'
});
return;
}
context.report({
node: node,
message: nodeType + ' elements must have an alt prop, either with meaningful text, or an empty string for decorative images.'
});
return;
}
// Check if alt prop is undefined.
var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
var isNullValued = altProp.value === null; // <img alt />
if (altValue && !isNullValued || altValue === '') {
return;
}
// Undefined alt prop error.
context.report({
node: node,
message: 'Invalid alt value for ' + nodeType + '. Use alt="" for presentational images.'
});
},
object: function object(context, node) {
var ariaLabelProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-label');
var arialLabelledByProp = (0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby');
var hasLabel = ariaLabelProp !== undefined || arialLabelledByProp !== undefined;
var titleProp = (0, _jsxAstUtils.getLiteralPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'title'));
var hasTitleAttr = !!titleProp;
if (hasLabel || hasTitleAttr || (0, _hasAccessibleChild2.default)(node.parent)) {
return;
}
context.report({
node: node,
message: 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.'
});
},
area: function area(context, node) {
var ariaLabelPropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'aria-label'));
var arialLabelledByPropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby'));
var hasLabel = ariaLabelPropValue !== undefined || arialLabelledByPropValue !== undefined;
if (hasLabel) {
return;
}
var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt');
if (altProp === undefined) {
context.report({
node: node,
message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
});
return;
}
var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
var isNullValued = altProp.value === null; // <area alt />
if (altValue && !isNullValued || altValue === '') {
return;
}
context.report({
node: node,
message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
});
},
'input[type="image"]': function inputImage(context, node) {
// Only test input[type="image"]
var nodeType = (0, _jsxAstUtils.elementType)(node);
if (nodeType === 'input') {
var typePropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'type'));
if (typePropValue !== 'image') {
return;
}
}
var ariaLabelPropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'aria-label'));
var arialLabelledByPropValue = (0, _jsxAstUtils.getPropValue)((0, _jsxAstUtils.getProp)(node.attributes, 'aria-labelledby'));
var hasLabel = ariaLabelPropValue !== undefined || arialLabelledByPropValue !== undefined;
if (hasLabel) {
return;
}
var altProp = (0, _jsxAstUtils.getProp)(node.attributes, 'alt');
if (altProp === undefined) {
context.report({
node: node,
message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
});
return;
}
var altValue = (0, _jsxAstUtils.getPropValue)(altProp);
var isNullValued = altProp.value === null; // <area alt />
if (altValue && !isNullValued || altValue === '') {
return;
}
context.report({
node: node,
message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.'
});
}
};
module.exports = {
meta: {
docs: {},
schema: [schema]
},
create: function create(context) {
var _ref;
var options = context.options[0] || {};
// Elements to validate for alt text.
var elementOptions = options.elements || DEFAULT_ELEMENTS;
// Get custom components for just the elements that will be tested.
var customComponents = elementOptions.map(function (element) {
return options[element];
}).reduce(function (components, customComponentsForElement) {
return components.concat(customComponentsForElement || []);
}, []);
var typesToValidate = new Set((_ref = []).concat.apply(_ref, [customComponents].concat(_toConsumableArray(elementOptions))).map(function (type) {
if (type === 'input[type="image"]') {
return 'input';
}
return type;
}));
return {
JSXOpeningElement: function JSXOpeningElement(node) {
var nodeType = (0, _jsxAstUtils.elementType)(node);
if (!typesToValidate.has(nodeType)) {
return;
}
var DOMElement = nodeType;
if (DOMElement === 'input') {
DOMElement = 'input[type="image"]';
}
// Map nodeType to the DOM element if we are running this on a custom component.
if (elementOptions.indexOf(DOMElement) === -1) {
DOMElement = elementOptions.find(function (element) {
var customComponentsForElement = options[element] || [];
return customComponentsForElement.indexOf(nodeType) > -1;
});
}
ruleByElement[DOMElement](context, node);
}
};
}
};

View File

@@ -0,0 +1,52 @@
'use strict';
var _jsxAstUtils = require('jsx-ast-utils');
var _schemas = require('../util/schemas');
var _hasAccessibleChild = require('../util/hasAccessibleChild');
var _hasAccessibleChild2 = _interopRequireDefault(_hasAccessibleChild);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var errorMessage = 'Anchors must have content and the content must be accessible by a screen reader.'; /**
* @fileoverview Enforce anchor elements to contain accessible content.
* @author Lisa Ring & Niklas Holmberg
*/
// ----------------------------------------------------------------------------
// Rule Definition
// ----------------------------------------------------------------------------
var schema = (0, _schemas.generateObjSchema)({ components: _schemas.arraySchema });
module.exports = {
meta: {
docs: {},
schema: [schema]
},
create: function create(context) {
return {
JSXOpeningElement: function JSXOpeningElement(node) {
var options = context.options[0] || {};
var componentOptions = options.components || [];
var typeCheck = ['a'].concat(componentOptions);
var nodeType = (0, _jsxAstUtils.elementType)(node);
// Only check anchor elements and custom types.
if (typeCheck.indexOf(nodeType) === -1) {
return;
} else if ((0, _hasAccessibleChild2.default)(node.parent)) {
return;
}
context.report({
node: node,
message: errorMessage
});
}
};
}
};

Some files were not shown because too many files have changed in this diff Show More