main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

17
node_modules/eslint-plugin-jsx-a11y/.babelrc generated vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"presets": [
[
"airbnb",
{
"targets": {
"node": 4
},
"transformRuntime": false
}
]
],
"plugins": [
"@babel/plugin-transform-flow-strip-types",
"add-module-exports"
]
}

44
node_modules/eslint-plugin-jsx-a11y/.eslintrc generated vendored Normal file
View File

@@ -0,0 +1,44 @@
{
"root": true,
"extends": [
"airbnb-base",
"plugin:flowtype/recommended"
],
"ignorePatterns": [
"lib/",
"reports/",
"examples/",
],
"parser": "@babel/eslint-parser",
"plugins": [
"flowtype",
],
"rules": {
"max-len": "off",
"no-template-curly-in-string": "off",
},
"overrides": [
{
"files": ["src/rules/*"],
"extends": ["plugin:eslint-plugin/rules-recommended"],
"rules": {
"eslint-plugin/require-meta-docs-description": ["error", { "pattern": "^(Enforce|Require|Disallow)" }],
"eslint-plugin/require-meta-docs-url": [
"error",
{ "pattern": "https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/docs/rules/{{name}}.md" },
],
"eslint-plugin/require-meta-type": "off",
},
},
{
"files": ["__tests__/src/rules/*.js"],
"extends": ["plugin:eslint-plugin/tests-recommended"],
},
{
"files": ["__tests__/**/*"],
"env": {
"jest": true,
},
},
],
}

745
node_modules/eslint-plugin-jsx-a11y/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,745 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v6.10.0](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.9.0...v6.10.0) - 2024-09-03
### Fixed
- [New] `label-has-associated-control`: add additional error message [`#1005`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/1005)
- [Fix] `label-has-associated-control`: ignore undetermined label text [`#966`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/966)
### Commits
- [Tests] switch from jest to tape [`a284cbf`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/a284cbf4eb21292c4cff87f02be0bfb82764757f)
- [New] add eslint 9 support [`deac4fd`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/deac4fd06eff4c0f5da27611c2a44a009b7e7fda)
- [New] add `attributes` setting [`a1ee7f8`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/a1ee7f8810efafe416eb5d7f6eb0505b52873495)
- [New] allow polymorphic linting to be restricted [`6cd1a70`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/6cd1a7011446e3925f2b49c51ff26246a21491d1)
- [Tests] remove duplicate tests [`74d5dec`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/74d5decb6f2e42c05ce40a45630041fd695a2e7f)
- [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types` [`6eca235`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/6eca2359f5457af72dbfba265b73297c9232cb3e)
- [readme] remove deprecated travis ci badge; add github actions badge [`0be7ea9`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/0be7ea95f560c6afc6817d381054d914ebd0b2ca)
- [Tests] use `npm audit` instead of `aud` [`05a5e49`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/05a5e4992900e0d5d61e29e13046c90797b68a7c)
- [Deps] update `axobject-query` [`912e98c`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/912e98c425ef9fcc2d7d22b45b4f7e3b445112a5)
- [Deps] unpin `axobject-query` [`75147aa`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/75147aa68888fc150a4efea5b99809969bdc32b2)
- [Deps] update `axe-core` [`27ff7cb`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/27ff7cbf562bf2685fd5a6062e58eb4727cb85c6)
- [readme] fix jsxA11y import name [`ce846e0`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/ce846e00414c41676a6a8601022059878bcc0b89)
- [readme] fix typo in shareable config section in readme [`cca288b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/cca288b73a39fa0932a57c02a7a88de68fc971fc)
## [v6.9.0](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.8.0...v6.9.0) - 2024-06-19
### Fixed
- [Fix] `img-redundant-alt`: fixed multibyte character support [`#969`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/969)
- [meta] fix changelog links [`#960`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/960)
### Commits
- [New] add support for Flat Config [`6b5f096`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/6b5f096f10b47326d68e2893152a48a79c8555b4)
- Revert "[Fix] `isNonInteractiveElement`: Upgrade aria-query to 5.3.0 and axobject-query to 3.2.1" [`75d5dd7`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/75d5dd722bd67186d97afa7b151fd6fee5885c70)
- [Robustness] use `safe-regex-test` [`4c7e781`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/4c7e7815c12a797587bb8e3cdced7f3003848964)
- [actions] update actions/checkout [`51a1ca7`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/51a1ca7b4d83d4fbd1ea62888f7f2dc21ece6788)
- [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `@babel/register`, `eslint-doc-generator`, `object.entries` [`1271ac1`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/1271ac1d6e5dcf9a2bc2c086faaf062335629171)
- [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/register`, `aud`, `eslint-plugin-import`, `npmignore`, `object.assign` [`540cb7a`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/540cb7aefead582f237071d55a40f098d0885478)
- [Deps] update `@babel/runtime`, `array-includes`, `es-iterator-helpers`, `hasown`, `object.fromentries`, `safe-regex-test` [`5d14408`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/5d1440825a8838ae10dc94cc3a4a7e1e967644b4)
- [Deps] pin `aria-query` and `axobject-query`, add `ls-engines` test to CI [`32fd82c`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/32fd82c628d7f3e4ec8c06a1994f4eca1be2be4f)
- [Dev Deps] update `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `eslint-doc-generator` [`d1b4114`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/d1b41142248a7cca45bb5f0b96ff23ee87fb9411)
- [Fix] ensure `summary` remains non-interactive [`6a048da`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/6a048dacf2b98eaa204e2a5a70dc7e3d48d9463a)
- [Deps] remove `@babel/runtime` [`0a98ad8`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/0a98ad83ffa7f4b66458cc1c39db2ef32bb2c480)
- [New] `no-noninteractive-element-to-interactive-role`: allow `menuitemradio` and `menuitemcheckbox` on <li> [`c0733f9`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/c0733f94031fe3eec6b4d54176afe47929bb0a84)
- [Deps] update `@babel/runtime`, `safe-regex-test` [`0d5321a`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/0d5321a5457c5f0da0ca216053cc5b4f571b53ae)
- [actions] pin codecov to v3.1.5 [`961817f`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/961817f61fa56cd7815c6940c27ef08469b1516b)
- [Deps] unpin `axe-core` [`b3559cf`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/b3559cf89be6b5352cd77ffa025831b3d793d565)
- [Deps] move `object.entries` to dev deps [`1be7b70`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/1be7b709eececd83f1d5f67a60b2c97cfe9a561d)
- [Deps] update `@babel/runtime` [`2a48abb`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/2a48abb5effa911e7d1a8575e1c9768c947a33f1)
- [Deps] update `@babel/runtime` [`1adec35`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/1adec3517fc2c9797212ca4d38858deed917e7be)
## [v6.8.0](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.7.1...v6.8.0) - 2023-11-01
### Merged
- Allow `title` attribute or `aria-label` attribute instead of accessible child in the "anchor-has-content" rule [`#727`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/pull/727)
### Fixed
- [Docs] `aria-activedescendant-has-tabindex`: align with changes from #708 [`#924`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/924)
- [Fix] `control-has-associated-label`: don't accept whitespace as an accessible label [`#918`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/918)
### Commits
- [Tests] migrate helper parsers function from `eslint-plugin-react` [`ce4d57f`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/ce4d57f853ce7f71bd31edaa524eeb3ff1d27cf1)
- [Refactor] use `es-iterator-helpers` [`52de824`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/52de82403752bb2ccbcac3379925650a0112d4af)
- [New] `mouse-events-have-key-events`: add `hoverInHandlers`/`hoverOutHandlers` config [`db64898`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/db64898fa591f17827053ad3c2ddeafdf7297dd6)
- [New] add `polymorphicPropName` setting for polymorphic components [`fffb05b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/fffb05b38c8eee926ee758e9ceb9eae4e697fbdd)
- [Fix] `isNonInteractiveElement`: Upgrade aria-query to 5.3.0 and axobject-query to 3.2.1 [`64bfea6`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/64bfea6352a704470a760fa6ea25cfc5a50414db)
- [Refactor] use `hasown` instead of `has` [`9a8edde`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/9a8edde7f2e80b7d104dd576f91526c6c4cbebb9)
- [actions] update used actions [`10c061a`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/10c061a70cac067641e3a084d0fb464960544505)
- [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `@babel/register`, `aud`, `eslint-doc-generator`, `eslint-plugin-import`, `minimist` [`6d5022d`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/6d5022d4894fa88d3c15c8b858114e8b2a8a440f)
- [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/register`, `eslint-doc-generator`, `eslint-plugin-import` [`4dc7f1e`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/4dc7f1e5c611aeea2f81dc50d4ec0b206566181a)
- [New] `anchor-has-content`: Allow title attribute OR aria-label attribute [`e6bfd5c`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/e6bfd5cb7c060fcaf54ede85a1be74ebe2f60d1e)
- [patch] `mouse-events-have-key-events`: rport the attribute, not the node [`eadd70c`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/eadd70cb1d0478c24538ee7604cf5493a96c0715)
- [Deps] update `@babel/runtime`, `array-includes`, `array.prototype.flatmap`, `object.entries`, `object.fromentries` [`46ffbc3`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/46ffbc38512be4ed3db2f0fcd7d21af830574f63)
- [Deps] update `@babel/runtime`, `axobject-query`, `jsx-ast-utils`, `semver` [`5999555`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/5999555714f594c0fccfeeab2063c2658d9e4392)
- [Fix] pin `aria-query` and `axe-core` to fix failing tests on main [`8d8f016`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/8d8f0169dbaaa28143cf936cba3046c6e53fa134)
- [patch] move `semver` from Deps to Dev Deps [`4da13e7`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/4da13e79743ad2e1073fc2bb682197e1ba6dbea3)
- [Deps] update `ast-types-flow` [`b755318`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/b755318e675e73a33b1bb7ee809abc88c1927408)
- [Dev Deps] update `eslint-plugin-import` [`f1c976b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/f1c976b6af2d4f5237b481348868a5216e169296)
- [Deps] unpin `language-tags` [`3d1d26d`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/3d1d26d89d492947cbf69f439deec9e7cfaf9867)
- [Docs] `no-static-element-interactions`: tabIndex is written tabindex [`1271153`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/1271153653ada3f8d95b8e39f0164d5b255abea0)
- [Deps] Upgrade ast-types-flow to mitigate Docker user namespacing problems [`f0d2ddb`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/f0d2ddb65f21278ad29be43fb167a1092287b4b1)
- [Dev Deps] pin `jackspeak` since 2.1.2+ depends on npm aliases, which kill the install process in npm < 6 [`0c278f4`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/0c278f4805ec18d8ee4d3e8dfa2f603a28d7e113)
## [v6.7.1](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.7.0...v6.7.1) - 2023-01-11
### Commits
- [Fix] `no-aria-hidden-on-focusable` rule's missing export [`b01219e`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/b01219edc2eb289c7a068b4fa195f2ac04e915fa)
## [v6.7.0](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.6.1...v6.7.0) - 2023-01-09
### Merged
- New rule: prefer-tag-over-role [`#833`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/pull/833)
### Fixed
- [Tests] `aria-role`: add now-passing test [`#756`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/756)
- [Docs] `control-has-associated-label`: fix metadata [`#892`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/892)
- [New] add `no-aria-hidden-on-focusable` rule [`#881`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/881)
### Commits
- [Docs] automate docs with `eslint-doc-generator` [`6d7a857`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/6d7a857eccceb58dabfa244f6a196ad1697c01a4)
- [Refactor] use fromEntries, flatMap, etc; better use iteration methods [`3d77c84`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/3d77c845a98b6fc8cf10c810996278c02e308f35)
- [New] add `anchor-ambiguous-text` rule [`7f6463e`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/7f6463e5cffd1faa5cf22e3b0d33465e22bd10e1)
- [New] add `getAccessibleChildText` util [`630116b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/630116b334e22db853a95cd64e20b7df9f2b6dc8)
- [New] Add `isFocusable` utils method [`e199d17`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/e199d17db0b6bf1d917dab13a9690876ef6f77e3)
- [Docs] update `eslint-doc-generator` to v1.0.0 [`6b9855b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/6b9855b9c3633308004960594327a10bc2551ad2)
- [Fix] `no-noninteractive-element-interactions`: Ignore contenteditable elements in no-noninteractive-element-interactions [`9aa878b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/9aa878bc39769f6c7b31c72bd1140c1370d202f1)
- [New] `anchor-ambiguous-text`: ignore punctuation [`bbae2c4`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/bbae2c46ab4ae94122be6c898f2ef313c6154c27)
- [New] `anchor-ambiguous-text`, `getAccessibleChildText`: Implements check for `alt` tags on `<img />` elements [`bb84abc`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/bb84abc793435a25398160242c5f2870b83b72ca)
- [meta] use `npmignore` to autogenerate an npmignore file [`6ad2312`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/6ad23124582741385df50e98d5ed0d070f86eafe)
- [meta] add `auto-changelog` [`283817b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/283817b82252ef4a6395c22585d8681f97305ca0)
- [Docs] missing descriptions in some rules [`79b975a`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/79b975ab7185cc4fbf6a3adea45c78fac2162d77)
- [Deps] update `aria-query`, `axobject-query` [`7b3cda3`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/7b3cda3854451affe20b2e4f2dd57cf317dd7d1b)
- [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `aud`, `object.assign` [`0852947`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/0852947cfd57a34353a97c67f6de28dbcc8be0e3)
- [meta] move `.eslintignore` to `ignorePatterns` [`65be35b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/65be35b0f6c6cf8b79e9a748cb657a64b78c6535)
- [Dev Deps] update `@babel/cli`, `@babel/core`, `aud`, `eslint-doc-generator` [`60c2df5`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/60c2df5388a3f841a7780eafe1a0fbb44056743d)
- [Deps] update `@babel/runtime`, `array-includes`, `axe-core` [`4abc751`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/4abc751d87a8491219a9a3d2dacd80ea8adcb79b)
- [Deps] update `@babel/runtime`, `axe-core` [`89f766c`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/89f766cd40fd32ada2020856b251ad6e34a6f365)
- [meta] run the build in prepack, not prepublish [`e411ce3`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/e411ce35cfa58181d375544ba5204c35db83678c)
- [Dev Deps] update `@babel/core`, `minimist` [`cccdb62`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/cccdb625d6237538fb4443349870293e8df818eb)
- [Dev Deps] update `markdown-magic` [`3382059`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/3382059feb5367c79e049943772e3a6e27e77609)
- [Fix] expose `prefer-tag-over-role` [`38d52f8`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/38d52f856a18d444e6db7d16d373e0d18c5b287d)
- [Docs] `label-has-for`: reran generate-list-of-rules [`9a2af01`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/9a2af0172cefad7fdce869401b2df42536812152)
- [Deps] pin `language-tags` to `v1.0.5` [`f84bb74`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/f84bb746857cfbc075f8e7104b3a16dddb66be7c)
- [Dev Deps] update `@babel/core` [`cf3f8d0`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/cf3f8d0a6bde6dc5ad39a96a6ed1912c1ad80e89)
- [Deps] update `axe-core` [`0a73cf4`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/0a73cf4ad0adca0bef0a383a10a14597acef5713)
- [Deps] update `@babel/runtime` [`053f04d`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/053f04da8b60d259e4c92f214ffba07a14f3ec61)
- [Deps] update `@babel/runtime` [`bccf0ae`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/bccf0aeab8dd337c5f134f892a6d3588fbc29bdf)
- [Deps] update `jsx-ast-utils` [`c9687cc`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/c9687cc2a1b7f5f72c8181a9fd6a47f49c373240)
- [readme] Preventing code repetition in user's eslint config file [`8b889bf`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/8b889bff2731c9db6988c88c0d76bdbff17bd3c5)
- [Docs] `prefer-tag-over-role`: rename docs file [`0bdf95b`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/commit/0bdf95b41cce32c8b7916367e7c8c663411d881c)
<!-- auto-changelog-above -->
6.6.1 / 2022-07-21
==================
- 38405ad [Fix] `no-interactive-tabindex`: allow role assignments using a ternary with literals on both sides
- 7524e0c [Fix] `no-static-element-interactions`: allow role assignments using a ternary with literals on both sides (#865)
- 1c06306 [readme] properly describe rule settings in builtin configs
- 0c19f02 [Docs] `no-noninteractive-tabindex`, `no-static-element-interactions`: document `allowExpressionValues` (#870)
- 2362832 [readme] added link to redirect eslint to relevant docs (#862)
- 2c6926c [Deps] unpin `axe-core`
- b78f19d [Deps] pin `axe-core` to v4.4.1, due to a breaking change in a patch
- 768910e [Deps] update `@babel/runtime`
- f0e04ce [Deps] update `@babel/runtime`, `jsx-ast-utils`
- 93b2a9d [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `@babel/register`
- a962211 [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/plugin-transform-flow-strip-types`, `@babel/register`
- 0d2bc43 [Tests] `no-noninteractive-element-interactions`: add passing test cases (#876)
- ffefbad [Tests] `no-noninteractive-element-interactions`: add passing tests for form with onSubmit (#871)
- e7d405d [Tests] `no-static-element-interactions`: add passing test cases
6.6.0 / 2022-06-23
==================
- 566011b [New] `aria-role`: add `allowedInvalidRoles` option (#828)
- 64dcac6 [New] Introduce a plugin-wide setting for custom components. (#844)
- ce2c328 [Fix] `no-redundant-roles`, `role-supports-aria-props`: Remove implicit role from dl element (#848)
- fb20bc4 [Refactor] `role-supports-aria-props`: clean up the logic a bit
- 1826628 [Refactor] reduce egregious use of array spread, in favor of `[].concat` idiom
- 0f1615a [Docs] `no-static-element-interactions`: Update error message (#843)
- 9980d1d [Docs] Add infrastructure for auto-generating markdown table and list (#837)
- f878d3b [Docs] Update project readme (#831)
- aea7671 [Deps] update `@babel/runtime`, `array-includes`, `axe-core`, `jsx-ast-utils`
- d74173a [Deps] update `jsx-ast-utils`
- f6ba03c [Deps] update `@babel/runtime`, `jsx-ast-utils`
- 547dab4 [Deps] update `@babel/runtime`, `axe-core`, `minimatch`
- baaf791 [Deps] update `@babel/runtime`, `minimatch`, `semver`
- c015fef [Deps] update `@babel/runtime`, `axe-core`, `damerau-levenshtein`
- 832cbd6 [meta] add `export default` instead of `module.exports` (#861)
- ee933a2 [meta] Add CONTRIBUTING.md to solicit contributions (#846)
- fa3c869 [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `aud`, `eslint-plugin-eslint-plugin`, `eslint-plugin-flowtype`, `eslint-plugin-import`
- fb3d51e [Dev Deps] update `@babel/core`, `@babel/register`, `eslint-plugin-import`, `minimist`
- 8c1df4d [Dev Deps] pin `@technote-space/doctoc` because v2.5 is a breaking change
- fb071ab [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `eslint-plugin-eslint-plugin`
- 5e966e5 [Dev Deps] update `@babel/cli`
- f597f5b [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`
- 287854a [Tests] Fix `npm run flow` (#856)
- 112261c [Tests] skip fragment tests in eslint < 6
- ea877c4 [Tests] `img-redundant-alt-test`: add passing tests (#832)
- 685426d test: align usage of jest expect across tests (#827)
- c460a8b [Tests] move invalid test case to valid; changed in axe-core v4.4
6.5.1 / 2021-11-10
==================
- 8f7d0b0 [Fix] properly build `module.exports` (#824)
- 2fd2087 [Dev Deps] update `eslint-plugin-import`
6.5.0 / 2021-11-09
==================
- 0f5f582 [New] support ESLint 8.x (#810)
- 1dbc416 [Deps] update `@babel/runtime`, `axe-core`
- 4043d31 [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `eslint-config-airbnb-base`
- d143cba [Docs] HTTP => HTTPS (#823)
- 309b040 [Docs] `anchor-has-content`: add missing close / for jsx succeed example (#821)
- ba1e312 [eslint] simplify eslint command
- 0269025 [meta] change all `master` references in URLs to `HEAD`
- f1414cf [Dev Deps] add `eslint-plugin-eslint-plugin` (#818)
- f44fc05 [meta] update URLs
- df34872 [Refactor] switch to `export default` syntax for exporting rules (#819)
- ff26b82 [meta] fix prepublish scripts
- d4a57d8 [Deps] update `@babel/runtime`, `array-includes`, `axe-core`, `jsx-ast-utils`
- bd1dec6 [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `eslint-plugin-import`, `estraverse`, `safe-publish-latest`
- 434c4cf [Tests] do not test eslint 7 on node 11
- aed7a20 [Tests] use `@babel/eslint-parser` instead of `babel-eslint` (#811)
- 0021489 [actions] use codecov action
- 1251088 [meta] delete FUNDING.yml in favor of `.github` repo
- ecf7a27 [Docs] `scope`: replace duplicate `scope` word (#799)
- 952af25 [Fix] `no-access-key`: Fix wording and grammar (#800)
- 6cf7ac0 [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/plugin-transform-flow-strip-types`, `aud`, `eslint-plugin-flowtype`, `eslint-plugin-import`
- 79a35d4 [Deps] update `@babel/runtime`, `axe-core`, `damerau-levenshtein`
- 2a9ab71 [Tests] delete `src/util/getComputedRole-test.js` test in node 6
- 0c1c587 [Tests] `autocomplete-valid`: move some failed tests to passing
- 8830902 [Tests] fix eslint < 7 not understanding `import type`
- d57887c [Tests] ensure all tests run
- 55e5c11 Support img role for canvas (#796)
- 36102cd [meta] use `prepublishOnly` script for npm 7+
- 2501a7f Remove the link-button focus css from the anchor-is-valid doc (#662)
- d927625 Update recommended config to allow fieldset to have the radiogroup role (#746)
- 5aa8db9 [Docs] Clarify the title of the strictness table in the main doc (#786)
- df3c7ad [Docs] Document the similarity between html-has-lang and lang (#778)
- 426d4c2 Fix Flow warnings (#785)
- ecec8e4 Fully deprecate accessible-emoji rule (#782)
- 8a0e43c [Tests] remove .travis.yml
- f88bf6b [Dev Deps] update `flow-bin` to support aarch64 (#784)
- 369f9db [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/plugin-transform-flow-strip-types`, `aud`, `eslint-plugin-flowtype`, `jscodeshift`
- ce0785f [Deps] update `@babel/runtime`, `array-includes`, `axe-core`, `emoji-regex`
- 2c2a2ad [actions] update to use `node/install` action
- c275964 [Docs] `anchor-is-valid`: general cleanup (#728)
- 3df059e [Docs] `no-redundant-roles`: Adds missing closing square bracket (#775)
- 42ce5b7 [Docs] `anchor-is-valid`: Add Next.js case (#769)
- 2e5df91 [Tests] fix tests breaking on npm 7
- 066ccff [Docs] `no-noninteractive-tabindex`: Add example for tabIndex on seemingly non-interactive element (#760)
- 6b19aa5 [Tests] migrate tests to Github Actions (#764)
- 7e158e3 [meta] run `aud` in `posttest`
- 71f390f [Tests] stop using coveralls
- e54b466 [meta] add Automatic Rebase and Require Allow Edits workflows
- 7d5511d [New] `label-has-associated-control`: Add glob support (#749)
- 854da0c Ran npm update; latest packages (#763)
- 8637aa7 (source/pr/734, fork/pr/26) [patch] `strict` config: Turn off `label-has-for` (#734)
- d85ce54 [doc] Add link to MDN Aria documentation (#762)
- 20b48a4 [patch] `no-onchange`: Remove rule from recommended/strict configs, and deprecate (#757)
6.4.1 / 2020-10-26
==================
- f8a4496 Upgrade jsx-ast-utils to v3.1.0
6.4.0 / 2020-10-26
==================
- 83e4ff2 [Deps] update `axe-core`, `jsx-ast-utils`
- eb92b07 [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/plugin-transform-flow-strip-types`, `eslint-plugin-flowtype`, `eslint-plugin-import`, `estraverse`, `expect`, `object.assign`
- 3d98d7a [Deps] update `@babel/runtime`, `axe-core`
- f702f62 [readme] add Spanish translation
- c2ae092 [Docs] `no-static-element-interactions`: Fixed rule name in comments
- b90e20d Fix screenreader -> screen reader
- 645900a Fixed rule name in comments
- 381b9d6 [fix:634] Ignore control elements that are hidden
- 2c47f0a [Fix] `autocomplete-valid`: workaround for axe not being able to handle `null`
- 00bd6d8 Add failing test for autocomplete with dynamic type
- 3c49c9a Add WCAG guidelines to rule documentation
- 4ecaf35 Add a testcase for tablist to interactive supports focus
- dac6864 Deprecate the accessible-emoji rule
- 5191053 Update to axobject-query@2.2.0
- b315698 Allow negative tabindex in aria-activedescendant-has-tabindex
- 8e6fcd0 docs: fix travis badge now points to correct location at travis-ci.com
- 2234df7 Account for additional control elements in label-has-associated-control
- 5cbb718 Adding test cases for label tests
- 66c425c Additional test case for no-redundant-roles
6.3.1 / 2020-06-19
==================
- 765da0f Update to aria-query 4.2.2
- d528e8c Fix aria-level allowed on elements wit role heading (#704)
- 29c6859 [meta] remove yarn registry from npmrc, so publishing works
- f52c206 chore(package): update estraverse to version 5.0.0
6.3.0 / 2020-06-18
==================
- cce838a Update aria-query to 4.2.0
- 121e8a4 Add two test cases found while upgrading to ARIA 1.2
- 8059f51 Fix test failures raised by the upgrade to ARIA 1.2
- 0d24e3a Update package.json
- b1f412a Fix test failures in role-has-required-aria-props due to ARIA 1.2 updates
- 74cec6e Fix test failures in no-noninteractive-element-interactions due to ARIA 1.2 updates
- 835b89e Fix test failures in role-supports-aria-props-test due to ARIA 1.2 updates
- 730319b Account for the null semantic generic role in ARIA 1.2
- 7dfa7c9 Update aria-query from 4.0.1 to 4.0.2
- 42098b9 [Refactor] `img-redundant-alt`: removing a use of `some`
- a910d83 [Tests] `label-has-associated-control`: add test for <div><label /><input /></div>
- b273fe5 [New] Support ESLint 7.x
- 1a97632 [Deps] update `@babel/runtime`, `array-includes`, `axe-core`, `axobject-query`, `damerau-levenshtein`, `jsx-ast-utils`
- b36976f [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/plugin-transform-flow-types`, `babel-eslint`, `babel-jest`, `coveralls`, `eslint-config-airbnb-base`, `eslint-plugin-flowtype`, `eslint-plugin-import`, `estraverse`, `in-publish`, `jest`, `minimist`, `rimraf`, `safe-publish-latest`
- 89acdc4 fix: removing the use of the some function
- 410ae43 chore(package): update eslint-plugin-flowtype to version 5.0.0
- a87f83d fix(package): update emoji-regex to version 9.0.0
- 71940e6 chore(package): update babel-preset-airbnb to version 5.0.0
- d471f54 docs: Fix 404 links to WAI-ARIA spec
- 42a2016 Fixes #669: use the `language-tags` package to check the `lang` rule
- 7bcea20 [Tests] update axe-core
- f13dc38 [Deps] Pin axe-core version
- 33670bb fix: require missing 'autocomplete-valid' rule
- aca4c37 chore(mouse-event): revert unrelated formatting changes
- df1e275 fix(mouse-event): remove check from custom elements
- 1a16a1c chore(package): update jscodeshift to version 0.7.0
- 7a55cdd chore(package): update flow-bin to version 0.113.0
- 8e0d22b Update aria-query and axobject-query to the latest versions
- dd49060 Added test cases for an empty or undefined value of aria-label and aria-labelledby in alt-text
- 1a7b94f Updated dependencies including eslint-config-airbnb-base
- 3aea217 chore: replace ignoreNonDOM with inputComponents
- 1848d00 feat(autocomplete-valid): add to recommended & strict config
- 8703840 refactor: use to axe-cre 3.4 SerialVirtualNode format
- 3519c7b chore: Remove axe VirtualNode abstraction
- 9ac55c4 autocomplete-valid: Add inline comment
- 44c6098 Update axe-core to 3.3.0
- 9916990 new autocomplete-valid rule
- 82f598e [Docs] examples: add language in code block for syntax highlight
- 2529ad3 fixing casing issue on aria-props
- 00926f2 Update README.md
- ce5d121 Update README.md
- 031574e chore(package): update flow-bin to version 0.103.0
- e00e1db [meta] add FUNDING.yml
- e1e5fae Fix readme file
6.2.3 / 2019-06-30
=================
- [617] Add @babel/runtime to the dependencies
6.2.2 / 2019-06-29
=================
- Update jsx-ast-utils to v2.2.1
- Add @babel/cli to the dev dependencies
- Update ESLint to v6
- Update jsx-ast-utils to 2.2.0
- Update flow-bin to version 0.102.0
- [589] Allow expression statements for attribute values in no-noninteractive-tabindexlow-bin-0.101.0
- [583] Allow expression values in attributes by configurationrror
- [596] Adding a test case for no-static-element-interactionseper/flow-bin-0.101.0) Merge branch 'master' into greenkeeper/flow-bin-0.101.0
- Only run branch test coverage on the master branch
- chore(package): update flow-bin to version 0.100.0
- Allow select as a valid child of label.
- Allow Node 4 / ESLint 3 failure to unblock ESLint upgrade in PR #568
- chore(package): update flow-bin to version 0.99.0
- Remove rootDir from Jest path configs
- (fix) Template literals with undefined evaluate to the string undefined.
- adds more tests to “anchor-is-valid”
- Fixes “anchor-is-valid” false positive for hrefs starting with the word “javascript”
- chore(package): update eslint-plugin-flowtype to version 3.5.0
- Modified no-static-element-interactions to pass on non-literal roles.
- Added isNonLiteralProperty util method
- [#399] Account for spread in parser options
- [552] control-has-associated-label should allow generic links
- [issue 392] ul role='list' test case
- chore(package): update eslint to version 5.15.2
- chore(package): update flow-bin to version 0.95.0
- chore(package): update expect to version 24.3.1
- Fix typo: defintions > definitions
- docs: add proper title to links to axe website for media-has-caption
- docs: removes deprecated rule label-has-for
- docs: fix typo and couple grammatical errors in Readme
- Ignore null/undefined values in role-supports-aria-props rule
- Ignore undefined values in aria-proptypes rule
- Ignore null values in aria-proptypes rule
- set target for node 4
6.2.1 / 2019-02-03
=================
- 9980e45 [fix] Prevent Error when JSXSpreadAttribute is passed to isSemanticRoleElement
6.2.0 / 2019-01-25
=================
- 5650674 [new rule] control-has-associated-label checks interactives for a label
- f234698 [docs] add How to manage IDs
- 9924d03 [docs] document jsx-a11y/label-has-associated-control assert option
- 77b9870 [docs] Add newlines below headings
- 8244e43 [docs] Add syntax highlighting to example
- 26f41c8 [docs] Change explanation for role="presentation" escape hatch
- 33a1f94 [fix] - Purely decorative emojis do not need descriptions.
- 29d20f7 [fix] (package): update emoji-regex to version 7.0.2
- 0b63f73 [chore] (package): update flow-bin to version 0.88.0
- baa1344 [fix] Disable jsx-a11y/label-has-for in recommended
- 2c5fb06 [chore] (package): update jscodeshift to version 0.6.0
- 87debc0 [fix] corrected no-noninteractive-element-to-interactive-role.md file
- d56265b [chore] (package): update flow-bin to version 0.87.0
- 477966f [fix] Update test for implicit role of `img`
- f484ce3 [fix] No implicit role for `<img>` with `alt=""`
- 6c33bcb [fix] Add select to the list of default control elements in label-has-associated-control
- 011f8d9 [fix] Dialog and Alert roles can host keyboard listeners
- 0f6a8af [fix] More easier `plugin:jsx-a11y/{recommended,strict}` configs
- 3844248 [fix] Mark the replacement for label-has-for
- 93265cb [fix] normalizedValues to values
- 651366c [fix] Make aria-role case sensitive
- 56d3b9a [fix] [484] Fix role-has-required-aria-props for semantic elements like input[checkbox]
- 46e9abd [fix] Handle the type={truthy} case in jsx
6.1.2 / 2018-10-05
=================
- [fix] Add link-type styling recommendation to anchor-is-valid #486
- [fix] `label-has-for`: `textarea`s are inputs too #470
6.1.1 / 2018-07-03
==================
- [fix] aria-proptypes support for idlist, #454
- [fix] Image with expanded props throws 'The prop must be a JSXAttribute collected by the AST parser.', #459
- [fix] label-has-for: broken in v6.1.0, #455
6.1.0 / 2018-06-26
==================
- [new] Support for eslint v5, #451
- [new] aria-query updated to latest version
- [new] eslint-config-airbnb-base updated to the latest version
- [deprecate] The rule label-has-for is deprecated and replaced with label-has-associated-control
- [fix] heading-has-content updated to work with custom components, #431
- [fix] aria-errormessage prop is now a valid ARIA property, #424
6.0.2 / 2017-06-28
==================
- [fix] Prefix directories in `.npmignore` with `/` so it only matches the top-level directory
6.0.1 / 2017-06-28
==================
- [temporary] Remove `src` and `flow` from package to resolve flow issues for consuming packages.
6.0.0 / 2017-06-05
=================
- [new] Add rule `anchor-is-valid`. See documentation for configuration options. Thanks @AlmeroSteyn.
- [breaking] `href-no-hash` replaced with `anchor-is-valid` in the recommended and strict configs. Use the `invalidHref` aspect (active by default) in `anchor-is-valid` to continue to apply the behavior provided by `href-no-hash`.
- [breaking] Removed support for ESLint peer dependency at version ^2.10.2.
- [update] The rule `label-has-for` now allows inputs nested in label tags. Previously it was strict about requiring a `for` attribute. Thanks @ignatiusreza and @mjaltamirano.
- [update] New configuration for `interactive-supports-focus`. Recommended and strict configs for now contain a trimmed-down whitelist of roles that will be checked.
- [fix] Incompatibility between node version 4 and 5. Thanks @evilebottnawi.
- [fix] Missing README entry for `media-has-caption`. Thanks @ismail-syed.
- [fix] README updates explaining recommended and strict configs. Thanks @Donaldini.
- [fix] Updated to aria-query@0.7.0, which includes new ARIA 1.1 properties. Previously, the `aria-props` rule incorrectly threw errors for these new properties.
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/jsx-eslint/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/jsx-eslint/eslint-plugin-jsx-a11y/tree/HEAD/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/jsx-eslint/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.

8
node_modules/eslint-plugin-jsx-a11y/LICENSE.md generated vendored Normal file
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.

423
node_modules/eslint-plugin-jsx-a11y/README.md generated vendored Normal file
View File

@@ -0,0 +1,423 @@
<p align="center">
<a href="https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/actions">
<img src="https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/jsx-eslint/eslint-plugin-jsx-a11y"
alt="CI 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/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/LICENSE.md">
<img src="https://img.shields.io/npm/l/eslint-plugin-jsx-a11y.svg"
alt="license">
</a>
<a href='https://coveralls.io/github/jsx-eslint/eslint-plugin-jsx-a11y?branch=master'>
<img src='https://coveralls.io/repos/github/jsx-eslint/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>
<a href='https://tidelift.com/subscription/pkg/npm-eslint-plugin-jsx-a11y?utm_source=npm-eslint-plugin-jsx-a11y&utm_medium=referral&utm_campaign=readme'>Get professional support for eslint-plugin-jsx-a11y on Tidelift</a>
# eslint-plugin-jsx-a11y
Static AST checker for accessibility rules on JSX elements.
#### _Read this in [other languages](https://github.com/ari-os310/eslint-plugin-jsx-a11y/blob/HEAD/translations/Translations.md)._
[Mexican Spanish🇲🇽](https://github.com/ari-os310/eslint-plugin-jsx-a11y/blob/HEAD/translations/README.mx.md)
## Why?
This plugin does a static evaluation of the JSX to spot accessibility issues in React apps. Because it only catches errors in static code, use it in combination with [@axe-core/react](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/react) to test the accessibility of the rendered DOM. Consider these tools just as one step of a larger a11y testing process and always test your apps with assistive technology.
## Installation
**If you are installing this plugin via `eslint-config-airbnb`, please follow [these instructions](https://github.com/airbnb/javascript/tree/HEAD/packages/eslint-config-airbnb#eslint-config-airbnb-1).**
You'll first need to install [ESLint](https://eslint.org/docs/latest/user-guide/getting-started):
```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.
<a id="usage"></a>
## Usage - Legacy Config (`.eslintrc`)
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"]
}
```
### Configurations
> As you are extending our configuration, you can omit `"plugins": ["jsx-a11y"]` from your `.eslintrc` configuration file.
```json
{
"settings": {
"jsx-a11y": {
"polymorphicPropName": "as",
"components": {
"CityInput": "input",
"CustomButton": "button",
"MyButton": "button",
"RoundButton": "button"
},
"attributes": {
"for": ["htmlFor", "for"]
}
}
}
}
```
## Usage - Flat Config (`eslint.config.js`)
The default export of `eslint-plugin-jsx-a11y` is a plugin object.
```js
const jsxA11y = require('eslint-plugin-jsx-a11y');
module.exports = [
{
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
plugins: {
'jsx-a11y': jsxA11y,
},
languageOptions: {
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
// ... any rules you want
'jsx-a11y/alt-text': 'error',
},
// ... others are omitted for brevity
},
];
```
### Shareable Configs
There are two shareable configs, provided by the plugin.
- `flatConfigs.recommended`
- `flatConfigs.strict`
#### CJS
```js
const jsxA11y = require('eslint-plugin-jsx-a11y');
export default [
jsxA11y.flatConfigs.recommended,
{
// Your additional configs and overrides
},
];
```
#### ESM
```js
import jsxA11y from 'eslint-plugin-jsx-a11y';
export default [
jsxA11y.flatConfigs.recommended,
{
// Your additional configs and overrides
},
];
```
**Note**: Our shareable configs do NOT configure `files` or [`languageOptions.globals`](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new#configuration-objects).
For most of the cases, you probably want to configure some of these properties yourself.
```js
const jsxA11y = require('eslint-plugin-jsx-a11y');
const globals = require('globals');
module.exports = [
{
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
...jsxA11y.flatConfigs.recommended,
languageOptions: {
...jsxA11y.flatConfigs.recommended.languageOptions,
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
];
```
#### Component Mapping
To enable your custom components to be checked as DOM elements, you can set global settings in your configuration file by mapping each custom component name to a DOM element type.
#### Attribute Mapping
To configure the JSX property to use for attribute checking, you can set global settings in your configuration file by mapping each DOM attribute to the JSX property you want to check.
For example, you may want to allow the `for` attribute in addition to the `htmlFor` attribute for checking label associations.
#### Polymorphic Components
You can optionally use the `polymorphicPropName` setting to define the prop your code uses to create polymorphic components.
This setting will be used determine the element type in rules that require semantic context.
For example, if you set the `polymorphicPropName` setting to `as` then this element:
`<Box as="h3">Configurations </Box>`
will be evaluated as an `h3`. If no `polymorphicPropName` is set, then the component will be evaluated as `Box`.
To restrict polymorphic linting to specified components, additionally set `polymorphicAllowList` to an array of component names.
⚠️ Polymorphic components can make code harder to maintain; please use this feature with caution.
## Supported Rules
<!-- begin auto-generated rules list -->
💼 Configurations enabled in.\
🚫 Configurations disabled in.\
☑️ Set in the `recommended` configuration.\
🔒 Set in the `strict` configuration.\
❌ Deprecated.
| Name                                          | Description | 💼 | 🚫 | ❌ |
| :----------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :---- | :---- | :- |
| [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-ambiguous-text](docs/rules/anchor-ambiguous-text.md) | Enforce `<a>` text to not exactly match "click here", "here", "link", or "a link". | | ☑️ | |
| [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. | ☑️ 🔒 | | |
| [autocomplete-valid](docs/rules/autocomplete-valid.md) | Enforce that autocomplete attributes are used correctly. | ☑️ 🔒 | | |
| [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. | ☑️ 🔒 | | |
| [control-has-associated-label](docs/rules/control-has-associated-label.md) | Enforce that a control (an interactive element) has a text label. | | ☑️ 🔒 | |
| [heading-has-content](docs/rules/heading-has-content.md) | Enforce heading (`h1`, `h2`, etc) elements contain accessible content. | ☑️ 🔒 | | |
| [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-associated-control](docs/rules/label-has-associated-control.md) | Enforce that a `label` tag has a text label and an associated control. | ☑️ 🔒 | | |
| [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-aria-hidden-on-focusable](docs/rules/no-aria-hidden-on-focusable.md) | Disallow `aria-hidden="true"` from being set on focusable elements. | | | |
| [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. | ☑️ 🔒 | | |
| [prefer-tag-over-role](docs/rules/prefer-tag-over-role.md) | Enforces using semantic DOM elements over the ARIA `role` property. | | | |
| [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. | ☑️ 🔒 | | |
<!-- end auto-generated rules list -->
The following rules have extra options when in _recommended_ mode:
### no-interactive-element-to-noninteractive-role
```js
'jsx-a11y/no-interactive-element-to-noninteractive-role': [
'error',
{
tr: ['none', 'presentation'],
},
]
```
### no-noninteractive-element-interactions
```js
'jsx-a11y/no-noninteractive-element-interactions': [
'error',
{
handlers: [
'onClick',
'onMouseDown',
'onMouseUp',
'onKeyPress',
'onKeyDown',
'onKeyUp',
],
},
]
```
### no-noninteractive-element-to-interactive-role
```js
'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
```js
'jsx-a11y/no-noninteractive-tabindex': [
'error',
{
tags: [],
roles: ['tabpanel'],
},
]
```
### no-static-element-interactions
```js
'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.
```sh
./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 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 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 afforded 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,15 @@
/**
* @flow
*/
export type IdentifierMockType = {|
type: 'Identifier',
name: string,
|};
export default function IdentifierMock(ident: string): IdentifierMockType {
return {
type: 'Identifier',
name: ident,
};
}

View File

@@ -0,0 +1,39 @@
/**
* @flow
*/
import toAST from 'to-ast'; // eslint-disable-line import/no-extraneous-dependencies
import JSXExpressionContainerMock from './JSXExpressionContainerMock';
export type JSXAttributeMockType = {
type: 'JSXAttribute',
name: {
type: 'JSXIdentifier',
name: string,
},
value: mixed,
};
export default function JSXAttributeMock(prop: string, value: mixed, isExpressionContainer?: boolean = false): JSXAttributeMockType {
let astValue;
if (value && value.type !== undefined) {
astValue = value;
} else {
astValue = toAST(value);
}
let attributeValue = astValue;
if (isExpressionContainer || astValue.type !== 'Literal') {
attributeValue = JSXExpressionContainerMock(astValue);
} else if (attributeValue.type === 'Literal' && !('raw' in (attributeValue: any))) {
(attributeValue: any).raw = JSON.stringify((attributeValue: any).value);
}
return {
type: 'JSXAttribute',
name: {
type: 'JSXIdentifier',
name: prop,
},
value: attributeValue,
};
}

View File

@@ -0,0 +1,37 @@
/**
* @flow
*/
import type { JSXAttributeMockType } from './JSXAttributeMock';
export type JSXElementMockType = {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: {
type: 'JSXIdentifier',
name: string,
},
attributes: Array<JSXAttributeMockType>,
},
children: Array<Node>,
};
export default function JSXElementMock(
tagName: string,
attributes: Array<JSXAttributeMockType> = [],
children?: Array<Node> = [],
): JSXElementMockType {
return {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: {
type: 'JSXIdentifier',
name: tagName,
},
attributes,
},
children,
};
}

View File

@@ -0,0 +1,15 @@
/**
* @flow
*/
export type JSXExpressionContainerMockType = {
type: 'JSXExpressionContainer',
expression: mixed,
}
export default function JSXExpressionContainerMock(exp: mixed): JSXExpressionContainerMockType {
return {
type: 'JSXExpressionContainer',
expression: exp,
};
}

View File

@@ -0,0 +1,18 @@
/**
* @flow
*/
import IdentifierMock from './IdentifierMock';
import type { IdentifierMockType } from './IdentifierMock';
export type JSXSpreadAttributeMockType = {
type: 'JSXSpreadAttribute',
argument: IdentifierMockType,
};
export default function JSXSpreadAttributeMock(identifier: string): JSXSpreadAttributeMockType {
return {
type: 'JSXSpreadAttribute',
argument: IdentifierMock(identifier),
};
}

View File

@@ -0,0 +1,17 @@
/**
* @flow
*/
export type JSXTextMockType = {|
type: 'JSXText',
value: string,
raw: string,
|};
export default function JSXTextMock(value: string): JSXTextMockType {
return {
type: 'JSXText',
value,
raw: value,
};
}

View File

@@ -0,0 +1,17 @@
/**
* @flow
*/
export type LiteralMockType = {|
type: 'Literal',
value: string,
raw: string,
|};
export default function LiteralMock(value: string): LiteralMockType {
return {
type: 'Literal',
value,
raw: value,
};
}

View File

@@ -0,0 +1,212 @@
/**
* @flow
*/
import { dom, roles } from 'aria-query';
import includes from 'array-includes';
import fromEntries from 'object.fromentries';
import JSXAttributeMock from './JSXAttributeMock';
import JSXElementMock from './JSXElementMock';
import type { JSXAttributeMockType } from './JSXAttributeMock';
import type { JSXElementMockType } from './JSXElementMock';
const domElements = [...dom.keys()];
const roleNames = [...roles.keys()];
const interactiveElementsMap = {
a: [{ prop: 'href', value: '#' }],
area: [{ prop: 'href', value: '#' }],
audio: [],
button: [],
canvas: [],
datalist: [],
embed: [],
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' }],
link: [{ prop: 'href', value: '#' }],
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: {[string]: Array<{[string]: string}>} = {
abbr: [],
aside: [],
article: [],
blockquote: [],
body: [],
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: [],
optgroup: [],
output: [],
p: [],
pre: [],
progress: [],
ruby: [],
'section[aria-label]': [{ prop: 'aria-label' }],
'section[aria-labelledby]': [{ prop: 'aria-labelledby' }],
table: [],
tbody: [],
td: [],
tfoot: [],
thead: [],
time: [],
ul: [],
};
const indeterminantInteractiveElementsMap: { [key: string]: Array<any> } = fromEntries(domElements.map((name: string) => [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
&& roles.get(role).superClass.some((klasses) => includes(klasses, 'widget'))
));
const nonInteractiveRoles = roleNames
.filter((role) => (
!roles.get(role).abstract
&& !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.
&& !includes(['toolbar'], role)
));
export function genElementSymbol(openingElement: Object): string {
return (
openingElement.name.name + (openingElement.attributes.length > 0
? `${openingElement.attributes.map((attr) => `[${attr.name.name}="${attr.value.value}"]`).join('')}`
: ''
)
);
}
export function genInteractiveElements(): Array<JSXElementMockType> {
return Object.keys(interactiveElementsMap).map((elementSymbol: string): JSXElementMockType => {
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(): Array<JSXElementMockType> {
return interactiveRoles.concat('button article', 'fakerole button article').map((value): JSXElementMockType => JSXElementMock(
'div',
[JSXAttributeMock('role', value)],
));
}
export function genNonInteractiveElements(): Array<JSXElementMockType> {
return Object.keys(nonInteractiveElementsMap).map((elementSymbol): JSXElementMockType => {
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(): Array<JSXElementMockType> {
return [
...nonInteractiveRoles,
'article button',
'fakerole article button',
].map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}
export function genAbstractRoleElements(): Array<JSXElementMockType> {
return abstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}
export function genNonAbstractRoleElements(): Array<JSXElementMockType> {
return nonAbstractRoles.map((value) => JSXElementMock('div', [JSXAttributeMock('role', value)]));
}
export function genIndeterminantInteractiveElements(): Array<JSXElementMockType> {
return Object.keys(indeterminantInteractiveElementsMap).map((name) => {
const attributes = indeterminantInteractiveElementsMap[name].map(({ prop, value }): JSXAttributeMockType => JSXAttributeMock(prop, value));
return JSXElementMock(name, attributes);
});
}

View File

@@ -0,0 +1,6 @@
/* eslint-disable import/prefer-default-export, no-underscore-dangle */
import * as axe from 'axe-core';
export function axeFailMessage(checkId, data) {
return axe.utils.getCheckMessage(checkId, 'fail', data);
}

View File

@@ -0,0 +1,9 @@
import { version } from 'eslint/package.json';
import semver from 'semver';
const isESLintV8 = semver.major(version) >= 8;
// eslint-disable-next-line global-require, import/no-dynamic-require, import/no-unresolved
const getESLintCoreRule = (ruleId) => (isESLintV8 ? require('eslint/use-at-your-own-risk').builtinRules.get(ruleId) : require(`eslint/lib/rules/${ruleId}`));
export default getESLintCoreRule;

View File

@@ -0,0 +1,186 @@
import path from 'path';
import semver from 'semver';
import entries from 'object.entries';
import { version } from 'eslint/package.json';
import flatMap from 'array.prototype.flatmap';
let tsParserVersion;
try {
// eslint-disable-next-line import/no-unresolved, global-require
tsParserVersion = require('@typescript-eslint/parser/package.json').version;
} catch (e) { /**/ }
const disableNewTS = semver.satisfies(tsParserVersion, '>= 4.1') // this rule is not useful on v4.1+ of the TS parser
? (x) => ({ ...x, features: [].concat(x.features, 'no-ts-new') })
: (x) => x;
function minEcmaVersion(features, parserOptions) {
const minEcmaVersionForFeatures = {
'class fields': 2022,
'optional chaining': 2020,
'nullish coalescing': 2020,
};
const result = Math.max(
...[].concat(
(parserOptions && parserOptions.ecmaVersion) || [],
flatMap(entries(minEcmaVersionForFeatures), (entry) => {
const f = entry[0];
const y = entry[1];
return features.has(f) ? y : [];
}),
).map((y) => (y > 5 && y < 2015 ? y + 2009 : y)), // normalize editions to years
);
return Number.isFinite(result) ? result : undefined;
}
const NODE_MODULES = '../../node_modules';
const parsers = {
BABEL_ESLINT: path.join(__dirname, NODE_MODULES, 'babel-eslint'),
'@BABEL_ESLINT': path.join(__dirname, NODE_MODULES, '@babel/eslint-parser'),
TYPESCRIPT_ESLINT: path.join(__dirname, NODE_MODULES, 'typescript-eslint-parser'),
'@TYPESCRIPT_ESLINT': path.join(__dirname, NODE_MODULES, '@typescript-eslint/parser'),
disableNewTS,
babelParserOptions: function parserOptions(test, features) {
return {
...test.parserOptions,
requireConfigFile: false,
babelOptions: {
presets: [
'@babel/preset-react',
],
plugins: [
'@babel/plugin-syntax-do-expressions',
'@babel/plugin-syntax-function-bind',
['@babel/plugin-syntax-decorators', { legacy: true }],
],
parserOpts: {
allowSuperOutsideMethod: false,
allowReturnOutsideFunction: false,
},
},
ecmaFeatures: {
...test.parserOptions && test.parserOptions.ecmaFeatures,
jsx: true,
modules: true,
legacyDecorators: features.has('decorators'),
},
};
},
all: function all(tests) {
const t = flatMap(tests, (test) => {
/* eslint no-param-reassign: 0 */
if (typeof test === 'string') {
test = { code: test };
}
if ('parser' in test) {
delete test.features;
return test;
}
const features = new Set([].concat(test.features || []));
delete test.features;
const es = minEcmaVersion(features, test.parserOptions);
function addComment(testObject, parser) {
const extras = [].concat(
`features: [${Array.from(features).join(',')}]`,
`parser: ${parser}`,
testObject.parserOptions ? `parserOptions: ${JSON.stringify(testObject.parserOptions)}` : [],
testObject.options ? `options: ${JSON.stringify(testObject.options)}` : [],
testObject.settings ? `settings: ${JSON.stringify(testObject.settings)}` : [],
);
const extraComment = `\n// ${extras.join(', ')}`;
// Augment expected fix code output with extraComment
const nextCode = { code: testObject.code + extraComment };
const nextOutput = testObject.output && { output: testObject.output + extraComment };
// Augment expected suggestion outputs with extraComment
// `errors` may be a number (expected number of errors) or an array of
// error objects.
const nextErrors = testObject.errors
&& typeof testObject.errors !== 'number'
&& {
errors: testObject.errors.map(
(errorObject) => {
const nextSuggestions = errorObject.suggestions && {
suggestions: errorObject.suggestions.map((suggestion) => ({ ...suggestion, output: suggestion.output + extraComment })),
};
return { ...errorObject, ...nextSuggestions };
},
),
};
return {
...testObject,
...nextCode,
...nextOutput,
...nextErrors,
};
}
const skipBase = (features.has('class fields') && semver.satisfies(version, '< 8'))
|| (es >= 2020 && semver.satisfies(version, '< 6'))
|| features.has('no-default')
|| features.has('bind operator')
|| features.has('do expressions')
|| features.has('decorators')
|| features.has('flow')
|| features.has('ts')
|| features.has('types')
|| (features.has('fragment') && semver.satisfies(version, '< 5'));
const skipBabel = features.has('no-babel');
const skipOldBabel = skipBabel
|| features.has('no-babel-old')
|| features.has('optional chaining')
|| semver.satisfies(version, '>= 8');
const skipNewBabel = skipBabel
|| features.has('no-babel-new')
|| !semver.satisfies(version, '^7.5.0') // require('@babel/eslint-parser/package.json').peerDependencies.eslint
|| features.has('flow')
|| features.has('types')
|| features.has('ts');
const skipTS = semver.satisfies(version, '<= 5') // TODO: make these pass on eslint 5
|| features.has('no-ts')
|| features.has('flow')
|| features.has('jsx namespace')
|| features.has('bind operator')
|| features.has('do expressions');
const tsOld = !skipTS && !features.has('no-ts-old');
const tsNew = !skipTS && !features.has('no-ts-new');
return [].concat(
skipBase ? [] : addComment(
{
...test,
...typeof es === 'number' && {
parserOptions: { ...test.parserOptions, ecmaVersion: es },
},
},
'default',
),
skipOldBabel ? [] : addComment({
...test,
parser: parsers.BABEL_ESLINT,
parserOptions: parsers.babelParserOptions(test, features),
}, 'babel-eslint'),
skipNewBabel ? [] : addComment({
...test,
parser: parsers['@BABEL_ESLINT'],
parserOptions: parsers.babelParserOptions(test, features),
}, '@babel/eslint-parser'),
tsOld ? addComment({ ...test, parser: parsers.TYPESCRIPT_ESLINT }, 'typescript-eslint') : [],
tsNew ? addComment({ ...test, parser: parsers['@TYPESCRIPT_ESLINT'] }, '@typescript-eslint/parser') : [],
);
});
return t;
},
};
export default parsers;

View File

@@ -0,0 +1,53 @@
import { version as eslintVersion } from 'eslint/package.json';
import semver from 'semver';
const usingLegacy = semver.major(eslintVersion) < 9;
const defaultParserOptions = {
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
};
const defaultLegacyParserOptions = {
...defaultParserOptions,
ecmaVersion: 2018,
};
const defaultLanguageOptions = {
ecmaVersion: 'latest',
parserOptions: {
...defaultParserOptions,
},
};
export default function parserOptionsMapper({
code,
errors,
options = [],
languageOptions = {},
settings = {},
}) {
return usingLegacy
? {
code,
errors,
options,
parserOptions: {
...defaultLegacyParserOptions,
...languageOptions,
},
settings,
}
: {
code,
errors,
options,
languageOptions: {
...defaultLanguageOptions,
...languageOptions,
},
settings,
};
}

View File

@@ -0,0 +1,33 @@
/**
* @flow
*/
import entries from 'object.entries';
import flatMap from 'array.prototype.flatmap';
import fromEntries from 'object.fromentries';
type ESLintTestRunnerTestCase = {
code: string,
errors: ?Array<{ message: string, type: string }>,
options: ?Array<mixed>,
parserOptions: ?Array<mixed>,
settings?: {[string]: mixed},
};
type RuleOptionsMapperFactoryType = (
params: ESLintTestRunnerTestCase
) => ESLintTestRunnerTestCase;
export default function ruleOptionsMapperFactory(ruleOptions: Array<mixed> = []): RuleOptionsMapperFactoryType {
// eslint-disable-next-line
return ({ code, errors, options, parserOptions, settings }: ESLintTestRunnerTestCase): ESLintTestRunnerTestCase => {
return {
code,
errors,
// Flatten the array of objects in an array of one object.
options: [fromEntries(flatMap((options || []).concat(ruleOptions), (item) => entries(item)))],
parserOptions,
settings,
};
};
}

View File

@@ -0,0 +1,40 @@
/* eslint global-require: 0 */
import fs from 'fs';
import path from 'path';
import test from 'tape';
import plugin from '../src';
const rules = fs.readdirSync(path.resolve(__dirname, '../src/rules/'))
.map((f) => path.basename(f, '.js'));
test('all rule files should be exported by the plugin', (t) => {
rules.forEach((ruleName) => {
t.equal(
plugin.rules[ruleName],
require(path.join('../src/rules', ruleName)), // eslint-disable-line import/no-dynamic-require
`exports ${ruleName}`,
);
});
t.end();
});
test('configurations', (t) => {
t.notEqual(plugin.configs.recommended, undefined, 'exports a \'recommended\' configuration');
t.end();
});
test('schemas', (t) => {
rules.forEach((ruleName) => {
const rule = require(path.join('../src/rules', ruleName)); // eslint-disable-line import/no-dynamic-require
const schema = rule.meta && rule.meta.schema && rule.meta.schema[0];
const { type } = schema;
t.equal(type, 'object', `${ruleName} exports a schema with type object`);
});
t.end();
});

View File

@@ -0,0 +1,66 @@
/**
* @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';
import parsers from '../../__util__/helpers/parsers';
// -----------------------------------------------------------------------------
// 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: parsers.all([].concat(
{ 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>' },
{ code: '<span aria-hidden>{props.emoji}</span>' },
{ code: '<span aria-hidden="true">🐼</span>' },
{ code: '<span aria-hidden>🐼</span>' },
{ code: '<div aria-hidden="true">🐼</div>' },
{ code: '<input type="hidden">🐼</input>' },
{
code: '<CustomInput type="hidden">🐼</CustomInput>',
settings: { 'jsx-a11y': { components: { CustomInput: 'input' } } },
},
{
code: '<Box as="input" type="hidden">🐼</Box>',
settings: { 'jsx-a11y': { polymorphicPropName: 'as' } },
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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] },
{ code: '<span aria-hidden="false">🐼</span>', errors: [expectedError] },
{ code: '<CustomInput type="hidden">🐼</CustomInput>', errors: [expectedError] },
{
code: '<Box as="span">🐼</Box>',
settings: { 'jsx-a11y': { polymorphicPropName: 'as' } },
errors: [expectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,291 @@
/**
* @fileoverview Enforce all elements that require alternative text have it.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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 ariaLabelValueError = {
message: 'The aria-label attribute must have a value. The alt attribute is preferred over aria-label for images.',
};
const ariaLabelledbyValueError = {
message: 'The aria-labelledby attribute must have a value. The alt attribute is preferred over aria-labelledby for images.',
};
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 = {
message: 'Embedded <object> elements must have alternative text by providing inner text, aria-label or aria-labelledby props.',
};
const areaError = {
message: 'Each area of an image map must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
};
const inputImageError = {
message: '<input> elements with type="image" must have a text alternative through the `alt`, `aria-label`, or `aria-labelledby` prop.',
};
const componentsSettings = {
'jsx-a11y': {
polymorphicPropName: 'as',
components: {
Input: 'input',
},
},
};
const array = [{
img: ['Thumbnail', 'Image'],
object: ['Object'],
area: ['Area'],
'input[type="image"]': ['InputImage'],
}];
ruleTester.run('alt-text', rule, {
valid: parsers.all([].concat(
// 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"} />' },
{ code: '<img aria-label="foo" />' },
{ code: '<img aria-labelledby="id1" />' },
// 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 />' },
{ code: '<Input type="image" alt="" />', settings: componentsSettings },
{ code: '<SomeComponent as="input" type="image" alt="" />', settings: componentsSettings },
// 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: parsers.all([].concat(
// 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()] },
{ code: '<img aria-label={undefined} />', errors: [ariaLabelValueError] },
{ code: '<img aria-labelledby={undefined} />', errors: [ariaLabelledbyValueError] },
{ code: '<img aria-label="" />', errors: [ariaLabelValueError] },
{ code: '<img aria-labelledby="" />', errors: [ariaLabelledbyValueError] },
{ code: '<SomeComponent as="img" aria-label="" />', settings: componentsSettings, errors: [ariaLabelValueError] },
// DEFAULT ELEMENT 'object' TESTS
{ code: '<object />', errors: [objectError] },
{ code: '<object><div aria-hidden /></object>', errors: [objectError] },
{ code: '<object title={undefined} />', errors: [objectError] },
{ code: '<object aria-label="" />', errors: [objectError] },
{ code: '<object aria-labelledby="" />', errors: [objectError] },
{ code: '<object aria-label={undefined} />', errors: [objectError] },
{ code: '<object aria-labelledby={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] },
{ code: '<area aria-label="" />', errors: [areaError] },
{ code: '<area aria-label={undefined} />', errors: [areaError] },
{ code: '<area aria-labelledby="" />', errors: [areaError] },
{ code: '<area aria-labelledby={undefined} />', 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] },
{ code: '<input type="image" aria-label="" />', errors: [inputImageError] },
{ code: '<input type="image" aria-label={undefined} />', errors: [inputImageError] },
{ code: '<input type="image" aria-labelledby="" />', errors: [inputImageError] },
{ code: '<input type="image" aria-labelledby={undefined} />', 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 },
{ code: '<Input type="image" />', errors: [inputImageError], settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,117 @@
/**
* @fileoverview Enforce `<a>` text to not exactly match "click here", "here", "link", or "a link".
* @author Matt Wang
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/anchor-ambiguous-text';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const DEFAULT_AMBIGUOUS_WORDS = [
'click here',
'here',
'link',
'a link',
'learn more',
];
const expectedErrorGenerator = (words) => ({
message: `Ambiguous text within anchor. Screenreader users rely on link text for context; the words "${words.join('", "')}" are ambiguous and do not provide enough context.`,
type: 'JSXOpeningElement',
});
const expectedError = expectedErrorGenerator(DEFAULT_AMBIGUOUS_WORDS);
ruleTester.run('anchor-ambiguous-text', rule, {
valid: parsers.all([].concat(
{ code: '<a>documentation</a>;' },
{ code: '<a>${here}</a>;' },
{ code: '<a aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</a>;' },
{ code: '<a><span aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</span></a>;' },
{ code: '<a><img alt="documentation" /></a>;' },
{
code: '<a>click here</a>',
options: [{
words: ['disabling the defaults'],
}],
},
{
code: '<Link>documentation</Link>;',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{
code: '<a><Image alt="documentation" /></a>;',
settings: { 'jsx-a11y': { components: { Image: 'img' } } },
},
{
code: '<Link>${here}</Link>;',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{
code: '<Link aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</Link>;',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{
code: '<Link>click here</Link>',
options: [{
words: ['disabling the defaults with components'],
}],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<a>here</a>;', errors: [expectedError] },
{ code: '<a>HERE</a>;', errors: [expectedError] },
{ code: '<a>click here</a>;', errors: [expectedError] },
{ code: '<a>learn more</a>;', errors: [expectedError] },
{ code: '<a>learn more</a>;', errors: [expectedError] },
{ code: '<a>learn more.</a>;', errors: [expectedError] },
{ code: '<a>learn more?</a>;', errors: [expectedError] },
{ code: '<a>learn more,</a>;', errors: [expectedError] },
{ code: '<a>learn more!</a>;', errors: [expectedError] },
{ code: '<a>learn more;</a>;', errors: [expectedError] },
{ code: '<a>learn more:</a>;', errors: [expectedError] },
{ code: '<a>link</a>;', errors: [expectedError] },
{ code: '<a>a link</a>;', errors: [expectedError] },
{ code: '<a aria-label="click here">something</a>;', errors: [expectedError] },
{ code: '<a> a link </a>;', errors: [expectedError] },
{ code: '<a>a<i></i> link</a>;', errors: [expectedError] },
{ code: '<a><i></i>a link</a>;', errors: [expectedError] },
{ code: '<a><span>click</span> here</a>;', errors: [expectedError] },
{ code: '<a><span> click </span> here</a>;', errors: [expectedError] },
{ code: '<a><span aria-hidden>more text</span>learn more</a>;', errors: [expectedError] },
{ code: '<a><span aria-hidden="true">more text</span>learn more</a>;', errors: [expectedError] },
{ code: '<a><img alt="click here"/></a>;', errors: [expectedError] },
{ code: '<a alt="tutorial on using eslint-plugin-jsx-a11y">click here</a>;', errors: [expectedError] },
{ code: '<a><span alt="tutorial on using eslint-plugin-jsx-a11y">click here</span></a>;', errors: [expectedError] },
{ code: '<a><CustomElement>click</CustomElement> here</a>;', errors: [expectedError] },
{
code: '<Link>here</Link>',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{
code: '<a><Image alt="click here" /></a>',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Image: 'img' } } },
},
{
code: '<a>a disallowed word</a>',
errors: [expectedErrorGenerator(['a disallowed word'])],
options: [{
words: ['a disallowed word'],
}],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,54 @@
/**
* @fileoverview Enforce anchor elements to contain accessible content.
* @author Lisa Ring & Niklas Holmberg
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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: parsers.all([].concat(
{ 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} />' },
{ code: '<Link />' },
{
code: '<Link>foo</Link>',
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
{ code: '<a title={title} />' },
{ code: '<a aria-label={ariaLabel} />' },
{ code: '<a title={title} aria-label={ariaLabel} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<a />', errors: [expectedError] },
{ code: '<a><Bar aria-hidden /></a>', errors: [expectedError] },
{ code: '<a>{undefined}</a>', errors: [expectedError] },
{
code: '<Link />',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Link: 'a' } } },
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,532 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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. Learn more: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/anchor-is-valid.md';
const noHrefErrorMessage = 'The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address as the href value. If you cannot provide an href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/anchor-is-valid.md';
const invalidHrefErrorMessage = 'The href attribute requires a valid value to be accessible. Provide a valid, navigable address as the href value. If you cannot provide a valid href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/anchor-is-valid.md';
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'],
}];
const componentsSettings = {
'jsx-a11y': {
components: {
Anchor: 'a',
Link: 'a',
},
},
};
ruleTester.run('anchor-is-valid', rule, {
valid: parsers.all([].concat(
// DEFAULT ELEMENT 'a' TESTS
{ code: '<Anchor />' },
{ 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="javascript" />' },
{ code: '<a href="javascriptFoo" />' },
{ code: '<a href={`#foo`}/>' },
{ code: '<a href={"foo"}/>' },
{ code: '<a href={"javascript"}/>' },
{ code: '<a href={`#javascript`}/>' },
{ code: '<a href="#foo" />' },
{ code: '<a href="#javascript" />' },
{ code: '<a href="#javascriptFoo" />' },
{ 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 },
{ code: '<Link href="#foo" />', settings: componentsSettings },
// 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 SPECIAL LINK AND ASPECT
{ code: '<Anchor hrefLeft={undefined} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
{ code: '<Anchor hrefLeft={null} />', options: componentsAndSpecialLinkAndInvalidHrefAspect },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// 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: [invalidHrefexpectedError] },
{ code: '<a href={"#"} />', errors: [invalidHrefexpectedError] },
{ 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: [invalidHrefexpectedError], options: components },
{ code: '<Link href={"#"} />', errors: [invalidHrefexpectedError], 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: [invalidHrefexpectedError], options: components },
{ code: '<Anchor href={"#"} />', errors: [invalidHrefexpectedError], 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,
},
{
code: '<Link href="#" onClick={() => void 0} />',
errors: [preferButtonexpectedError],
settings: componentsSettings,
},
// 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: [invalidHrefexpectedError], options: specialLink },
{ code: '<a hrefLeft={"#"} />', errors: [invalidHrefexpectedError], 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 SPECIAL LINK 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: [invalidHrefexpectedError], options: componentsAndSpecialLink },
{ code: '<Anchor hrefLeft={"#"} />', errors: [invalidHrefexpectedError], 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: [noHrefexpectedError] },
{ code: '<a />', options: noHrefPreferButtonAspect, errors: [noHrefexpectedError] },
{ code: '<a />', options: noHrefInvalidHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href={undefined} />', options: noHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href={undefined} />', options: noHrefPreferButtonAspect, errors: [noHrefexpectedError] },
{ code: '<a href={undefined} />', options: noHrefInvalidHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href={null} />', options: noHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href={null} />', options: noHrefPreferButtonAspect, errors: [noHrefexpectedError] },
{ code: '<a href={null} />', options: noHrefInvalidHrefAspect, errors: [noHrefexpectedError] },
// INVALID HREF
{ code: '<a href="" />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="" />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="#" />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="#" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="#" />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href={"#"} />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href={"#"} />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href={"#"} />;', options: preferButtonInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="javascript:void(0)" />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href="javascript:void(0)" />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{
code: '<a href="javascript:void(0)" />;',
options: preferButtonInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{ code: '<a href={"javascript:void(0)"} />;', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{ code: '<a href={"javascript:void(0)"} />;', options: noHrefInvalidHrefAspect, errors: [invalidHrefexpectedError] },
{
code: '<a href={"javascript:void(0)"} />;',
options: preferButtonInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
// SHOULD BE BUTTON
{ code: '<a onClick={() => void 0} />', options: preferButtonAspect, errors: [preferButtonexpectedError] },
{
code: '<a onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonexpectedError],
},
{ code: '<a onClick={() => void 0} />', options: noHrefPreferButtonAspect, errors: [preferButtonexpectedError] },
{ code: '<a onClick={() => void 0} />', options: noHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a onClick={() => void 0} />', options: noHrefInvalidHrefAspect, errors: [noHrefexpectedError] },
{ code: '<a href="#" onClick={() => void 0} />', options: preferButtonAspect, errors: [preferButtonexpectedError] },
{
code: '<a href="#" onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href="#" onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonexpectedError],
},
{ code: '<a href="#" onClick={() => void 0} />', options: invalidHrefAspect, errors: [invalidHrefexpectedError] },
{
code: '<a href="#" onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: preferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: invalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{
code: '<a href="javascript:void(0)" onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: preferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: noHrefPreferButtonAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: preferButtonInvalidHrefAspect,
errors: [preferButtonexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: invalidHrefAspect,
errors: [invalidHrefexpectedError],
},
{
code: '<a href={"javascript:void(0)"} onClick={() => void 0} />',
options: noHrefInvalidHrefAspect,
errors: [invalidHrefexpectedError],
},
// CUSTOM COMPONENTS AND SPECIAL LINK AND ASPECT
{
code: '<Anchor hrefLeft={undefined} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefexpectedError],
},
{
code: '<Anchor hrefLeft={null} />',
options: componentsAndSpecialLinkAndNoHrefAspect,
errors: [noHrefexpectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,95 @@
/**
* @fileoverview Enforce elements with aria-activedescendant are tabbable.
* @author Jesse Beach <@jessebeach>
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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 have a tabindex',
type: 'JSXOpeningElement',
};
ruleTester.run('aria-activedescendant-has-tabindex', rule, {
valid: parsers.all([].concat(
{
code: '<CustomComponent />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={0} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={-1} />;',
},
{
code: '<CustomComponent aria-activedescendant={someID} tabIndex={0} />;',
settings: { 'jsx-a11y': { components: { CustomComponent: 'div' } } },
},
{
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={1} />;',
},
{
code: '<input aria-activedescendant={someID} tabIndex={0} />;',
},
{
code: '<input aria-activedescendant={someID} tabIndex={-1} />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex={-1} />;',
},
{
code: '<div aria-activedescendant={someID} tabIndex="-1" />;',
},
{
code: '<input aria-activedescendant={someID} tabIndex={-1} />;',
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{
code: '<div aria-activedescendant={someID} />;',
errors: [expectedError],
},
{
code: '<CustomComponent aria-activedescendant={someID} />;',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { CustomComponent: 'div' } } },
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,69 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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: parsers.all([].concat(
// 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 />' },
{ code: '<input type="text" aria-errormessage="foobar" />' },
)).concat(basicValidityTests).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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,311 @@
/**
* @fileoverview Enforce ARIA state and property values are valid.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { aria } from 'aria-query';
import { RuleTester } from 'eslint';
import test from 'tape';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/aria-proptypes';
const { validityCheck } = rule;
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = (name) => {
const {
type,
values: permittedValues,
} = aria.get(name.toLowerCase());
switch (type) {
case 'tristate':
return { message: `The value for ${name} must be a boolean or the string "mixed".` };
case 'token':
return { message: `The value for ${name} must be a single token from the following: ${permittedValues}.` };
case 'tokenlist':
return {
message: `The value for ${name} must be a list of one or more \
tokens from the following: ${permittedValues}.`,
};
case 'idlist':
return { message: `The value for ${name} must be a list of strings that represent DOM element IDs (idlist)` };
case 'id':
return { message: `The value for ${name} must be a string that represents a DOM element ID` };
case 'boolean':
case 'string':
case 'integer':
case 'number':
default:
return { message: `The value for ${name} must be a ${type}.` };
}
};
test('validityCheck', (t) => {
t.equal(
validityCheck(null, null),
false,
'is false for an unknown expected type',
);
t.end();
});
ruleTester.run('aria-proptypes', rule, {
valid: parsers.all([].concat(
// 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={null} />' },
{ code: '<div aria-hidden={undefined} />' },
{ 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: '<div aria-label={null} />' },
{ code: '<div aria-label={undefined} />' },
{ 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`} />' },
{ code: '<div aria-checked={null} />' },
{ code: '<div aria-checked={undefined} />' },
// 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} />' },
{ code: '<div aria-level={null} />' },
{ code: '<div aria-level={undefined} />' },
// 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} />' },
{ code: '<div aria-valuemax={null} />' },
{ code: '<div aria-valuemax={undefined} />' },
// 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" />' },
{ code: '<div aria-invalid={null} />' },
{ code: '<div aria-invalid={undefined} />' },
// 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} />' },
{ code: '<div aria-relevant={null} />' },
{ code: '<div aria-relevant={undefined} />' },
// ID
{ code: '<div aria-activedescendant="ascending" />' },
{ code: '<div aria-activedescendant="ASCENDING" />' },
{ code: '<div aria-activedescendant={"ascending"} />' },
{ code: '<div aria-activedescendant={`ascending`} />' },
{ code: '<div aria-activedescendant="descending" />' },
{ code: '<div aria-activedescendant={"descending"} />' },
{ code: '<div aria-activedescendant={`descending`} />' },
{ code: '<div aria-activedescendant="none" />' },
{ code: '<div aria-activedescendant={"none"} />' },
{ code: '<div aria-activedescendant={`none`} />' },
{ code: '<div aria-activedescendant="other" />' },
{ code: '<div aria-activedescendant={"other"} />' },
{ code: '<div aria-activedescendant={`other`} />' },
{ code: '<div aria-activedescendant={foo} />' },
{ code: '<div aria-activedescendant={foo.bar} />' },
{ code: '<div aria-activedescendant={null} />' },
{ code: '<div aria-activedescendant={undefined} />' },
// IDLIST
{ code: '<div aria-labelledby="additions" />' },
{ code: '<div aria-labelledby={"additions"} />' },
{ code: '<div aria-labelledby={`additions`} />' },
{ code: '<div aria-labelledby="additions removals" />' },
{ code: '<div aria-labelledby="additions additions" />' },
{ code: '<div aria-labelledby={"additions removals"} />' },
{ code: '<div aria-labelledby={`additions removals`} />' },
{ code: '<div aria-labelledby="additions removals text" />' },
{ code: '<div aria-labelledby={"additions removals text"} />' },
{ code: '<div aria-labelledby={`additions removals text`} />' },
{ code: '<div aria-labelledby="additions removals text all" />' },
{ code: '<div aria-labelledby={"additions removals text all"} />' },
{ code: '<div aria-labelledby={`removals additions text all`} />' },
{ code: '<div aria-labelledby={foo} />' },
{ code: '<div aria-labelledby={foo.bar} />' },
{ code: '<div aria-labelledby={null} />' },
{ code: '<div aria-labelledby={undefined} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// BOOLEAN
{ 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 />', 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="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="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="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={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={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,119 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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 roleKeys = [...roles.keys()];
const validRoles = roleKeys.filter((role) => roles.get(role).abstract === false);
const invalidRoles = roleKeys.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 = { ...test };
invalidTest.errors = [errorMessage];
return invalidTest;
});
const allowedInvalidRoles = [{
allowedInvalidRoles: ['invalid-role', 'other-invalid-role'],
}];
const ignoreNonDOMSchema = [{
ignoreNonDOM: true,
}];
const customDivSettings = {
'jsx-a11y': {
polymorphicPropName: 'asChild',
components: {
Div: 'div',
},
},
};
ruleTester.run('aria-role', rule, {
valid: parsers.all([].concat(
// 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: '<img role="invalid-role" />', options: allowedInvalidRoles },
{ code: '<img role="invalid-role tabpanel" />', options: allowedInvalidRoles },
{ code: '<img role="invalid-role other-invalid-role" />', options: allowedInvalidRoles },
{ code: '<Foo role="bar" />', options: ignoreNonDOMSchema },
{ code: '<fakeDOM role="bar" />', options: ignoreNonDOMSchema },
{ code: '<img role="presentation" />', options: ignoreNonDOMSchema },
{
code: '<Div role="button" />',
settings: customDivSettings,
},
{
code: '<Box asChild="div" role="button" />',
settings: customDivSettings,
},
{
code: '<svg role="graphics-document document" />',
},
)).concat(validTests).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div role="foobar" />', errors: [errorMessage] },
{ code: '<div role="datepicker"></div>', errors: [errorMessage] },
{ code: '<div role="range"></div>', errors: [errorMessage] },
{ code: '<div role="Button"></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="unknown-invalid-role" />', errors: [errorMessage], options: allowedInvalidRoles },
{ code: '<div role={null}></div>', errors: [errorMessage] },
{ code: '<Foo role="datepicker" />', errors: [errorMessage] },
{ code: '<Foo role="Button" />', errors: [errorMessage] },
{ code: '<Div role="Button" />', errors: [errorMessage], settings: customDivSettings },
{
code: '<Div role="Button" />',
errors: [errorMessage],
options: ignoreNonDOMSchema,
settings: customDivSettings,
},
{
code: '<Box asChild="div" role="Button" />',
settings: customDivSettings,
errors: [errorMessage],
},
)).concat(invalidTests).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,75 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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} />`,
};
}).concat({
code: '<fake aria-hidden />',
errors: [errorMessage('aria-hidden')],
});
// Generate invalid test cases.
const invalidRoleValidityTests = domElements
.filter((element) => dom.get(element).reserved)
.map((reservedElem) => ({
code: `<${reservedElem} role {...props} />`,
errors: [errorMessage('role')],
})).concat({
code: '<Meta aria-hidden />',
errors: [errorMessage('aria-hidden')],
settings: { 'jsx-a11y': { components: { Meta: 'meta' } } },
});
const invalidAriaValidityTests = domElements
.filter((element) => dom.get(element).reserved)
.map((reservedElem) => ({
code: `<${reservedElem} aria-hidden aria-role="none" {...props} />`,
errors: [errorMessage('aria-hidden')],
}));
ruleTester.run('aria-unsupported-elements', rule, {
valid: parsers.all([].concat(roleValidityTests, ariaValidityTests)).map(parserOptionsMapper),
invalid: parsers.all([].concat(invalidRoleValidityTests, invalidAriaValidityTests))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,77 @@
/**
* @fileoverview Ensure autocomplete attribute is correct.
* @author Wilco Fiers
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import { axeFailMessage } from '../../__util__/axeMapping';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/autocomplete-valid';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const invalidAutocomplete = [{
message: axeFailMessage('autocomplete-valid'),
type: 'JSXOpeningElement',
}];
const inappropriateAutocomplete = [{
message: axeFailMessage('autocomplete-appropriate'),
type: 'JSXOpeningElement',
}];
const componentsSettings = {
'jsx-a11y': {
components: {
Input: 'input',
},
},
};
ruleTester.run('autocomplete-valid', rule, {
valid: parsers.all([].concat(
// INAPPLICABLE
{ code: '<input type="text" />;' },
// // PASSED AUTOCOMPLETE
{ code: '<input type="text" autocomplete="name" />;' },
{ code: '<input type="text" autocomplete="" />;' },
{ code: '<input type="text" autocomplete="off" />;' },
{ code: '<input type="text" autocomplete="on" />;' },
{ code: '<input type="text" autocomplete="billing family-name" />;' },
{ code: '<input type="text" autocomplete="section-blue shipping street-address" />;' },
{ code: '<input type="text" autocomplete="section-somewhere shipping work email" />;' },
{ code: '<input type="text" autocomplete />;' },
{ code: '<input type="text" autocomplete={autocompl} />;' },
{ code: '<input type="text" autocomplete={autocompl || "name"} />;' },
{ code: '<input type="text" autocomplete={autocompl || "foo"} />;' },
{ code: '<Foo autocomplete="bar"></Foo>;' },
{ code: '<input type={isEmail ? "email" : "text"} autocomplete="none" />;' },
{ code: '<Input type="text" autocomplete="name" />', settings: componentsSettings },
{ code: '<Input type="text" autocomplete="baz" />' },
// PASSED "autocomplete-appropriate"
// see also: https://github.com/dequelabs/axe-core/issues/2912
{ code: '<input type="date" autocomplete="email" />;', errors: inappropriateAutocomplete },
{ code: '<input type="number" autocomplete="url" />;', errors: inappropriateAutocomplete },
{ code: '<input type="month" autocomplete="tel" />;', errors: inappropriateAutocomplete },
{ code: '<Foo type="month" autocomplete="tel"></Foo>;', errors: inappropriateAutocomplete, options: [{ inputComponents: ['Foo'] }] },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// FAILED "autocomplete-valid"
{ code: '<input type="text" autocomplete="foo" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="name invalid" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="invalid name" />;', errors: invalidAutocomplete },
{ code: '<input type="text" autocomplete="home url" />;', errors: invalidAutocomplete },
{ code: '<Bar autocomplete="baz"></Bar>;', errors: invalidAutocomplete, options: [{ inputComponents: ['Bar'] }] },
{ code: '<Input type="text" autocomplete="baz" />', errors: invalidAutocomplete, settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,77 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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: parsers.all([].concat(
{ 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: '<div onClick={() => void 0} role="presentation" />;' },
{ code: '<div onClick={() => void 0} role="none" />;' },
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
{ code: '<Footer onClick={doFoo} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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] },
{ code: '<Footer onClick={doFoo} />', errors: [expectedError], settings: { 'jsx-a11y': { components: { Footer: 'footer' } } } },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,327 @@
/**
* @fileoverview Control elements must be associated with a text label
* @author jessebeach
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import { configs } from '../../../src/index';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
import rule from '../../../src/rules/control-has-associated-label';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const ruleName = 'jsx-a11y/control-has-associated-label';
const expectedError = {
message: 'A control must be associated with a text label.',
type: 'JSXOpeningElement',
};
const alwaysValid = [
// Custom Control Components
{ code: '<CustomControl><span><span>Save</span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }] },
{ code: '<CustomControl><span><span label="Save"></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'], labelAttributes: ['label'] }] },
{ code: '<CustomControl>Save</CustomControl>', settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } },
// Interactive Elements
{ code: '<button>Save</button>' },
{ code: '<button><span>Save</span></button>' },
{ code: '<button><span><span>Save</span></span></button>', options: [{ depth: 3 }] },
{ code: '<button><span><span><span><span><span><span><span><span>Save</span></span></span></span></span></span></span></span></button>', options: [{ depth: 9 }] },
{ code: '<button><img alt="Save" /></button>' },
{ code: '<button aria-label="Save" />' },
{ code: '<button><span aria-label="Save" /></button>' },
{ code: '<button aria-labelledby="js_1" />' },
{ code: '<button><span aria-labelledby="js_1" /></button>' },
{ code: '<button>{sureWhyNot}</button>' },
{ code: '<button><span><span label="Save"></span></span></button>', options: [{ depth: 3, labelAttributes: ['label'] }] },
{ code: '<a href="#">Save</a>' },
{ code: '<area href="#">Save</area>' },
{ code: '<link>Save</link>' },
{ code: '<menuitem>Save</menuitem>' },
{ code: '<option>Save</option>' },
{ code: '<th>Save</th>' },
// Interactive Roles
{ code: '<div role="button">Save</div>' },
{ code: '<div role="checkbox">Save</div>' },
{ code: '<div role="columnheader">Save</div>' },
{ code: '<div role="combobox">Save</div>' },
{ code: '<div role="gridcell">Save</div>' },
{ code: '<div role="link">Save</div>' },
{ code: '<div role="menuitem">Save</div>' },
{ code: '<div role="menuitemcheckbox">Save</div>' },
{ code: '<div role="menuitemradio">Save</div>' },
{ code: '<div role="option">Save</div>' },
{ code: '<div role="progressbar">Save</div>' },
{ code: '<div role="radio">Save</div>' },
{ code: '<div role="rowheader">Save</div>' },
{ code: '<div role="searchbox">Save</div>' },
{ code: '<div role="slider">Save</div>' },
{ code: '<div role="spinbutton">Save</div>' },
{ code: '<div role="switch">Save</div>' },
{ code: '<div role="tab">Save</div>' },
{ code: '<div role="textbox">Save</div>' },
{ code: '<div role="treeitem">Save</div>' },
{ code: '<div role="button" aria-label="Save" />' },
{ code: '<div role="checkbox" aria-label="Save" />' },
{ code: '<div role="columnheader" aria-label="Save" />' },
{ code: '<div role="combobox" aria-label="Save" />' },
{ code: '<div role="gridcell" aria-label="Save" />' },
{ code: '<div role="link" aria-label="Save" />' },
{ code: '<div role="menuitem" aria-label="Save" />' },
{ code: '<div role="menuitemcheckbox" aria-label="Save" />' },
{ code: '<div role="menuitemradio" aria-label="Save" />' },
{ code: '<div role="option" aria-label="Save" />' },
{ code: '<div role="progressbar" aria-label="Save" />' },
{ code: '<div role="radio" aria-label="Save" />' },
{ code: '<div role="rowheader" aria-label="Save" />' },
{ code: '<div role="searchbox" aria-label="Save" />' },
{ code: '<div role="slider" aria-label="Save" />' },
{ code: '<div role="spinbutton" aria-label="Save" />' },
{ code: '<div role="switch" aria-label="Save" />' },
{ code: '<div role="tab" aria-label="Save" />' },
{ code: '<div role="textbox" aria-label="Save" />' },
{ code: '<div role="treeitem" aria-label="Save" />' },
{ code: '<div role="button" aria-labelledby="js_1" />' },
{ code: '<div role="checkbox" aria-labelledby="js_1" />' },
{ code: '<div role="columnheader" aria-labelledby="js_1" />' },
{ code: '<div role="combobox" aria-labelledby="js_1" />' },
{ code: '<div role="gridcell" aria-labelledby="Save" />' },
{ code: '<div role="link" aria-labelledby="js_1" />' },
{ code: '<div role="menuitem" aria-labelledby="js_1" />' },
{ code: '<div role="menuitemcheckbox" aria-labelledby="js_1" />' },
{ code: '<div role="menuitemradio" aria-labelledby="js_1" />' },
{ code: '<div role="option" aria-labelledby="js_1" />' },
{ code: '<div role="progressbar" aria-labelledby="js_1" />' },
{ code: '<div role="radio" aria-labelledby="js_1" />' },
{ code: '<div role="rowheader" aria-labelledby="js_1" />' },
{ code: '<div role="searchbox" aria-labelledby="js_1" />' },
{ code: '<div role="slider" aria-labelledby="js_1" />' },
{ code: '<div role="spinbutton" aria-labelledby="js_1" />' },
{ code: '<div role="switch" aria-labelledby="js_1" />' },
{ code: '<div role="tab" aria-labelledby="js_1" />' },
{ code: '<div role="textbox" aria-labelledby="js_1" />' },
{ code: '<div role="treeitem" aria-labelledby="js_1" />' },
// Non-interactive Elements
{ code: '<abbr />' },
{ code: '<article />' },
{ code: '<blockquote />' },
{ code: '<br />' },
{ code: '<caption />' },
{ code: '<dd />' },
{ code: '<details />' },
{ code: '<dfn />' },
{ code: '<dialog />' },
{ code: '<dir />' },
{ code: '<dl />' },
{ code: '<dt />' },
{ code: '<fieldset />' },
{ code: '<figcaption />' },
{ code: '<figure />' },
{ code: '<footer />' },
{ code: '<form />' },
{ code: '<frame />' },
{ code: '<h1 />' },
{ code: '<h2 />' },
{ code: '<h3 />' },
{ code: '<h4 />' },
{ code: '<h5 />' },
{ code: '<h6 />' },
{ code: '<hr />' },
{ code: '<iframe />' },
{ code: '<img />' },
{ code: '<label />' },
{ code: '<legend />' },
{ code: '<li />' },
{ code: '<link />' },
{ code: '<main />' },
{ code: '<mark />' },
{ code: '<marquee />' },
{ code: '<menu />' },
{ code: '<meter />' },
{ code: '<nav />' },
{ code: '<ol />' },
{ code: '<p />' },
{ code: '<pre />' },
{ code: '<progress />' },
{ code: '<ruby />' },
{ code: '<section />' },
{ code: '<table />' },
{ code: '<tbody />' },
{ code: '<td />' },
{ code: '<tfoot />' },
{ code: '<thead />' },
{ code: '<time />' },
{ code: '<ul />' },
// Non-interactive Roles
{ 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="none" />' },
{ code: '<div role="note" />' },
{ code: '<div role="presentation" />' },
{ code: '<div role="region" />' },
{ code: '<div role="rowgroup" />' },
{ code: '<div role="search" />' },
{ code: '<div role="separator" />' },
{ code: '<div role="status" />' },
{ code: '<div role="table" />' },
{ code: '<div role="tabpanel" />' },
{ code: '<div role="term" />' },
{ code: '<div role="timer" />' },
{ code: '<div role="tooltip" />' },
// Via config
// Inputs. Ignore them because they might get a label from a wrapping label element.
{ code: '<input />' },
{ code: '<input type="button" />' },
{ code: '<input type="checkbox" />' },
{ code: '<input type="color" />' },
{ code: '<input type="date" />' },
{ code: '<input type="datetime" />' },
{ code: '<input type="email" />' },
{ code: '<input type="file" />' },
{ code: '<input type="hidden" />' },
{ code: '<input type="hidden" name="bot-field"/>' },
{ code: '<input type="hidden" name="form-name" value="Contact Form"/>' },
{ code: '<input type="image" />' },
{ code: '<input type="month" />' },
{ code: '<input type="number" />' },
{ code: '<input type="password" />' },
{ code: '<input type="radio" />' },
{ code: '<input type="range" />' },
{ code: '<input type="reset" />' },
{ code: '<input type="search" />' },
{ code: '<input type="submit" />' },
{ code: '<input type="tel" />' },
{ code: '<input type="text" />' },
{ code: '<label>Foo <input type="text" /></label>' },
{ code: '<input name={field.name} id="foo" type="text" value={field.value} disabled={isDisabled} onChange={changeText(field.onChange, field.name)} onBlur={field.onBlur} />' },
{ code: '<input type="time" />' },
{ code: '<input type="url" />' },
{ code: '<input type="week" />' },
// Marginal interactive elements. It is difficult to insist that these
// elements contain a text label.
{ code: '<audio />' },
{ code: '<canvas />' },
{ code: '<embed />' },
{ code: '<textarea />' },
{ code: '<tr />' },
{ code: '<video />' },
// Interactive roles to ignore
{ code: '<div role="grid" />' },
{ code: '<div role="listbox" />' },
{ code: '<div role="menu" />' },
{ code: '<div role="menubar" />' },
{ code: '<div role="radiogroup" />' },
{ code: '<div role="row" />' },
{ code: '<div role="tablist" />' },
{ code: '<div role="toolbar" />' },
{ code: '<div role="tree" />' },
{ code: '<div role="treegrid" />' },
];
const neverValid = [
{ code: '<button />', errors: [expectedError] },
{ code: '<button><span /></button>', errors: [expectedError] },
{ code: '<button><img /></button>', errors: [expectedError] },
{ code: '<button><span title="This is not a real label" /></button>', errors: [expectedError] },
{ code: '<button><span><span><span>Save</span></span></span></button>', options: [{ depth: 3 }], errors: [expectedError] },
{ code: '<CustomControl><span><span></span></span></CustomControl>', options: [{ depth: 3, controlComponents: ['CustomControl'] }], errors: [expectedError] },
{ code: '<CustomControl></CustomControl>', errors: [expectedError], settings: { 'jsx-a11y': { components: { CustomControl: 'button' } } } },
{ code: '<a href="#" />', errors: [expectedError] },
{ code: '<area href="#" />', errors: [expectedError] },
{ code: '<menuitem />', errors: [expectedError] },
{ code: '<option />', errors: [expectedError] },
{ code: '<th />', errors: [expectedError] },
// Interactive Roles
{ code: '<div role="button" />', errors: [expectedError] },
{ code: '<div role="checkbox" />', errors: [expectedError] },
{ code: '<div role="columnheader" />', errors: [expectedError] },
{ code: '<div role="combobox" />', errors: [expectedError] },
{ code: '<div role="link" />', errors: [expectedError] },
{ code: '<div role="gridcell" />', errors: [expectedError] },
{ code: '<div role="menuitem" />', errors: [expectedError] },
{ code: '<div role="menuitemcheckbox" />', errors: [expectedError] },
{ code: '<div role="menuitemradio" />', errors: [expectedError] },
{ code: '<div role="option" />', errors: [expectedError] },
{ code: '<div role="progressbar" />', errors: [expectedError] },
{ code: '<div role="radio" />', errors: [expectedError] },
{ code: '<div role="rowheader" />', errors: [expectedError] },
{ code: '<div role="scrollbar" />', errors: [expectedError] },
{ code: '<div role="searchbox" />', errors: [expectedError] },
{ code: '<div role="slider" />', errors: [expectedError] },
{ code: '<div role="spinbutton" />', errors: [expectedError] },
{ code: '<div role="switch" />', errors: [expectedError] },
{ code: '<div role="tab" />', errors: [expectedError] },
{ code: '<div role="textbox" />', errors: [expectedError] },
];
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
const strictOptions = (configs.strict.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:no-config`, rule, {
valid: parsers.all([].concat(
{ code: '<input type="hidden" />' },
{ code: '<input type="text" aria-hidden="true" />' },
))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<input type="text" />', errors: [expectedError] },
))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,85 @@
/**
* @fileoverview Enforce heading (h1, h2, etc) elements contain accessible content.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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',
};
const components = [{
components: ['Heading', 'Title'],
}];
const componentsSettings = {
'jsx-a11y': {
components: {
CustomInput: 'input',
Title: 'h1',
Heading: 'h2',
},
},
};
ruleTester.run('heading-has-content', rule, {
valid: parsers.all([].concat(
// DEFAULT ELEMENT TESTS
{ 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} />' },
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
{ code: '<Heading>Foo</Heading>', options: components },
{ code: '<Title>Foo</Title>', options: components },
{ code: '<Heading><Bar /></Heading>', options: components },
{ code: '<Heading>{foo}</Heading>', options: components },
{ code: '<Heading>{foo.bar}</Heading>', options: components },
{ code: '<Heading dangerouslySetInnerHTML={{ __html: "foo" }} />', options: components },
{ code: '<Heading children={children} />', options: components },
{ code: '<h1 aria-hidden />' },
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
{ code: '<Heading>Foo</Heading>', settings: componentsSettings },
{ code: '<h1><CustomInput type="hidden" /></h1>' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// DEFAULT ELEMENT TESTS
{ code: '<h1 />', errors: [expectedError] },
{ code: '<h1><Bar aria-hidden /></h1>', errors: [expectedError] },
{ code: '<h1>{undefined}</h1>', errors: [expectedError] },
{ code: '<h1><input type="hidden" /></h1>', errors: [expectedError] },
// CUSTOM ELEMENT TESTS FOR COMPONENTS OPTION
{ code: '<Heading />', errors: [expectedError], options: components },
{ code: '<Heading><Bar aria-hidden /></Heading>', errors: [expectedError], options: components },
{ code: '<Heading>{undefined}</Heading>', errors: [expectedError], options: components },
// CUSTOM ELEMENT TESTS FOR COMPONENTS SETTINGS
{ code: '<Heading />', errors: [expectedError], settings: componentsSettings },
{ code: '<h1><CustomInput type="hidden" /></h1>', errors: [expectedError], settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,42 @@
/**
* @fileoverview Enforce html element has lang prop.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<html lang="en" />' },
{ code: '<html lang="en-US" />' },
{ code: '<html lang={foo} />' },
{ code: '<html lang />' },
{ code: '<HTML />' },
{ code: '<HTMLTop lang="en" />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<html />', errors: [expectedError] },
{ code: '<html {...props} />', errors: [expectedError] },
{ code: '<html lang={undefined} />', errors: [expectedError] },
{ code: '<HTMLTop />', errors: [expectedError], settings: { 'jsx-a11y': { components: { HTMLTop: 'html' } } } },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,55 @@
/**
* @fileoverview Enforce iframe elements have a title attribute.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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',
};
const componentsSettings = {
'jsx-a11y': {
components: {
FooComponent: 'iframe',
},
},
};
ruleTester.run('html-has-lang', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<iframe title="Unique title" />' },
{ code: '<iframe title={foo} />' },
{ code: '<FooComponent />' },
{ code: '<FooComponent title="Unique title" />', settings: componentsSettings },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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] },
{ code: '<FooComponent />', errors: [expectedError], settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,137 @@
/**
* @fileoverview Enforce img alt attribute does not have the word image, picture, or photo.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import semver from 'semver';
import { version as eslintVersion } from 'eslint/package.json';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/img-redundant-alt';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const array = [{
components: ['Image'],
words: ['Word1', 'Word2'],
}];
const componentsSettings = {
'jsx-a11y': {
components: {
Image: 'img',
},
},
};
const ruleTester = new RuleTester();
const expectedError = {
message: 'Redundant alt attribute. Screen-readers already announce `img` tags as an image. You dont 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: parsers.all([].concat(
{ 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 />' },
{ code: '<img alt={imageAlt} />' },
{ code: '<img alt={imageAlt.name} />' },
semver.satisfies(eslintVersion, '>= 6') ? [
{ code: '<img alt={imageAlt?.name} />', languageOptions: { ecmaVersion: 2020 } },
{ code: '<img alt="Doing cool things" aria-hidden={foo?.bar}/>', languageOptions: { ecmaVersion: 2020 } },
] : [],
{ code: '<img alt="Photography" />;' },
{ code: '<img alt="ImageMagick" />;' },
{ code: '<Image alt="Photo of a friend" />' },
{ code: '<Image alt="Foo" />', settings: componentsSettings },
{ code: '<img alt="画像" />', options: [{ words: ['イメージ'] }] },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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],
},
{ code: '<Image alt="Photo of a friend" />', errors: [expectedError], settings: componentsSettings },
// 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] },
{ code: '<img alt="イメージ" />', options: [{ words: ['イメージ'] }], errors: [expectedError] },
{ code: '<img alt="イメージです" />', options: [{ words: ['イメージ'] }], errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,267 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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 componentsSettings = {
'jsx-a11y': {
components: {
Div: 'div',
},
},
};
const buttonError = { message: tabbableTemplate('button'), type };
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: '<summary onClick={() => void 0} />' },
{ 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="tablist" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="tab" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="textbox" tabIndex="0" onClick={() => void 0} />' },
{ code: '<div role="textbox" aria-disabled="true" onClick={() => void 0} />' },
{ code: '<Foo.Bar onClick={() => void 0} aria-hidden={false} />;' },
{ code: '<Input onClick={() => void 0} type="hidden" />;' },
{ code: '<Div onClick={() => void 0} role="button" tabIndex="0" />', settings: componentsSettings },
];
const neverValid = [
{ code: '<Div onClick={() => void 0} role="button" />', errors: [buttonError], settings: componentsSettings },
];
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: parsers.all([].concat(
...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: parsers.all([].concat(
...neverValid,
...failReducer(recommendedRoles, triggeringHandlers, tabbableTemplate),
...failReducer(
interactiveRoles.filter((role) => !includes(recommendedRoles, role)),
triggeringHandlers,
focusableTemplate,
),
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...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: parsers.all([].concat(
...neverValid,
...failReducer(strictRoles, triggeringHandlers, tabbableTemplate),
...failReducer(
interactiveRoles.filter((role) => !includes(strictRoles, role)),
triggeringHandlers,
focusableTemplate,
),
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,241 @@
/**
* @fileoverview Enforce label tags have an associated control.
* @author Jesse Beach
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/label-has-associated-control';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const ruleName = 'label-has-associated-control';
const expectedError = {
message: 'A form label must be associated with a control.',
type: 'JSXOpeningElement',
};
const expectedErrorNoLabel = {
message: 'A form label must have accessible text.',
type: 'JSXOpeningElement',
};
const componentsSettings = {
'jsx-a11y': {
components: {
CustomInput: 'input',
CustomLabel: 'label',
},
},
};
const attributesSettings = {
'jsx-a11y': {
attributes: {
for: ['htmlFor', 'for'],
},
},
};
const htmlForValid = [
{ code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }] },
{ code: '<label htmlFor="js_id" aria-label="A label" />' },
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />' },
{ code: '<div><label htmlFor="js_id">A label</label><input id="js_id" /></div>' },
{ code: '<label for="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], settings: attributesSettings },
{ code: '<label for="js_id" aria-label="A label" />', settings: attributesSettings },
{ code: '<label for="js_id" aria-labelledby="A label" />', settings: attributesSettings },
{ code: '<div><label for="js_id">A label</label><input id="js_id" /></div>', settings: attributesSettings },
// Custom label component.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', settings: componentsSettings },
// Custom label attributes.
{ code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }] },
// Glob support for controlComponents option.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ controlComponents: ['Custom*'] }] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ controlComponents: ['*Label'] }] },
// Rule does not error if presence of accessible label cannot be determined
{ code: '<div><label htmlFor="js_id"><CustomText /></label><input id="js_id" /></div>' },
];
const nestingValid = [
{ code: '<label>A label<input /></label>' },
{ code: '<label>A label<textarea /></label>' },
{ code: '<label><img alt="A label" /><input /></label>' },
{ code: '<label><img aria-label="A label" /><input /></label>' },
{ code: '<label><span>A label<input /></span></label>' },
{ code: '<label><span><span>A label<input /></span></span></label>', options: [{ depth: 3 }] },
{ code: '<label><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }] },
{ code: '<label><span><span><span><span>A label</span><input /></span></span></span></label>', options: [{ depth: 5 }] },
{ code: '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>', options: [{ depth: 5 }] },
{ code: '<label><span><span><span><input aria-label="A label" /></span></span></span></label>', options: [{ depth: 5 }] },
// Other controls
{ code: '<label>foo<meter /></label>' },
{ code: '<label>foo<output /></label>' },
{ code: '<label>foo<progress /></label>' },
{ code: '<label>foo<textarea /></label>' },
// Custom controlComponents.
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }] },
{ code: '<label><span>A label<CustomInput /></span></label>', settings: componentsSettings },
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }] },
// Glob support for controlComponents option.
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['Custom*'] }] },
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['*Input'] }] },
// Rule does not error if presence of accessible label cannot be determined
{ code: '<label><CustomText /><input /></label>' },
];
const bothValid = [
{ code: '<label htmlFor="js_id"><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }] },
{ code: '<label htmlFor="js_id" aria-label="A label"><input /></label>' },
{ code: '<label htmlFor="js_id" aria-labelledby="A label"><input /></label>' },
{ code: '<label htmlFor="js_id" aria-labelledby="A label"><textarea /></label>' },
// Custom label component.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', options: [{ labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" label="A label"><input /></CustomLabel>', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><input /></CustomLabel>', settings: componentsSettings },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label"><CustomInput /></CustomLabel>', settings: componentsSettings },
// Custom label attributes.
{ code: '<label htmlFor="js_id" label="A label"><input /></label>', options: [{ labelAttributes: ['label'] }] },
{ code: '<label htmlFor="selectInput">Some text<select id="selectInput" /></label>' },
];
const alwaysValid = [
{ code: '<div />' },
{ code: '<CustomElement />' },
{ code: '<input type="hidden" />' },
];
const htmlForInvalid = [
{ code: '<label htmlFor="js_id"><span><span><span>A label</span></span></span></label>', options: [{ depth: 4 }], errors: [expectedError] },
{ code: '<label htmlFor="js_id" aria-label="A label" />', errors: [expectedError] },
{ code: '<label htmlFor="js_id" aria-labelledby="A label" />', errors: [expectedError] },
// Custom label component.
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel htmlFor="js_id" aria-label="A label" />', settings: componentsSettings, errors: [expectedError] },
// Custom label attributes.
{ code: '<label htmlFor="js_id" label="A label" />', options: [{ labelAttributes: ['label'] }], errors: [expectedError] },
];
const nestingInvalid = [
{ code: '<label>A label<input /></label>', errors: [expectedError] },
{ code: '<label>A label<textarea /></label>', errors: [expectedError] },
{ code: '<label><img alt="A label" /><input /></label>', errors: [expectedError] },
{ code: '<label><img aria-label="A label" /><input /></label>', errors: [expectedError] },
{ code: '<label><span>A label<input /></span></label>', errors: [expectedError] },
{ code: '<label><span><span>A label<input /></span></span></label>', options: [{ depth: 3 }], errors: [expectedError] },
{ code: '<label><span><span><span>A label<input /></span></span></span></label>', options: [{ depth: 4 }], errors: [expectedError] },
{ code: '<label><span><span><span><span>A label</span><input /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
{ code: '<label><span><span><span><span aria-label="A label" /><input /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
{ code: '<label><span><span><span><input aria-label="A label" /></span></span></span></label>', options: [{ depth: 5 }], errors: [expectedError] },
// Custom controlComponents.
{ code: '<label><span>A label<CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedError] },
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel><span label="A label"><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }], errors: [expectedError] },
{ code: '<label><span>A label<CustomInput /></span></label>', settings: componentsSettings, errors: [expectedError] },
{ code: '<CustomLabel><span>A label<CustomInput /></span></CustomLabel>', settings: componentsSettings, errors: [expectedError] },
];
const neverValid = [
{ code: '<label htmlFor="js_id" />', errors: [expectedErrorNoLabel] },
{ code: '<label htmlFor="js_id"><input /></label>', errors: [expectedErrorNoLabel] },
{ code: '<label htmlFor="js_id"><textarea /></label>', errors: [expectedErrorNoLabel] },
{ code: '<label></label>', errors: [expectedErrorNoLabel] },
{ code: '<label>A label</label>', errors: [expectedError] },
{ code: '<div><label /><input /></div>', errors: [expectedErrorNoLabel] },
{ code: '<div><label>A label</label><input /></div>', errors: [expectedError] },
// Custom label component.
{ code: '<CustomLabel aria-label="A label" />', options: [{ labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel label="A label" />', options: [{ labelAttributes: ['label'], labelComponents: ['CustomLabel'] }], errors: [expectedError] },
{ code: '<CustomLabel aria-label="A label" />', settings: componentsSettings, errors: [expectedError] },
// Custom label attributes.
{ code: '<label label="A label" />', options: [{ labelAttributes: ['label'] }], errors: [expectedError] },
// Custom controlComponents.
{ code: '<label><span><CustomInput /></span></label>', options: [{ controlComponents: ['CustomInput'] }], errors: [expectedErrorNoLabel] },
{ code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'] }], errors: [expectedErrorNoLabel] },
{ code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', options: [{ controlComponents: ['CustomInput'], labelComponents: ['CustomLabel'], labelAttributes: ['label'] }], errors: [expectedErrorNoLabel] },
{ code: '<label><span><CustomInput /></span></label>', settings: componentsSettings, errors: [expectedErrorNoLabel] },
{ code: '<CustomLabel><span><CustomInput /></span></CustomLabel>', settings: componentsSettings, errors: [expectedErrorNoLabel] },
];
// htmlFor valid
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...htmlForValid,
))
.map(ruleOptionsMapperFactory({
assert: 'htmlFor',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
...nestingInvalid,
))
.map(ruleOptionsMapperFactory({
assert: 'htmlFor',
}))
.map(parserOptionsMapper),
});
// nesting valid
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...nestingValid,
))
.map(ruleOptionsMapperFactory({
assert: 'nesting',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
...htmlForInvalid,
))
.map(ruleOptionsMapperFactory({
assert: 'nesting',
}))
.map(parserOptionsMapper),
});
// either valid
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...htmlForValid,
...nestingValid,
))
.map(ruleOptionsMapperFactory({
assert: 'either',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
)).map(parserOptionsMapper),
});
// both valid
ruleTester.run(ruleName, rule, {
valid: parsers.all([].concat(
...alwaysValid,
...bothValid,
))
.map(ruleOptionsMapperFactory({
assert: 'both',
}))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,235 @@
/**
* @fileoverview Enforce label tags have htmlFor attribute.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import assign from 'object.assign';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/label-has-for';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedNestingError = {
message: 'Form label must have the following type of associated control: nesting',
type: 'JSXOpeningElement',
};
const expectedSomeError = {
message: 'Form label must have ANY of the following types of associated control: nesting, id',
type: 'JSXOpeningElement',
};
const expectedEveryError = {
message: 'Form label must have ALL of the following types of associated control: nesting, id',
type: 'JSXOpeningElement',
};
const optionsComponents = [{
components: ['Label', 'Descriptor'],
}];
const optionsRequiredNesting = [{
required: 'nesting',
}];
const optionsRequiredSome = [{
required: { some: ['nesting', 'id'] },
}];
const optionsRequiredEvery = [{
required: { every: ['nesting', 'id'] },
}];
const optionsChildrenAllowed = [{
allowChildren: true,
}];
const attributesSettings = {
'jsx-a11y': {
attributes: {
for: ['htmlFor', 'for'],
},
},
};
ruleTester.run('label-has-for', rule, {
valid: parsers.all([].concat(
// DEFAULT ELEMENT 'label' TESTS
{ code: '<div />' },
{ code: '<label htmlFor="foo"><input /></label>' },
{ code: '<label htmlFor="foo"><textarea /></label>' },
{ code: '<label for="foo"><input /></label>', settings: attributesSettings },
{ code: '<label for="foo"><textarea /></label>', settings: attributesSettings },
{ code: '<Label />' }, // lower-case convention refers to real HTML elements.
{ code: '<Label htmlFor="foo" />' },
{ code: '<Label for="foo" />', settings: attributesSettings },
{ code: '<Descriptor />' },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>' },
{ code: '<Descriptor for="foo">Test!</Descriptor>', settings: attributesSettings },
{ code: '<UX.Layout>test</UX.Layout>' },
// CUSTOM ELEMENT ARRAY OPTION TESTS
{ code: '<Label htmlFor="foo" />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={"foo"} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={foo} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Label htmlFor={`${id}`} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<div />', options: optionsComponents },
{ code: '<Label htmlFor="something"><input /></Label>', options: optionsComponents },
{ code: '<Label htmlFor="foo">Test!</Label>', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor="foo" />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={"foo"} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={foo} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor={`${id}`} />', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: [assign({}, optionsComponents[0], optionsRequiredSome[0])] },
{ code: '<label htmlFor="foo" />', options: optionsRequiredSome },
{ code: '<label htmlFor={"foo"} />', options: optionsRequiredSome },
{ code: '<label htmlFor={foo} />', options: optionsRequiredSome },
{ code: '<label htmlFor={`${id}`} />', options: optionsRequiredSome },
{ code: '<label htmlFor="foo">Test!</label>', options: optionsRequiredSome },
{ code: '<label><input /></label>', options: optionsRequiredSome },
{ code: '<label><input /></label>', options: optionsRequiredNesting },
{ code: '<label htmlFor="input"><input /></label>', options: optionsRequiredEvery },
{ code: '<label><input /></label>', options: optionsChildrenAllowed },
{ code: '<Descriptor htmlFor="foo">Test!</Descriptor>', options: [assign({}, optionsComponents, optionsChildrenAllowed)] },
{ code: '<label>Test!</label>', options: optionsChildrenAllowed },
{ code: '<label htmlFor="foo">Test!</label>', options: optionsChildrenAllowed },
{ code: '<label>{children}</label>', options: optionsChildrenAllowed },
{ code: '<label htmlFor="children">{children}</label>', options: optionsChildrenAllowed },
{ code: '<label htmlFor={id}>{ labelText }<div><input id={id} type="checkbox" name={id} value={value} /></div></label>', options: optionsRequiredEvery },
{ code: '<label htmlFor={id}>{ labelText }<div><div><div><div><input id={id} type="checkbox" name={id} value={value} /></div></div></div></div></label>', options: optionsRequiredEvery },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// DEFAULT ELEMENT 'label' TESTS
{ code: '<label id="foo" />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={undefined} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={`${undefined}`} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label>First Name</label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label {...props}>Foo</label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label><input /></label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label><textarea /></label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label>{children}</label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor="foo" />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={"foo"} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={foo} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={`${id}`} />', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor="foo">Test!</label>', errors: [expectedEveryError], options: optionsRequiredEvery },
{ code: '<label htmlFor={id}>{ labelText }<div><div><div><div><div id={id} type="checkbox" name={id} value={value} /></div></div></div></div></label>', errors: [expectedEveryError], options: optionsRequiredEvery },
//
// // CUSTOM ELEMENT ARRAY OPTION TESTS
{
code: '<Label></Label>',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Label htmlFor="foo" />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Label htmlFor={"foo"} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Label htmlFor={foo} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Label htmlFor={`${id}`} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Label htmlFor="foo">Test!</Label>',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor="foo" />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor={"foo"} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor={foo} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor={`${id}`} />',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{
code: '<Descriptor htmlFor="foo">Test!</Descriptor>',
errors: [expectedEveryError],
options: [{ ...optionsComponents[0], ...optionsRequiredEvery[0] }],
},
{ code: '<Label id="foo" />', errors: [expectedEveryError], options: optionsComponents },
{
code: '<Label htmlFor={undefined} />',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Label htmlFor={`${undefined}`} />',
errors: [expectedEveryError],
options: optionsComponents,
},
{ code: '<Label>First Name</Label>', errors: [expectedEveryError], options: optionsComponents },
{
code: '<Label {...props}>Foo</Label>',
errors: [expectedEveryError],
options: optionsComponents,
},
{ code: '<Descriptor id="foo" />', errors: [expectedEveryError], options: optionsComponents },
{
code: '<Descriptor htmlFor={undefined} />',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Descriptor htmlFor={`${undefined}`} />',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Descriptor>First Name</Descriptor>',
errors: [expectedEveryError],
options: optionsComponents,
},
{
code: '<Descriptor {...props}>Foo</Descriptor>',
errors: [expectedEveryError],
options: optionsComponents,
},
{ code: '<label>{children}</label>', errors: [expectedEveryError], options: optionsComponents },
{ code: '<label htmlFor="foo" />', errors: [expectedNestingError], options: optionsRequiredNesting },
{ code: '<label>First Name</label>', errors: [expectedNestingError], options: optionsRequiredNesting },
{ code: '<label>First Name</label>', errors: [expectedSomeError], options: optionsRequiredSome },
{ code: '<label>{children}</label>', errors: [expectedSomeError], options: optionsRequiredSome },
{ code: '<label>{children}</label>', errors: [expectedNestingError], options: optionsRequiredNesting },
{
code: '<form><input type="text" id="howmuch" value="1" /><label htmlFor="howmuch">How much ?</label></form>',
errors: [expectedEveryError],
options: optionsRequiredEvery,
},
{
code: '<form><input type="text" id="howmuch" value="1" /><label htmlFor="howmuch">How much ?<span /></label></form>',
errors: [expectedEveryError],
options: optionsRequiredEvery,
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,59 @@
/**
* @fileoverview Enforce lang attribute has a valid value.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/lang';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'lang attribute must have a valid value.',
type: 'JSXAttribute',
};
const componentsSettings = {
'jsx-a11y': {
polymorphicPropName: 'as',
components: {
Foo: 'html',
},
},
};
ruleTester.run('lang', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div foo="bar" />;' },
{ code: '<div lang="foo" />;' },
{ code: '<html lang="en" />' },
{ code: '<html lang="en-US" />' },
{ code: '<html lang="zh-Hans" />' },
{ code: '<html lang="zh-Hant-HK" />' },
{ code: '<html lang="zh-yue-Hant" />' },
{ code: '<html lang="ja-Latn" />' },
{ code: '<html lang={foo} />' },
{ code: '<HTML lang="foo" />' },
{ code: '<Foo lang={undefined} />' },
{ code: '<Foo lang="en" />', settings: componentsSettings },
{ code: '<Box as="html" lang="en" />', settings: componentsSettings },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<html lang="foo" />', errors: [expectedError] },
{ code: '<html lang="zz-LL" />', errors: [expectedError] },
{ code: '<html lang={undefined} />', errors: [expectedError] },
{ code: '<Foo lang={undefined} />', settings: componentsSettings, errors: [expectedError] },
{ code: '<Box as="html" lang="foo" />', settings: componentsSettings, errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,220 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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'],
},
];
const componentsSettings = {
'jsx-a11y': {
polymorphicPropName: 'as',
components: {
Audio: 'audio',
Video: 'video',
Track: 'track',
},
},
};
ruleTester.run('media-has-caption', rule, {
valid: parsers.all([].concat(
{ 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 muted={true}></audio>',
},
{
code: '<video muted={true}></video>',
},
{
code: '<video muted></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,
},
{
code: '<Video muted></Video>',
options: customSchema,
},
{
code: '<Video muted={true}></Video>',
options: customSchema,
},
{
code: '<Audio muted></Audio>',
options: customSchema,
},
{
code: '<Audio muted={true}></Audio>',
options: customSchema,
},
{
code: '<Audio><track kind="captions" /></Audio>',
settings: componentsSettings,
},
{
code: '<audio><Track kind="captions" /></audio>',
settings: componentsSettings,
},
{
code: '<Video><track kind="captions" /></Video>',
settings: componentsSettings,
},
{
code: '<video><Track kind="captions" /></video>',
settings: componentsSettings,
},
{
code: '<Audio><Track kind="captions" /></Audio>',
settings: componentsSettings,
},
{
code: '<Video><Track kind="captions" /></Video>',
settings: componentsSettings,
},
{
code: '<Video muted></Video>',
settings: componentsSettings,
},
{
code: '<Video muted={true}></Video>',
settings: componentsSettings,
},
{
code: '<Audio muted></Audio>',
settings: componentsSettings,
},
{
code: '<Audio muted={true}></Audio>',
settings: componentsSettings,
},
{
code: '<Box as="audio" muted={true}></Box>',
settings: componentsSettings,
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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: '<Audio muted={false}></Audio>',
options: customSchema,
errors: [expectedError],
},
{
code: '<Video muted={false}></Video>',
options: customSchema,
errors: [expectedError],
},
{
code: '<Audio muted={false}></Audio>',
settings: componentsSettings,
errors: [expectedError],
},
{
code: '<Video muted={false}></Video>',
settings: componentsSettings,
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 />', settings: componentsSettings, errors: [expectedError] },
{ code: '<Video />', settings: componentsSettings, 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],
},
{
code: '<Audio><Track kind="subtitles" /></Audio>',
settings: componentsSettings,
errors: [expectedError],
},
{
code: '<Video><Track kind="subtitles" /></Video>',
settings: componentsSettings,
errors: [expectedError],
},
{
code: '<Box as="audio"><Track kind="subtitles" /></Box>',
settings: componentsSettings,
errors: [expectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,154 @@
/**
* @fileoverview Enforce onmouseover/onmouseout are accompanied
* by onfocus/onblur.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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: 'JSXAttribute',
};
const pointerEnterError = {
message: 'onPointerEnter must be accompanied by onFocus for accessibility.',
type: 'JSXAttribute',
};
const mouseOutError = {
message: 'onMouseOut must be accompanied by onBlur for accessibility.',
type: 'JSXAttribute',
};
const pointerLeaveError = {
message: 'onPointerLeave must be accompanied by onBlur for accessibility.',
type: 'JSXAttribute',
};
ruleTester.run('mouse-events-have-key-events', rule, {
valid: parsers.all([].concat(
{ 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 onBlur={() => {}} />' },
{ code: '<div onFocus={() => {}} />' },
{ 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} />' },
{ code: '<MyElement />' },
{ code: '<MyElement onMouseOver={() => {}} />' },
{ code: '<MyElement onMouseOut={() => {}} />' },
{ code: '<MyElement onBlur={() => {}} />' },
{ code: '<MyElement onFocus={() => {}} />' },
{ code: '<MyElement onMouseOver={() => {}} {...props} />' },
{ code: '<MyElement onMouseOut={() => {}} {...props} />' },
{ code: '<MyElement onBlur={() => {}} {...props} />' },
{ code: '<MyElement onFocus={() => {}} {...props} />' },
/* Passing in empty options doesn't check any event handlers */
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [{ hoverInHandlers: [], hoverOutHandlers: [] }],
},
/* Passing in custom handlers */
{
code: '<div onMouseOver={() => {}} onFocus={() => {}} />',
options: [{ hoverInHandlers: ['onMouseOver'] }],
},
{
code: '<div onMouseEnter={() => {}} onFocus={() => {}} />',
options: [{ hoverInHandlers: ['onMouseEnter'] }],
},
{
code: '<div onMouseOut={() => {}} onBlur={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseOut'] }],
},
{
code: '<div onMouseLeave={() => {}} onBlur={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseLeave'] }],
},
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [
{ hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] },
],
},
/* Custom options only checks the handlers passed in */
{
code: '<div onMouseLeave={() => {}} />',
options: [{ hoverOutHandlers: ['onPointerLeave'] }],
},
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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],
},
/* Custom options */
{
code: '<div onMouseOver={() => {}} onMouseOut={() => {}} />',
options: [
{ hoverInHandlers: ['onMouseOver'], hoverOutHandlers: ['onMouseOut'] },
],
errors: [mouseOverError, mouseOutError],
},
{
code: '<div onPointerEnter={() => {}} onPointerLeave={() => {}} />',
options: [
{ hoverInHandlers: ['onPointerEnter'], hoverOutHandlers: ['onPointerLeave'] },
],
errors: [pointerEnterError, pointerLeaveError],
},
{
code: '<div onMouseOver={() => {}} />',
options: [{ hoverInHandlers: ['onMouseOver'] }],
errors: [mouseOverError],
},
{
code: '<div onPointerEnter={() => {}} />',
options: [{ hoverInHandlers: ['onPointerEnter'] }],
errors: [pointerEnterError],
},
{
code: '<div onMouseOut={() => {}} />',
options: [{ hoverOutHandlers: ['onMouseOut'] }],
errors: [mouseOutError],
},
{
code: '<div onPointerLeave={() => {}} />',
options: [{ hoverOutHandlers: ['onPointerLeave'] }],
errors: [pointerLeaveError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,48 @@
/**
* @fileoverview Enforce no accesskey attribute on element.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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 commands used by screenreaders and keyboard-only users create a11y complications.',
type: 'JSXOpeningElement',
};
ruleTester.run('no-access-key', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div {...props} />' },
{ code: '<div accessKey={undefined} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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] },
{ code: '<div accessKey={`${undefined}`} />', errors: [expectedError] },
{ code: '<div accessKey={`${undefined}${undefined}`} />', errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,44 @@
/**
* @fileoverview Enforce `aria-hidden="true"` is not used on focusable elements.
* @author Kate Higa
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-aria-hidden-on-focusable';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const expectedError = {
message: 'aria-hidden="true" must not be set on focusable elements.',
type: 'JSXOpeningElement',
};
ruleTester.run('no-aria-hidden-on-focusable', rule, {
valid: parsers.all([].concat(
{ code: '<div aria-hidden="true" />;' },
{ code: '<div onClick={() => void 0} aria-hidden="true" />;' },
{ code: '<img aria-hidden="true" />' },
{ code: '<a aria-hidden="false" href="#" />' },
{ code: '<button aria-hidden="true" tabIndex="-1" />' },
{ code: '<button />' },
{ code: '<a href="/" />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div aria-hidden="true" tabIndex="0" />;', errors: [expectedError] },
{ code: '<input aria-hidden="true" />;', errors: [expectedError] },
{ code: '<a href="/" aria-hidden="true" />', errors: [expectedError] },
{ code: '<button aria-hidden="true" />', errors: [expectedError] },
{ code: '<textarea aria-hidden="true" />', errors: [expectedError] },
{ code: '<p tabindex="0" aria-hidden="true">text</p>;', errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,68 @@
/**
* @fileoverview Enforce autoFocus prop is not used.
* @author Ethan Cohen <@evcohen>
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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,
},
];
const componentsSettings = {
'jsx-a11y': {
components: {
Button: 'button',
},
},
};
ruleTester.run('no-autofocus', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div autofocus />;' },
{ code: '<input autofocus="true" />;' },
{ code: '<Foo bar />' },
{ code: '<Foo autoFocus />', options: ignoreNonDOMSchema },
{ code: '<div><div autofocus /></div>', options: ignoreNonDOMSchema },
{ code: '<Button />', settings: componentsSettings },
{ code: '<Button />', options: ignoreNonDOMSchema, settings: componentsSettings },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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] },
{ code: '<Button autoFocus />', errors: [expectedError], settings: componentsSettings },
{
code: '<Button autoFocus />',
errors: [expectedError],
options: ignoreNonDOMSchema,
settings: componentsSettings,
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,51 @@
/**
* @fileoverview Enforce distracting elements are not used.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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',
});
const componentsSettings = {
'jsx-a11y': {
components: {
Blink: 'blink',
},
},
};
ruleTester.run('no-marquee', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<Marquee />' },
{ code: '<div marquee />' },
{ code: '<Blink />' },
{ code: '<div blink />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ 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')] },
{ code: '<Blink />', settings: componentsSettings, errors: [expectedError('blink')] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,405 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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 componentsSettings = {
'jsx-a11y': {
components: {
Button: 'button',
Link: 'a',
},
},
};
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" />' },
{ code: '<Link href="http://x.y.z" role="img" />' },
{ code: '<Link href="http://x.y.z" />', settings: componentsSettings },
{ code: '<Button onClick={doFoo} />', settings: componentsSettings },
];
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] },
/* Custom elements */
{ code: '<Link href="http://x.y.z" role="img" />', errors: [expectedError], settings: componentsSettings },
];
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
{ code: '<tr role="presentation" />;' },
{ code: '<canvas role="img" />;' },
{ code: '<Component role="presentation" />;' },
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
{ code: '<tr role="presentation" />;', errors: [expectedError] },
{ code: '<canvas role="img" />;', errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,502 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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} />' },
{ code: '<Image onClick={() => void 0} />;' },
{
code: '<Button onClick={() => void 0} />;',
settings: { 'jsx-a11y': { components: { Button: 'button' } } },
},
/* 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: '<audio onClick={() => {}} />;' },
{ code: '<b onClick={() => {}} />;' },
{ code: '<base onClick={() => {}} />;' },
{ code: '<bdi onClick={() => {}} />;' },
{ code: '<bdo onClick={() => {}} />;' },
{ code: '<big onClick={() => {}} />;' },
{ code: '<blink onClick={() => {}} />;' },
{ code: '<body onLoad={() => {}} />;' },
{ 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: '<font onSubmit={() => {}} />;' },
{ code: '<form onSubmit={() => {}} />;' },
{
code: `
<form onSubmit={this.handleSubmit.bind(this)} method="POST">
<button type="submit">
Save
</button>
</form>
`,
},
{ code: '<frameset onClick={() => {}} />;' },
{ code: '<head onClick={() => {}} />;' },
{ code: '<header onClick={() => {}} />;' },
{ code: '<hgroup onClick={() => {}} />;' },
{ code: '<html onClick={() => {}} />;' },
{ code: '<i onClick={() => {}} />;' },
{ code: '<iframe onLoad={() => {}} />;' },
{
code: `
<iframe
name="embeddedExternalPayment"
ref="embeddedExternalPayment"
style={iframeStyle}
onLoad={this.handleLoadIframe}
/>
`,
},
{ code: '<img {...props} onError={() => {}} />;' },
{ code: '<img onLoad={() => {}} />;' },
{ code: '<img src={currentPhoto.imageUrl} onLoad={this.handleImageLoad} alt="for review" />' },
{
code: `
<img
ref={this.ref}
className="c-responsive-image-placeholder__image"
src={src}
alt={alt}
data-test-id="test-id"
onLoad={this.fetchCompleteImage}
/>
`,
},
{ 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: '<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="scrollbar" 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="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={() => {}} />;' },
// 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" onChange={() => {}} />;' },
{ code: '<div role="article" onInput={() => {}} />;' },
{ code: '<div role="article" onSubmit={() => {}} />;' },
{ 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" 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" onAnimationStart={() => {}} />;' },
{ code: '<div role="article" onAnimationEnd={() => {}} />;' },
{ code: '<div role="article" onAnimationIteration={() => {}} />;' },
{ code: '<div role="article" onTransitionEnd={() => {}} />;' },
];
const neverValid = [
/* HTML elements with an inherent, non-interactive role */
{ code: '<main onClick={() => void 0} />;', errors: [expectedError] },
{ code: '<article onClick={() => {}} />;', errors: [expectedError] },
{ code: '<aside onClick={() => {}} />;', errors: [expectedError] },
{ code: '<blockquote onClick={() => {}} />;', errors: [expectedError] },
{ code: '<body 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: '<optgroup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<output onClick={() => {}} />;', errors: [expectedError] },
{ code: '<p onClick={() => {}} />;', errors: [expectedError] },
{ code: '<pre onClick={() => {}} />;', errors: [expectedError] },
{ code: '<progress onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ruby onClick={() => {}} />;', errors: [expectedError] },
{ code: '<section onClick={() => {}} aria-label="Aardvark" />;', errors: [expectedError] },
{ code: '<section onClick={() => {}} aria-labelledby="js_1" />;', 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: '<ul onClick={() => {}} />;', errors: [expectedError] },
{ code: '<ul contentEditable="false" onClick={() => {}} />;', errors: [expectedError] },
{ code: '<article contentEditable onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div contentEditable role="article" onKeyDown={() => {}} />;', 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="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="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] },
// 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" onLoad={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onError={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseDown={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onMouseUp={() => {}} />;', errors: [expectedError] },
// Custom component
{
code: '<Image onClick={() => void 0} />;',
errors: [expectedError],
settings: { 'jsx-a11y': { components: { Image: 'img' } } },
},
];
const recommendedOptions = configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {};
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...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" 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" onAnimationStart={() => {}} />;' },
{ code: '<div role="article" onAnimationEnd={() => {}} />;' },
{ code: '<div role="article" onAnimationIteration={() => {}} />;' },
{ code: '<div role="article" onTransitionEnd={() => {}} />;' },
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
const strictOptions = configs.strict.rules[`jsx-a11y/${ruleName}`][1] || {};
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
// All the possible handlers
{ code: '<div role="article" onFocus={() => {}} />;', errors: [expectedError] },
{ code: '<div role="article" onBlur={() => {}} />;', 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" 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] },
))
.map(ruleOptionsMapperFactory(strictOptions))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,500 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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 componentsSettings = {
'jsx-a11y': {
components: {
Article: 'article',
Input: 'input',
},
},
};
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: '<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: '<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: '<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: '<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" />;' },
{ code: '<ul role="list" />;' },
/* Custom components */
{ code: '<Article role="button" />' },
{ code: '<Input role="button" />', settings: componentsSettings },
];
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: '<aside role="button" />;', errors: [expectedError] },
{ code: '<blockquote role="button" />;', errors: [expectedError] },
{ code: '<body 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: '<optgroup role="button" />;', errors: [expectedError] },
{ code: '<output 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: '<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: '<section role="button" aria-label="Aardvark" />;', 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] },
/* Custom components */
{ code: '<Article role="button" />', errors: [expectedError], settings: componentsSettings },
];
const recommendedOptions = (configs.recommended.rules[ruleName][1] || {});
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...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="menuitemcheckbox" />;' },
{ code: '<li role="menuitemradio" />;' },
{ code: '<li role="row" />;' },
{ code: '<li role="treeitem" />;' },
{ code: '<Component role="treeitem" />;' },
{ code: '<fieldset role="radiogroup" />;' },
{ code: '<fieldset role="presentation" />;' },
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
...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,123 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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 componentsSettings = {
'jsx-a11y': {
components: {
Article: 'article',
MyButton: 'button',
},
},
};
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" />' },
{ code: '<Article tabIndex="-1" />', settings: componentsSettings },
{ code: '<MyButton tabIndex={0} />', settings: componentsSettings },
];
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] },
{ code: '<Article tabIndex={0} />', errors: [expectedError], settings: componentsSettings },
];
const recommendedOptions = (
configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {}
);
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
{ code: '<div role="tabpanel" tabIndex="0" />' },
// Expressions should pass in recommended mode
{ code: '<div role={ROLE_BUTTON} onClick={() => {}} tabIndex="0" />;' },
// Cases for allowExpressionValues set to true
{
code: '<div role={BUTTON} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: true }],
},
// Specific case for ternary operator with literals on both side
{
code: '<div role={isButton ? "button" : "link"} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: true }],
},
{
code: '<div role={isButton ? "button" : LINK} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: true }],
errors: [expectedError],
},
{
code: '<div role={isButton ? BUTTON : LINK} onClick={() => {}} tabIndex="0"/>;',
options: [{ allowExpressionValues: true }],
errors: [expectedError],
},
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
{ code: '<div role="tabpanel" tabIndex="0" />', errors: [expectedError] },
// Expressions should fail in strict mode
{ code: '<div role={ROLE_BUTTON} onClick={() => {}} tabIndex="0" />;', errors: [expectedError] },
// Cases for allowExpressionValues set to false
{
code: '<div role={BUTTON} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: false }],
errors: [expectedError],
},
// Specific case for ternary operator with literals on both side
{
code: '<div role={isButton ? "button" : "link"} onClick={() => {}} tabIndex="0" />;',
options: [{ allowExpressionValues: false }],
errors: [expectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,57 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
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',
};
const componentsSettings = {
'jsx-a11y': {
components: {
CustomOption: 'option',
Input: 'input',
},
},
};
ruleTester.run('no-onchange', rule, {
valid: parsers.all([].concat(
{ 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} />' },
{ code: '<Input onChange={() => {}} />;', settings: componentsSettings },
{ code: '<CustomOption onChange={() => {}} />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<select onChange={() => {}} />;', errors: [expectedError] },
{ code: '<select onChange={handleOnChange} />;', errors: [expectedError] },
{ code: '<option onChange={() => {}} />', errors: [expectedError] },
{ code: '<option onChange={() => {}} {...props} />', errors: [expectedError] },
{ code: '<CustomOption onChange={() => {}} />;', errors: [expectedError], settings: componentsSettings },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,94 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-redundant-roles';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// 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',
});
const ruleName = 'jsx-a11y/no-redundant-roles';
const componentsSettings = {
'jsx-a11y': {
components: {
Button: 'button',
},
},
};
const alwaysValid = [
{ code: '<div />;' },
{ code: '<button role="main" />' },
{ code: '<MyComponent role="button" />' },
{ code: '<button role={`${foo}button`} />' },
{ code: '<Button role={`${foo}button`} />', settings: componentsSettings },
];
const neverValid = [
{ code: '<button role="button" />', errors: [expectedError('button', 'button')] },
{ code: '<body role="DOCUMENT" />', errors: [expectedError('body', 'document')] },
{ code: '<Button role="button" />', settings: componentsSettings, errors: [expectedError('button', 'button')] },
];
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
...alwaysValid,
{ code: '<nav role="navigation" />' },
))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
neverValid,
))
.map(parserOptionsMapper),
});
const noNavExceptionsOptions = { nav: [] };
const listException = { ul: ['list'], ol: ['list'] };
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
alwaysValid
.map(ruleOptionsMapperFactory(noNavExceptionsOptions)),
))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
...neverValid,
{ code: '<nav role="navigation" />', errors: [expectedError('nav', 'navigation')] },
))
.map(ruleOptionsMapperFactory(noNavExceptionsOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:recommended (valid list role override)`, rule, {
valid: parsers.all([].concat(
{ code: '<ul role="list" />' },
{ code: '<ol role="list" />' },
{ code: '<dl role="list" />' },
))
.map(ruleOptionsMapperFactory(listException))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<ul role="list" />', errors: [expectedError('ul', 'list')] },
{ code: '<ol role="list" />', errors: [expectedError('ol', 'list')] },
))
.map(parserOptionsMapper),
});

View File

@@ -0,0 +1,501 @@
/**
* @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 parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/no-static-element-interactions';
import ruleOptionsMapperFactory from '../../__util__/ruleOptionsMapperFactory';
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
const ruleTester = new RuleTester();
const errorMessage = 'Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.';
const expectedError = {
message: errorMessage,
type: 'JSXOpeningElement',
};
const ruleName = 'no-static-element-interactions';
const componentsSettings = {
'jsx-a11y': {
components: {
Button: 'button',
TestComponent: 'div',
},
},
};
const alwaysValid = [
{ code: '<TestComponent onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />' },
{ code: '<Button onClick={doFoo} />', settings: componentsSettings },
{ 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: '<datalist onClick={() => {}} />;' },
{ 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={() => {}} />;' },
{ code: '<form onSubmit={() => {}} />;' },
{ code: '<link onClick={() => {}} href="#" />;' },
/* 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={() => {}} />;' },
{ code: '<div role="presentation" onKeyDown={() => {}} />;' },
/* HTML elements with an inherent, non-interactive role */
{ code: '<article onClick={() => {}} />;' },
{ code: '<article onDblClick={() => void 0} />;' },
{ code: '<aside onClick={() => {}} />;' },
{ code: '<blockquote onClick={() => {}} />;' },
{ code: '<body 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: '<main onClick={() => void 0} />;' },
{ code: '<mark onClick={() => {}} />;' },
{ code: '<marquee onClick={() => {}} />;' },
{ code: '<menu onClick={() => {}} />;' },
{ code: '<meter onClick={() => {}} />;' },
{ code: '<nav onClick={() => {}} />;' },
{ code: '<ol onClick={() => {}} />;' },
{ code: '<optgroup onClick={() => {}} />;' },
{ code: '<output onClick={() => {}} />;' },
{ code: '<p onClick={() => {}} />;' },
{ code: '<pre onClick={() => {}} />;' },
{ code: '<progress onClick={() => {}} />;' },
{ code: '<ruby onClick={() => {}} />;' },
{ code: '<section onClick={() => {}} aria-label="Aa" />;' },
{ code: '<section onClick={() => {}} aria-labelledby="js_1" />;' },
{ 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="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="section" 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={() => {}} />;' },
// All the possible handlers
{ code: '<div onCopy={() => {}} />;' },
{ code: '<div onCut={() => {}} />;' },
{ code: '<div onPaste={() => {}} />;' },
{ code: '<div onCompositionEnd={() => {}} />;' },
{ code: '<div onCompositionStart={() => {}} />;' },
{ code: '<div onCompositionUpdate={() => {}} />;' },
{ code: '<div onChange={() => {}} />;' },
{ code: '<div onInput={() => {}} />;' },
{ code: '<div onSubmit={() => {}} />;' },
{ 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={() => {}} />;' },
];
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: '<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: '<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: '<del 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: '<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: '<sup onClick={() => {}} />;', errors: [expectedError] },
{ code: '<summary 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] },
// 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] },
// Custom components
{ code: '<TestComponent onClick={doFoo} />', settings: componentsSettings, errors: [expectedError] },
];
const recommendedOptions = configs.recommended.rules[`jsx-a11y/${ruleName}`][1] || {};
ruleTester.run(`${ruleName}:recommended`, rule, {
valid: parsers.all([].concat(
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={() => {}} />;' },
// Expressions should pass in recommended mode
{ code: '<div role={ROLE_BUTTON} onClick={() => {}} />;' },
{ code: '<div {...this.props} role={this.props.role} onKeyPress={e => this.handleKeyPress(e)}>{this.props.children}</div>' },
// Cases for allowExpressionValues set to true
{
code: '<div role={BUTTON} onClick={() => {}} />;',
options: [{ allowExpressionValues: true }],
},
// Specific case for ternary operator with literals on both side
{
code: '<div role={isButton ? "button" : "link"} onClick={() => {}} />;',
options: [{ allowExpressionValues: true }],
},
{
code: '<div role={isButton ? "button" : LINK} onClick={() => {}} />;',
options: [{ allowExpressionValues: true }],
errors: [expectedError],
},
{
code: '<div role={isButton ? BUTTON : LINK} onClick={() => {}} />;',
options: [{ allowExpressionValues: true }],
errors: [expectedError],
},
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
invalid: parsers.all([].concat(
neverValid,
))
.map(ruleOptionsMapperFactory(recommendedOptions))
.map(parserOptionsMapper),
});
ruleTester.run(`${ruleName}:strict`, rule, {
valid: parsers.all([].concat(
alwaysValid,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
neverValid,
// All the possible handlers
{ 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 onMouseEnter={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseLeave={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseMove={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseOut={() => {}} />;', errors: [expectedError] },
{ code: '<div onMouseOver={() => {}} />;', errors: [expectedError] },
// Expressions should fail in strict mode
{ code: '<div role={ROLE_BUTTON} onClick={() => {}} />;', errors: [expectedError] },
{ code: '<div {...this.props} role={this.props.role} onKeyPress={e => this.handleKeyPress(e)}>{this.props.children}</div>', errors: [expectedError] },
// Cases for allowExpressionValues set to false
{
code: '<div role={BUTTON} onClick={() => {}} />;',
options: [{ allowExpressionValues: false }],
errors: [expectedError],
},
// Specific case for ternary operator with literals on both side
{
code: '<div role={isButton ? "button" : "link"} onClick={() => {}} />;',
options: [{ allowExpressionValues: false }],
errors: [expectedError],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,63 @@
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
import rule from '../../../src/rules/prefer-tag-over-role';
const ruleTester = new RuleTester();
const expectedError = (role, tag) => ({
message: `Use ${tag} instead of the "${role}" role to ensure accessibility across all devices.`,
type: 'JSXOpeningElement',
});
ruleTester.run('prefer-tag-over-role', rule, {
valid: parsers.all([].concat(
{ code: '<div />;' },
{ code: '<div role="unknown" />;' },
{ code: '<div role="also unknown" />;' },
{ code: '<other />' },
{ code: '<img role="img" />' },
{ code: '<input role="checkbox" />' },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{
code: '<div role="checkbox" />',
errors: [expectedError('checkbox', '<input type="checkbox">')],
},
{
code: '<div role="button checkbox" />',
errors: [expectedError('checkbox', '<input type="checkbox">')],
},
{
code: '<div role="heading" />',
errors: [
expectedError('heading', '<h1>, <h2>, <h3>, <h4>, <h5>, or <h6>'),
],
},
{
code: '<div role="link" />',
errors: [
expectedError(
'link',
'<a href=...>, <area href=...>, or <link href=...>',
),
],
},
{
code: '<div role="rowgroup" />',
errors: [expectedError('rowgroup', '<tbody>, <tfoot>, or <thead>')],
},
{
code: '<span role="checkbox" />',
errors: [expectedError('checkbox', '<input type="checkbox">')],
},
{
code: '<other role="checkbox" />',
errors: [expectedError('checkbox', '<input type="checkbox">')],
},
{
code: '<div role="banner" />',
errors: [expectedError('banner', '<header>')],
},
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,135 @@
/**
* @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 iterFrom from 'es-iterator-helpers/Iterator.from';
import map from 'es-iterator-helpers/Iterator.prototype.map';
import toArray from 'es-iterator-helpers/Iterator.prototype.toArray';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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',
};
};
const componentsSettings = {
'jsx-a11y': {
components: {
MyComponent: 'div',
},
},
};
// Create basic test cases using all valid role types.
const basicValidityTests = toArray(map(iterFrom(roles.keys()), (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: parsers.all([].concat(
{ code: '<Bar baz />' },
{ code: '<MyComponent role="combobox" />' },
// 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>' },
{ code: '<input role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0" {...props} type="checkbox" />' },
{ code: '<input type="checkbox" role="switch" />' },
{ code: '<MyComponent role="checkbox" aria-checked="false" aria-labelledby="foo" tabindex="0" />', settings: componentsSettings },
)).concat(basicValidityTests).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// 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')],
},
// 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')],
},
{
code: '<div role="heading" />',
errors: [errorMessage('heading')],
},
{
code: '<div role="option" />',
errors: [errorMessage('option')],
},
// Custom element
{ code: '<MyComponent role="combobox" />', settings: componentsSettings, errors: [errorMessage('combobox')] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,575 @@
/**
* @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 { version as eslintVersion } from 'eslint/package.json';
import semver from 'semver';
import iterFrom from 'es-iterator-helpers/Iterator.from';
import filter from 'es-iterator-helpers/Iterator.prototype.filter';
import map from 'es-iterator-helpers/Iterator.prototype.map';
import toArray from 'es-iterator-helpers/Iterator.prototype.toArray';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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 componentsSettings = {
'jsx-a11y': {
components: {
Link: 'a',
},
},
};
const nonAbstractRoles = toArray(filter(iterFrom(roles.keys()), (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 = filter(
map(iterFrom(aria.keys()), (attribute) => attribute.toLowerCase()),
(attribute) => validPropsForRole.indexOf(attribute) === -1,
);
const normalRole = role.toLowerCase();
return [
tests[0].concat(validPropsForRole.map((prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
}))),
tests[1].concat(toArray(map(invalidPropsForRole, (prop) => ({
code: `<div role="${normalRole}" ${prop.toLowerCase()} />`,
errors: [errorMessage(prop.toLowerCase(), normalRole, 'div', false)],
})))),
];
}, [[], []]);
const [validTests, invalidTests] = createTests(nonAbstractRoles);
ruleTester.run('role-supports-aria-props', rule, {
valid: parsers.all([].concat(
{ code: '<Foo bar />' },
{ code: '<div />' },
{ code: '<div id="main" />' },
{ code: '<div role />' },
{ code: '<div role="presentation" {...props} />' },
{ code: '<Foo.Bar baz={true} />' },
{ code: '<Link href="#" aria-checked />' },
// 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-current />' },
{ code: '<a href="#" aria-describedby />' },
{ code: '<a href="#" aria-disabled />' },
{ code: '<a href="#" aria-dropeffect />' },
{ code: '<a href="#" aria-flowto />' },
{ code: '<a href="#" aria-haspopup />' },
{ code: '<a href="#" aria-grabbed />' },
{ code: '<a href="#" aria-hidden />' },
{ 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-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-hidden />' },
{ code: '<link href="#" aria-haspopup />' },
{ 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 - no implicit role
{ code: '<img alt="" aria-checked />' },
// 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-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-hidden />' },
{ 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-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-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-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-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-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-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-atomic />' },
{ code: '<input type="checkbox" aria-busy />' },
{ code: '<input type="checkbox" aria-checked />' },
{ 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-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-atomic />' },
{ code: '<input type="radio" aria-busy />' },
{ code: '<input type="radio" aria-checked />' },
{ 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-hidden />' },
{ 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-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 />' },
// Allow null/undefined values regardless of role
{ code: '<h2 role="presentation" aria-level={null} />' },
{ code: '<h2 role="presentation" aria-level={undefined} />' },
// OTHER TESTS
{ code: '<button aria-pressed />' },
{ 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-current />' },
{ code: '<meter aria-atomic />' },
{ code: '<option aria-atomic />' },
{ code: '<progress aria-atomic />' },
{ code: '<textarea aria-hidden />' },
{ code: '<select aria-expanded />' },
{ code: '<datalist aria-expanded />' },
{ code: '<div role="heading" aria-level />' },
{ code: '<div role="heading" aria-level="1" />' },
semver.satisfies(eslintVersion, '>= 6') ? {
code: `
const HelloThere = () => (
<Hello
role="searchbox"
frag={
<>
<div>Hello</div>
<div>There</div>
</>
}
/>
);
const Hello = (props) => <div>{props.frag}</div>;
`,
} : [],
validTests,
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
// 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="foobar" aria-checked />',
errors: [errorMessage('aria-checked', 'img', '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)],
},
{
code: '<ul aria-expanded />',
errors: [errorMessage('aria-expanded', 'list', 'ul', true)],
},
{
code: '<details aria-expanded />',
errors: [errorMessage('aria-expanded', 'group', 'details', true)],
},
{
code: '<dialog aria-expanded />',
errors: [errorMessage('aria-expanded', 'dialog', 'dialog', true)],
},
{
code: '<aside aria-expanded />',
errors: [errorMessage('aria-expanded', 'complementary', 'aside', true)],
},
{
code: '<article aria-expanded />',
errors: [errorMessage('aria-expanded', 'article', 'article', true)],
},
{
code: '<body aria-expanded />',
errors: [errorMessage('aria-expanded', 'document', 'body', true)],
},
{
code: '<li aria-expanded />',
errors: [errorMessage('aria-expanded', 'listitem', 'li', true)],
},
{
code: '<nav aria-expanded />',
errors: [errorMessage('aria-expanded', 'navigation', 'nav', true)],
},
{
code: '<ol aria-expanded />',
errors: [errorMessage('aria-expanded', 'list', 'ol', true)],
},
{
code: '<output aria-expanded />',
errors: [errorMessage('aria-expanded', 'status', 'output', true)],
},
{
code: '<section aria-expanded />',
errors: [errorMessage('aria-expanded', 'region', 'section', true)],
},
{
code: '<tbody aria-expanded />',
errors: [errorMessage('aria-expanded', 'rowgroup', 'tbody', true)],
},
{
code: '<tfoot aria-expanded />',
errors: [errorMessage('aria-expanded', 'rowgroup', 'tfoot', true)],
},
{
code: '<thead aria-expanded />',
errors: [errorMessage('aria-expanded', 'rowgroup', 'thead', true)],
},
{
code: '<input type="radio" aria-invalid />',
errors: [errorMessage('aria-invalid', 'radio', 'input', true)],
},
{
code: '<input type="radio" aria-selected />',
errors: [errorMessage('aria-selected', 'radio', 'input', true)],
},
{
code: '<input type="radio" aria-haspopup />',
errors: [errorMessage('aria-haspopup', 'radio', 'input', true)],
},
{
code: '<input type="checkbox" aria-haspopup />',
errors: [errorMessage('aria-haspopup', 'checkbox', 'input', true)],
},
{
code: '<input type="reset" aria-invalid />',
errors: [errorMessage('aria-invalid', 'button', 'input', true)],
},
{
code: '<input type="submit" aria-invalid />',
errors: [errorMessage('aria-invalid', 'button', 'input', true)],
},
{
code: '<input type="image" aria-invalid />',
errors: [errorMessage('aria-invalid', 'button', 'input', true)],
},
{
code: '<input type="button" aria-invalid />',
errors: [errorMessage('aria-invalid', 'button', 'input', true)],
},
{
code: '<menuitem type="command" aria-invalid />',
errors: [errorMessage('aria-invalid', 'menuitem', 'menuitem', true)],
},
{
code: '<menuitem type="radio" aria-selected />',
errors: [errorMessage('aria-selected', 'menuitemradio', 'menuitem', true)],
},
{
code: '<menu type="toolbar" aria-haspopup />',
errors: [errorMessage('aria-haspopup', 'toolbar', 'menu', true)],
},
{
code: '<menu type="toolbar" aria-invalid />',
errors: [errorMessage('aria-invalid', 'toolbar', 'menu', true)],
},
{
code: '<menu type="toolbar" aria-expanded />',
errors: [errorMessage('aria-expanded', 'toolbar', 'menu', true)],
},
{
code: '<link href="#" aria-invalid />',
errors: [errorMessage('aria-invalid', 'link', 'link', true)],
},
{
code: '<area href="#" aria-invalid />',
errors: [errorMessage('aria-invalid', 'link', 'area', true)],
},
{
code: '<a href="#" aria-invalid />',
errors: [errorMessage('aria-invalid', 'link', 'a', true)],
},
{
code: '<Link href="#" aria-checked />',
errors: [errorMessage('aria-checked', 'link', 'a', true)],
settings: componentsSettings,
},
)).concat(invalidTests).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,50 @@
/**
* @fileoverview Enforce scope prop is only used on <th> elements.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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',
};
const componentsSettings = {
'jsx-a11y': {
components: {
Foo: 'div',
TableHeader: 'th',
},
},
};
ruleTester.run('scope', rule, {
valid: parsers.all([].concat(
{ 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} />' },
{ code: '<TableHeader scope="row" />', settings: componentsSettings },
)).map(parserOptionsMapper),
invalid: parsers.all([].concat(
{ code: '<div scope />', errors: [expectedError] },
{ code: '<Foo scope="bar" />', settings: componentsSettings, errors: [expectedError] },
)).map(parserOptionsMapper),
});

View File

@@ -0,0 +1,55 @@
/**
* @fileoverview Enforce tabIndex value is not greater than zero.
* @author Ethan Cohen
*/
// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------
import { RuleTester } from 'eslint';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
import parsers from '../../__util__/helpers/parsers';
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: parsers.all([].concat(
{ 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: parsers.all([].concat(
{ 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,91 @@
import test from 'tape';
import attributesComparator from '../../../src/util/attributesComparator';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
test('attributesComparator', (t) => {
t.equal(
attributesComparator(),
true,
'baseAttributes are undefined and attributes are undefined -> true',
);
t.equal(
attributesComparator([], []),
true,
'baseAttributes are empty and attributes are empty -> true',
);
t.equal(
attributesComparator([], [
JSXAttributeMock('foo', 0),
JSXAttributeMock('bar', 'baz'),
]),
true,
'baseAttributes are empty and attributes have values -> true',
);
const baseAttributes = [
{
name: 'biz',
value: 1,
}, {
name: 'fizz',
value: 'pop',
}, {
name: 'fuzz',
value: 'lolz',
},
];
t.equal(
attributesComparator(baseAttributes, []),
false,
'baseAttributes have values and attributes are empty -> false',
);
t.equal(
attributesComparator(baseAttributes, [
JSXElementMock(),
JSXAttributeMock('biz', 2),
JSXAttributeMock('ziff', 'opo'),
JSXAttributeMock('far', 'lolz'),
]),
false,
'baseAttributes have values and attributes have values, and the values are different -> false',
);
t.equal(
attributesComparator(baseAttributes, [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('goo', 'gazz'),
]),
false,
'baseAttributes have values and attributes have values, and the values are a subset -> false',
);
t.equal(
attributesComparator(baseAttributes, [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('fuzz', 'lolz'),
]),
true,
'baseAttributes have values and attributes have values, and the values are the same -> true',
);
t.equal(
attributesComparator(baseAttributes, [
JSXAttributeMock('biz', 1),
JSXAttributeMock('fizz', 'pop'),
JSXAttributeMock('fuzz', 'lolz'),
JSXAttributeMock('dar', 'tee'),
]),
true,
'baseAttributes have values and attributes have values, and the values are a superset -> true',
);
t.end();
});

View File

@@ -0,0 +1,174 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import getAccessibleChildText from '../../../src/util/getAccessibleChildText';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
test('getAccessibleChildText', (t) => {
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-label', 'foo')],
), elementType),
'foo',
'returns the aria-label when present',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-label', 'foo')],
[{ type: 'JSXText', value: 'bar' }],
), elementType),
'foo',
'returns the aria-label instead of children',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[JSXAttributeMock('aria-hidden', 'true')],
), elementType),
'',
'skips elements with aria-hidden=true',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'JSXText', value: 'bar' }],
), elementType),
'bar',
'returns literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
JSXAttributeMock('alt', 'a sensible label'),
])],
), elementType),
'a sensible label',
'returns alt text for img child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock('span', [
JSXAttributeMock('alt', 'a sensible label'),
])],
), elementType),
'',
'returns blank when alt tag is used on arbitrary element',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'bar' }],
), elementType),
'bar',
'returns literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: ' bar ' }],
), elementType),
'bar',
'returns trimmed literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo bar' }],
), elementType),
'foo bar',
'returns space-collapsed literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo, bar. baz? foo; bar:' }],
), elementType),
'foo bar baz foo bar',
'returns punctuation-stripped literal value for JSXText child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock(
'span',
[],
[{ type: 'Literal', value: 'bar' }],
)],
), elementType),
'bar',
'returns recursive value for JSXElement child',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[JSXElementMock(
'span',
[],
[JSXElementMock(
'span',
[JSXAttributeMock('aria-hidden', 'true')],
)],
)],
), elementType),
'',
'skips children with aria-hidden-true',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo' }, { type: 'Literal', value: 'bar' }],
), elementType),
'foo bar',
'joins multiple children properly - no spacing',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: ' foo ' }, { type: 'Literal', value: ' bar ' }],
), elementType),
'foo bar',
'joins multiple children properly - with spacing',
);
t.equal(
getAccessibleChildText(JSXElementMock(
'a',
[],
[{ type: 'Literal', value: 'foo' }, { type: 'Unknown' }, { type: 'Literal', value: 'bar' }],
), elementType),
'foo bar',
'skips unknown elements',
);
t.end();
});

View File

@@ -0,0 +1,71 @@
import test from 'tape';
import getComputedRole from '../../../src/util/getComputedRole';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('getComputedRole', (t) => {
t.equal(
getComputedRole(
'div',
[JSXAttributeMock('role', 'button')],
),
'button',
'explicit role + valid role -> returns the role',
);
t.equal(
getComputedRole(
'li',
[JSXAttributeMock('role', 'beeswax')],
),
'listitem',
'explicit role + invalid role + has implicit -> returns the implicit role',
);
t.equal(
getComputedRole(
'div',
[JSXAttributeMock('role', 'beeswax')],
),
null,
'explicit role + invalid role + lacks implicit -> returns null',
);
t.equal(
getComputedRole(
'li',
[],
),
'listitem',
'explicit role + no role + has implicit -> returns the implicit role',
);
t.equal(
getComputedRole(
'div',
[],
),
null,
'explicit role + no role + lacks implicit -> returns null',
);
t.equal(
getComputedRole(
'li',
[JSXAttributeMock('role', 'beeswax')],
),
'listitem',
'implicit role + has implicit -> returns the implicit role',
);
t.equal(
getComputedRole(
'div',
[],
),
null,
'implicit role + lacks implicit -> returns null',
);
t.end();
});

View File

@@ -0,0 +1,154 @@
import test from 'tape';
import getElementType from '../../../src/util/getElementType';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('getElementType', (t) => {
t.test('no settings in context', (st) => {
const elementType = getElementType({ settings: {} });
st.equal(
elementType(JSXElementMock('input').openingElement),
'input',
'returns the exact tag name for a DOM element',
);
st.equal(
elementType(JSXElementMock('CustomInput').openingElement),
'CustomInput',
'returns the exact tag name for a custom element',
);
st.equal(
elementType(JSXElementMock('toString').openingElement),
'toString',
'returns the exact tag name for names that are in Object.prototype',
);
st.equal(
elementType(JSXElementMock('span', [JSXAttributeMock('as', 'h1')]).openingElement),
'span',
'returns the default tag name provided',
);
st.end();
});
t.test('components settings in context', (st) => {
const elementType = getElementType({
settings: {
'jsx-a11y': {
components: {
CustomInput: 'input',
},
},
},
});
st.equal(
elementType(JSXElementMock('input').openingElement),
'input',
'returns the exact tag name for a DOM element',
);
st.equal(
elementType(JSXElementMock('CustomInput').openingElement),
'input',
'returns the mapped tag name for a custom element',
);
st.equal(
elementType(JSXElementMock('CityInput').openingElement),
'CityInput',
'returns the exact tag name for a custom element not in the components map',
);
st.equal(
elementType(JSXElementMock('span', [JSXAttributeMock('as', 'h1')]).openingElement),
'span',
'return the default tag name since not polymorphicPropName was provided',
);
st.end();
});
t.test('polymorphicPropName settings in context', (st) => {
const elementType = getElementType({
settings: {
'jsx-a11y': {
polymorphicPropName: 'asChild',
components: {
CustomButton: 'button',
},
},
},
});
st.equal(
elementType(JSXElementMock('span', [JSXAttributeMock('asChild', 'h1')]).openingElement),
'h1',
'returns the tag name provided by the polymorphic prop, "asChild", defined in the settings',
);
st.equal(
elementType(JSXElementMock('CustomButton', [JSXAttributeMock('asChild', 'a')]).openingElement),
'a',
'returns the tag name provided by the polymorphic prop, "asChild", defined in the settings instead of the component mapping tag',
);
st.equal(
elementType(JSXElementMock('CustomButton', [JSXAttributeMock('as', 'a')]).openingElement),
'button',
'returns the tag name provided by the componnet mapping if the polymorphic prop, "asChild", defined in the settings is not set',
);
st.end();
});
t.test('polymorphicPropName settings and explicitly defined polymorphicAllowList in context', (st) => {
const elementType = getElementType({
settings: {
'jsx-a11y': {
polymorphicPropName: 'asChild',
polymorphicAllowList: [
'Box',
'Icon',
],
components: {
Box: 'div',
Icon: 'svg',
},
},
},
});
st.equal(
elementType(JSXElementMock('Spinner', [JSXAttributeMock('asChild', 'img')]).openingElement),
'Spinner',
'does not use the polymorphic prop if polymorphicAllowList is defined, but element is not part of polymorphicAllowList',
);
st.equal(
elementType(JSXElementMock('Icon', [JSXAttributeMock('asChild', 'img')]).openingElement),
'img',
'uses the polymorphic prop if it is in explicitly defined polymorphicAllowList',
);
st.equal(
elementType(JSXElementMock('Box', [JSXAttributeMock('asChild', 'span')]).openingElement),
'span',
'returns the tag name provided by the polymorphic prop, "asChild", defined in the settings instead of the component mapping tag',
);
st.equal(
elementType(JSXElementMock('Box', [JSXAttributeMock('as', 'a')]).openingElement),
'div',
'returns the tag name provided by the component mapping if the polymorphic prop, "asChild", defined in the settings is not set',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,35 @@
import test from 'tape';
import getExplicitRole from '../../../src/util/getExplicitRole';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('getExplicitRole', (t) => {
t.equal(
getExplicitRole(
'div',
[JSXAttributeMock('role', 'button')],
),
'button',
'valid role returns the role',
);
t.equal(
getExplicitRole(
'div',
[JSXAttributeMock('role', 'beeswax')],
),
null,
'invalid role returns null',
);
t.equal(
getExplicitRole(
'div',
[],
),
null,
'no role returns null',
);
t.end();
});

View File

@@ -0,0 +1,25 @@
import test from 'tape';
import getImplicitRole from '../../../src/util/getImplicitRole';
test('getImplicitRole', (t) => {
t.equal(
getImplicitRole(
'li',
[],
),
'listitem',
'has implicit, returns implicit role',
);
t.equal(
getImplicitRole(
'div',
[],
),
null,
'lacks implicit, returns null',
);
t.end();
});

View File

@@ -0,0 +1,33 @@
import test from 'tape';
import getSuggestion from '../../../src/util/getSuggestion';
test('spell check suggestion API', (t) => {
t.deepEqual([], getSuggestion('foo'), 'returns no suggestions given empty word and no dictionary');
t.deepEqual(
getSuggestion('foo'),
[],
'returns no suggestions given real word and no dictionary',
);
t.deepEqual(
getSuggestion('fo', ['foo', 'bar', 'baz']),
['foo'],
'returns correct suggestion given real word and a dictionary',
);
t.deepEqual(
getSuggestion('theer', ['there', 'their', 'foo', 'bar']),
['there', 'their'],
'returns multiple correct suggestions given real word and a dictionary',
);
t.deepEqual(
getSuggestion('theer', ['there', 'their', 'foo', 'bar'], 1),
['there'],
'returns correct # of suggestions given the limit argument',
);
t.end();
});

View File

@@ -0,0 +1,85 @@
import test from 'tape';
import getTabIndex from '../../../src/util/getTabIndex';
import IdentifierMock from '../../../__mocks__/IdentifierMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('getTabIndex', (t) => {
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', 0)),
0,
'tabIndex is defined as zero -> zero',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', 1)),
1,
'tabIndex is defined as a positive integer -> returns it',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', -1)),
-1,
'tabIndex is defined as a negative integer -> returns it',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', '')),
undefined,
'tabIndex is defined as an empty string -> undefined',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', 9.1)),
undefined,
'tabIndex is defined as a float -> undefined',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', '0')),
0,
'tabIndex is defined as a string which converts to a number -> returns the integer',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', '0a')),
undefined,
'tabIndex is defined as a string which is NaN -> returns undefined',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', true)),
undefined,
'tabIndex is defined as true -> returns undefined',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', false)),
undefined,
'tabIndex is defined as false -> returns undefined',
);
t.equal(
typeof getTabIndex(JSXAttributeMock('tabIndex', () => 0)),
'function',
'tabIndex is defined as a function expression -> returns the correct type',
);
const name = 'identName';
t.equal(
getTabIndex(JSXAttributeMock(
'tabIndex',
IdentifierMock(name),
true,
)),
name,
'tabIndex is defined as a variable expression -> returns the Identifier name',
);
t.equal(
getTabIndex(JSXAttributeMock('tabIndex', undefined)),
undefined,
'tabIndex is not defined -> returns undefined',
);
t.end();
});

View File

@@ -0,0 +1,157 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import hasAccessibleChild from '../../../src/util/hasAccessibleChild';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock';
test('hasAccessibleChild', (t) => {
t.equal(
hasAccessibleChild(JSXElementMock('div', []), elementType),
false,
'has no children and does not set dangerouslySetInnerHTML -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [JSXAttributeMock('dangerouslySetInnerHTML', true)], []),
elementType,
),
true,
'has no children and sets dangerouslySetInnerHTML -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock(
'div',
[],
[{
type: 'Literal',
value: 'foo',
}],
),
elementType,
),
true,
'has children + Literal child -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [JSXElementMock('div', [])]),
elementType,
),
true,
'has children + visible JSXElement child -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [{
type: 'JSXText',
value: 'foo',
}]),
elementType,
),
true,
'has children + JSText element -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXElementMock('div', [
JSXAttributeMock('aria-hidden', true),
]),
]),
elementType,
),
false,
'has children + hidden child JSXElement -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXExpressionContainerMock({
type: 'Identifier',
name: 'foo',
}),
]),
elementType,
),
true,
'defined JSXExpressionContainer -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXExpressionContainerMock({
type: 'Identifier',
name: 'undefined',
}),
]),
elementType,
),
false,
'has children + undefined JSXExpressionContainer -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [{
type: 'Unknown',
}]),
elementType,
),
false,
'unknown child type -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [JSXAttributeMock('children', true)], []),
elementType,
),
true,
'children passed as a prop -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXElementMock('input', [JSXAttributeMock('type', 'hidden')]),
]),
elementType,
),
false,
'has chidren -> hidden child input JSXElement -> false',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXElementMock('CustomInput', [JSXAttributeMock('type', 'hidden')]),
]),
elementType,
),
true,
'has children + custom JSXElement of type hidden -> true',
);
t.equal(
hasAccessibleChild(
JSXElementMock('div', [], [
JSXElementMock('CustomInput', [JSXAttributeMock('type', 'hidden')]),
]),
() => 'input',
),
false,
'custom JSXElement mapped to input if type is hidden -> false',
);
t.end();
});

View File

@@ -0,0 +1,87 @@
import test from 'tape';
import JSXAttributeMock from '../../../../__mocks__/JSXAttributeMock';
import getImplicitRoleForInput from '../../../../src/util/implicitRoles/input';
test('isAbstractRole', (t) => {
t.test('works for buttons', (st) => {
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'button')]),
'button',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'image')]),
'button',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'reset')]),
'button',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'submit')]),
'button',
);
st.end();
});
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'checkbox')]),
'checkbox',
'works for checkboxes',
);
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'radio')]),
'radio',
'works for radios',
);
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'range')]),
'slider',
'works for ranges',
);
t.test('works for textboxes', (st) => {
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'email')]),
'textbox',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'password')]),
'textbox',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'search')]),
'textbox',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'tel')]),
'textbox',
);
st.equal(
getImplicitRoleForInput([JSXAttributeMock('type', 'url')]),
'textbox',
);
st.end();
});
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', '')]),
'textbox',
'works for the default case',
);
t.equal(
getImplicitRoleForInput([JSXAttributeMock('type', true)]),
'textbox',
'works for the true case',
);
t.end();
});

View File

@@ -0,0 +1,20 @@
import test from 'tape';
import JSXAttributeMock from '../../../../__mocks__/JSXAttributeMock';
import getImplicitRoleForMenu from '../../../../src/util/implicitRoles/menu';
test('isAbstractRole', (t) => {
t.equal(
getImplicitRoleForMenu([JSXAttributeMock('type', 'toolbar')]),
'toolbar',
'works for toolbars',
);
t.equal(
getImplicitRoleForMenu([JSXAttributeMock('type', '')]),
'',
'works for non-toolbars',
);
t.end();
});

View File

@@ -0,0 +1,38 @@
import test from 'tape';
import JSXAttributeMock from '../../../../__mocks__/JSXAttributeMock';
import getImplicitRoleForMenuitem from '../../../../src/util/implicitRoles/menuitem';
test('isAbstractRole', (t) => {
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', 'command')]),
'menuitem',
'works for menu items',
);
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', 'checkbox')]),
'menuitemcheckbox',
'works for menu item checkboxes',
);
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', 'radio')]),
'menuitemradio',
'works for menu item radios',
);
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', '')]),
'',
'works for non-toolbars',
);
t.equal(
getImplicitRoleForMenuitem([JSXAttributeMock('type', true)]),
'',
'works for the true case',
);
t.end();
});

View File

@@ -0,0 +1,51 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isAbstractRole from '../../../src/util/isAbstractRole';
import {
genElementSymbol,
genAbstractRoleElements,
genNonAbstractRoleElements,
} from '../../../__mocks__/genInteractives';
test('isAbstractRole', (t) => {
t.equal(
isAbstractRole(undefined, []),
false,
'does NOT identify JSX Components (no tagName) as abstract role elements',
);
t.test('elements with an abstract role', (st) => {
genAbstractRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isAbstractRole(
elementType(openingElement),
attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as an abstract role element`,
);
});
st.end();
});
t.test('elements with a non-abstract role', (st) => {
genNonAbstractRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isAbstractRole(
elementType(openingElement),
attributes,
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` as an abstract role element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,52 @@
import test from 'tape';
import isContentEditable from '../../../src/util/isContentEditable';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('isContentEditable - HTML5', (t) => {
t.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', 'true'),
]),
true,
'identifies HTML5 contentEditable elements',
);
t.test('not content editable', (st) => {
st.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', null),
]),
false,
'does not identify HTML5 content editable elements with null as the value',
);
st.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', undefined),
]),
false,
'does not identify HTML5 content editable elements with undefined as the value',
);
st.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', true),
]),
false,
'does not identify HTML5 content editable elements with true as the value',
);
st.equal(
isContentEditable('some tag', [
JSXAttributeMock('contentEditable', 'false'),
]),
false,
'does not identify HTML5 content editable elements with "false" as the value',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,30 @@
import test from 'tape';
import { dom } from 'aria-query';
import { elementType } from 'jsx-ast-utils';
import isDOMElement from '../../../src/util/isDOMElement';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
test('isDOMElement', (t) => {
t.test('DOM elements', (st) => {
dom.forEach((_, el) => {
const element = JSXElementMock(el);
st.equal(
isDOMElement(elementType(element.openingElement)),
true,
`identifies ${el} as a DOM element`,
);
});
st.end();
});
t.equal(
isDOMElement(JSXElementMock('CustomElement')),
false,
'does not identify a custom element',
);
t.end();
});

View File

@@ -0,0 +1,88 @@
import test from 'tape';
import isDisabledElement from '../../../src/util/isDisabledElement';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('isDisabledElement', (t) => {
t.test('HTML5', (st) => {
st.equal(
isDisabledElement([
JSXAttributeMock('disabled', 'disabled'),
]),
true,
'identifies HTML5 disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('disabled', null),
]),
true,
'identifies HTML5 disabled elements with null as the value',
);
st.equal(
isDisabledElement([
JSXAttributeMock('disabled', undefined),
]),
false,
'does not identify HTML5 disabled elements with undefined as the value',
);
st.end();
});
t.test('ARIA', (st) => {
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', 'true'),
]),
true,
'does not identify ARIA disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', true),
]),
true,
'does not identify ARIA disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', 'false'),
]),
false,
'does not identify ARIA disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', false),
]),
false,
'does not identify ARIA disabled elements',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', null),
]),
false,
'does not identify ARIA disabled elements with null as the value',
);
st.equal(
isDisabledElement([
JSXAttributeMock('aria-disabled', undefined),
]),
false,
'does not identify ARIA disabled elements with undefined as the value',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,111 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isFocusable from '../../../src/util/isFocusable';
import {
genElementSymbol,
genInteractiveElements,
genNonInteractiveElements,
} from '../../../__mocks__/genInteractives';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
function mergeTabIndex(index, attributes) {
return [].concat(attributes, JSXAttributeMock('tabIndex', index));
}
test('isFocusable', (t) => {
t.test('interactive elements', (st) => {
genInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isFocusable(
elementType(openingElement),
openingElement.attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(-1, openingElement.attributes),
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` with tabIndex of -1 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(0, openingElement.attributes),
),
true,
`identifies \`${genElementSymbol(openingElement)}\` with tabIndex of 0 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(1, openingElement.attributes),
),
true,
`identifies \`${genElementSymbol(openingElement)}\` with tabIndex of 1 as a focusable element`,
);
});
st.end();
});
t.test('non-interactive elements', (st) => {
genNonInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isFocusable(
elementType(openingElement),
openingElement.attributes,
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(-1, openingElement.attributes),
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` with tabIndex of -1 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(0, openingElement.attributes),
),
true,
`identifies \`${genElementSymbol(openingElement)}\` with tabIndex of 0 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex(1, openingElement.attributes),
),
true,
`identifies \`${genElementSymbol(openingElement)}\` with tabIndex of 1 as a focusable element`,
);
st.equal(
isFocusable(
elementType(openingElement),
mergeTabIndex('bogus', openingElement.attributes),
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` with tabIndex of 'bogus' as a focusable element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,104 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isInteractiveElement from '../../../src/util/isInteractiveElement';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import {
genElementSymbol,
genIndeterminantInteractiveElements,
genInteractiveElements,
genInteractiveRoleElements,
genNonInteractiveElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
test('isInteractiveElement', (t) => {
t.equal(
isInteractiveElement(undefined, []),
false,
'identifies them as interactive elements',
);
t.test('interactive elements', (st) => {
genInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.test('interactive role elements', (st) => {
genInteractiveRoleElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.test('non-interactive elements', (st) => {
genNonInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.test('non-interactive role elements', (st) => {
genNonInteractiveRoleElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.test('indeterminate elements', (st) => {
genIndeterminantInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive element`,
);
});
st.end();
});
t.equal(
isInteractiveElement('CustomComponent', JSXElementMock()),
false,
'JSX elements are not interactive',
);
t.end();
});

View File

@@ -0,0 +1,59 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isInteractiveRole from '../../../src/util/isInteractiveRole';
import {
genElementSymbol,
genInteractiveRoleElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
test('isInteractiveRole', (t) => {
t.equal(
isInteractiveRole(undefined, []),
false,
'identifies JSX Components (no tagName) as interactive role elements',
);
t.test('elements with a non-interactive role', (st) => {
genNonInteractiveRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isInteractiveRole(
elementType(openingElement),
attributes,
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` as an interactive role element`,
);
});
st.end();
});
t.equal(
isInteractiveRole('div', []),
false,
'does NOT identify elements without a role as interactive role elements',
);
t.test('elements with an interactive role', (st) => {
genInteractiveRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isInteractiveRole(
elementType(openingElement),
attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as an interactive role element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,97 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isNonInteractiveElement from '../../../src/util/isNonInteractiveElement';
import {
genElementSymbol,
genIndeterminantInteractiveElements,
genInteractiveElements,
genInteractiveRoleElements,
genNonInteractiveElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
test('isNonInteractiveElement', (t) => {
t.equal(
isNonInteractiveElement(undefined, []),
false,
'identifies JSX Components (no tagName) as non-interactive elements',
);
t.test('non-interactive elements', (st) => {
genNonInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.test('non-interactive role elements', (st) => {
genNonInteractiveRoleElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.test('interactive elements', (st) => {
genInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.test('interactive role elements', (st) => {
genInteractiveRoleElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.test('indeterminate elements', (st) => {
genIndeterminantInteractiveElements().forEach(({ openingElement }) => {
st.equal(
isNonInteractiveElement(
elementType(openingElement),
openingElement.attributes,
),
false,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,59 @@
import test from 'tape';
import { elementType } from 'jsx-ast-utils';
import isNonInteractiveRole from '../../../src/util/isNonInteractiveRole';
import {
genElementSymbol,
genInteractiveRoleElements,
genNonInteractiveRoleElements,
} from '../../../__mocks__/genInteractives';
test('isNonInteractiveRole', (t) => {
t.equal(
isNonInteractiveRole(undefined, []),
false,
'identifies JSX Components (no tagName) as non-interactive elements',
);
t.test('elements with a non-interactive role', (st) => {
genNonInteractiveRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isNonInteractiveRole(
elementType(openingElement),
attributes,
),
true,
`identifies \`${genElementSymbol(openingElement)}\` as a non-interactive role element`,
);
});
st.end();
});
t.equal(
isNonInteractiveRole('div', []),
false,
'does NOT identify elements without a role as non-interactive role elements',
);
t.test('elements with an interactive role', (st) => {
genInteractiveRoleElements().forEach(({ openingElement }) => {
const { attributes } = openingElement;
st.equal(
isNonInteractiveRole(
elementType(openingElement),
attributes,
),
false,
`does NOT identify \`${genElementSymbol(openingElement)}\` as a non-interactive role element`,
);
});
st.end();
});
t.end();
});

View File

@@ -0,0 +1,52 @@
import test from 'tape';
import isNonLiteralProperty from '../../../src/util/isNonLiteralProperty';
import IdentifierMock from '../../../__mocks__/IdentifierMock';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXSpreadAttributeMock from '../../../__mocks__/JSXSpreadAttributeMock';
import JSXTextMock from '../../../__mocks__/JSXTextMock';
import LiteralMock from '../../../__mocks__/LiteralMock';
const theProp = 'theProp';
const spread = JSXSpreadAttributeMock('theSpread');
test('isNonLiteralProperty', (t) => {
t.equal(
isNonLiteralProperty([], theProp),
false,
'does not identify them as non-literal role elements',
);
t.equal(
isNonLiteralProperty([JSXAttributeMock(theProp, LiteralMock('theRole'))], theProp),
false,
'does not identify elements with a literal property as non-literal role elements without spread operator',
);
t.equal(
isNonLiteralProperty([spread, JSXAttributeMock(theProp, LiteralMock('theRole'))], theProp),
false,
'does not identify elements with a literal property as non-literal role elements with spread operator',
);
t.equal(
isNonLiteralProperty([JSXAttributeMock(theProp, JSXTextMock('theRole'))], theProp),
false,
'identifies elements with a JSXText property as non-literal role elements',
);
t.equal(
isNonLiteralProperty([JSXAttributeMock(theProp, IdentifierMock('undefined'))], theProp),
false,
'does not identify elements with a property of undefined as non-literal role elements',
);
t.equal(
isNonLiteralProperty([JSXAttributeMock(theProp, IdentifierMock('theIdentifier'))], theProp),
true,
'identifies elements with an expression property as non-literal role elements',
);
t.end();
});

View File

@@ -0,0 +1,72 @@
import test from 'tape';
import isSemanticRoleElement from '../../../src/util/isSemanticRoleElement';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
test('isSemanticRoleElement', (t) => {
t.equal(
isSemanticRoleElement('input', [
JSXAttributeMock('type', 'checkbox'),
JSXAttributeMock('role', 'switch'),
]),
true,
'identifies semantic role elements',
);
t.test('rejects non-semantics role elements', (st) => {
st.equal(
isSemanticRoleElement('input', [
JSXAttributeMock('type', 'radio'),
JSXAttributeMock('role', 'switch'),
]),
false,
);
st.equal(
isSemanticRoleElement('input', [
JSXAttributeMock('type', 'text'),
JSXAttributeMock('role', 'combobox'),
]),
false,
);
st.equal(
isSemanticRoleElement('button', [
JSXAttributeMock('role', 'switch'),
JSXAttributeMock('aria-pressed', 'true'),
]),
false,
);
st.equal(
isSemanticRoleElement('input', [
JSXAttributeMock('role', 'switch'),
]),
false,
);
st.end();
});
t.doesNotThrow(
() => {
isSemanticRoleElement('input', [
JSXAttributeMock('type', 'checkbox'),
JSXAttributeMock('role', 'checkbox'),
JSXAttributeMock('aria-checked', 'false'),
JSXAttributeMock('aria-labelledby', 'foo'),
JSXAttributeMock('tabindex', '0'),
{
type: 'JSXSpreadAttribute',
argument: {
type: 'Identifier',
name: 'props',
},
},
]);
},
'does not throw on JSXSpreadAttribute',
);
t.end();
});

View File

@@ -0,0 +1,219 @@
import test from 'tape';
import mayContainChildComponent from '../../../src/util/mayContainChildComponent';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock';
test('mayContainChildComponent', (t) => {
t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
'FancyComponent',
5,
),
false,
'no FancyComponent returns false',
);
t.test('contains an indicated component', (st) => {
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('input'),
]),
'input',
),
true,
'returns true',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'FancyComponent',
),
true,
'returns true',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
]),
'FancyComponent',
),
false,
'FancyComponent is outside of default depth, should return false',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
]),
'FancyComponent',
2,
),
true,
'FancyComponent is inside of custom depth, should return true',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXElementMock('FancyComponent'),
]),
]),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
'FancyComponent',
6,
),
true,
'deep nesting, returns true',
);
st.end();
});
t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXExpressionContainerMock('mysteryBox'),
]),
'FancyComponent',
),
true,
'Intederminate situations + expression container children - returns true',
);
t.test('Glob name matching - component name contains question mark ? - match any single character', (st) => {
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'Fanc?Co??onent',
),
true,
'returns true',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'FancyComponent?',
),
false,
'returns false',
);
st.test('component name contains asterisk * - match zero or more characters', (s2t) => {
s2t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'Fancy*',
),
true,
'returns true',
);
s2t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'*Component',
),
true,
'returns true',
);
s2t.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('FancyComponent'),
]),
'Fancy*C*t',
),
true,
'returns true',
);
s2t.end();
});
st.end();
});
t.test('using a custom elementType function', (st) => {
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('CustomInput'),
]),
'input',
2,
() => 'input',
),
true,
'returns true when the custom elementType returns the proper name',
);
st.equal(
mayContainChildComponent(
JSXElementMock('div', [], [
JSXElementMock('CustomInput'),
]),
'input',
2,
() => 'button',
),
false,
'returns false when the custom elementType returns a wrong name',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,256 @@
import test from 'tape';
import mayHaveAccessibleLabel from '../../../src/util/mayHaveAccessibleLabel';
import JSXAttributeMock from '../../../__mocks__/JSXAttributeMock';
import JSXElementMock from '../../../__mocks__/JSXElementMock';
import JSXExpressionContainerMock from '../../../__mocks__/JSXExpressionContainerMock';
import JSXSpreadAttributeMock from '../../../__mocks__/JSXSpreadAttributeMock';
import JSXTextMock from '../../../__mocks__/JSXTextMock';
import LiteralMock from '../../../__mocks__/LiteralMock';
test('mayHaveAccessibleLabel', (t) => {
t.equal(
mayHaveAccessibleLabel(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
5,
),
false,
'no label returns false',
);
t.test('label via attributes', (st) => {
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-label', 'A delicate label'),
], [])),
true,
'aria-label returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-label', ''),
], [])),
false,
'aria-label without content returns false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-label', ' '),
], [])),
false,
'aria-label with only spaces whitespace, should return false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-label', '\n'),
], [])),
false,
'aria-label with only newline whitespace, should return false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-labelledby', 'elementId'),
], [])),
true,
'aria-labelledby returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-labelledby', ''),
], [])),
false,
'aria-labelledby without content returns false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('aria-labelledby', 'elementId', true),
], [])),
true,
'aria-labelledby with an expression container, should return true',
);
st.end();
});
t.test('label via custom label attribute', (st) => {
const customLabelProp = 'cowbell';
st.equal(
mayHaveAccessibleLabel(
JSXElementMock('div', [
JSXAttributeMock(customLabelProp, 'A delicate label'),
], []),
1,
[customLabelProp],
),
true,
'aria-label returns true',
);
st.end();
});
t.test('text label', (st) => {
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
LiteralMock('A fancy label'),
])),
true,
'Literal text, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
LiteralMock(' '),
])),
false,
'Literal spaces whitespace, returns false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
LiteralMock('\n'),
])),
false,
'Literal newline whitespace, returns false',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXTextMock('A fancy label'),
])),
true,
'JSXText, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXTextMock('A fancy label'),
]),
])),
false,
'label is outside of default depth, returns false',
);
st.equal(
mayHaveAccessibleLabel(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXTextMock('A fancy label'),
]),
]),
2,
),
true,
'label is inside of custom depth, returns true',
);
st.equal(
mayHaveAccessibleLabel(
JSXElementMock('div', [], [
JSXElementMock('div', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], []),
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXElementMock('span', [], [
JSXTextMock('A fancy label'),
]),
]),
]),
]),
]),
JSXElementMock('span', [], []),
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
]),
6,
),
true,
'deep nesting, returns true',
);
st.end();
});
t.test('image content', (st) => {
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
]),
])),
false,
'without alt, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
JSXAttributeMock('alt', 'A sensible label'),
]),
])),
true,
'with alt, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXElementMock('img', [
JSXAttributeMock('src', 'some/path'),
JSXAttributeMock('aria-label', 'A sensible label'),
]),
])),
true,
'with aria-label, returns true',
);
st.end();
});
t.test('Intederminate situations', (st) => {
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [], [
JSXExpressionContainerMock('mysteryBox'),
])),
true,
'expression container children, returns true',
);
st.equal(
mayHaveAccessibleLabel(JSXElementMock('div', [
JSXAttributeMock('style', 'some-junk'),
JSXSpreadAttributeMock('props'),
], [])),
true,
'spread operator in attributes, returns true',
);
st.end();
});
t.end();
});

View File

@@ -0,0 +1,93 @@
import { version as eslintVersion } from 'eslint/package.json';
import test from 'tape';
import semver from 'semver';
import parserOptionsMapper from '../../__util__/parserOptionsMapper';
const usingLegacy = semver.major(eslintVersion) < 9;
test('parserOptionsMapper', (t) => {
const expectedResult = usingLegacy
? {
code: '<div />',
errors: [],
options: {},
parserOptions: {
ecmaVersion: 2018,
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
settings: {},
}
: {
code: '<div />',
errors: [],
options: {},
languageOptions: {
ecmaVersion: 'latest',
parserOptions: {
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
},
settings: {},
};
t.deepEqual(
parserOptionsMapper({
code: '<div />',
errors: [],
options: {},
}),
expectedResult,
'returns a test case object',
);
const expectedResult2 = usingLegacy
? {
code: '<div />',
errors: [],
options: {},
parserOptions: {
ecmaVersion: 5,
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
settings: {},
}
: {
code: '<div />',
errors: [],
options: {},
languageOptions: {
ecmaVersion: 5,
parserOptions: {
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true,
},
},
},
settings: {},
};
t.deepEqual(
parserOptionsMapper({
code: '<div />',
errors: [],
options: {},
languageOptions: {
ecmaVersion: 5,
},
}),
expectedResult2,
'allows for overriding parserOptions',
);
t.end();
});

View File

@@ -0,0 +1,35 @@
import test from 'tape';
import { generateObjSchema, arraySchema, enumArraySchema } from '../../../src/util/schemas';
test('schemas', (t) => {
t.test('should generate an object schema with correct properties', (st) => {
const schema = generateObjSchema({
foo: 'bar',
baz: arraySchema,
});
const properties = schema.properties || {};
st.deepEqual(properties.foo, properties.foo, 'bar');
st.deepEqual(properties.baz.type, 'array');
st.end();
});
t.deepEqual(
enumArraySchema(),
{
additionalItems: false,
items: {
enum: [],
type: 'string',
},
minItems: 0,
type: 'array',
uniqueItems: true,
},
'enumArraySchema works with no arguments',
);
t.end();
});

View File

@@ -0,0 +1,30 @@
# jsx-a11y/accessible-emoji
❌ This rule is deprecated.
<!-- end auto-generated rule header -->
Emoji have become a common way of communicating content to the end user. To a person using a screenreader, however, they 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.
## 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>
```
## Accessibility guidelines
- [WCAG 1.1.1](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html)
### Resources
- [Léonie Watson, Accessible Emoji](https://tink.uk/accessible-emoji/)

View File

@@ -0,0 +1,168 @@
# jsx-a11y/alt-text
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
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 screen reader 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>`.
## 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=""`).
```jsx
<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 options
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/alt-text": [ 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} />
```
## Accessibility guidelines
- [WCAG 1.1.1](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html)
### Resources
- [axe-core, object-alt](https://dequeuniversity.com/rules/axe/3.2/object-alt)
- [axe-core, image-alt](https://dequeuniversity.com/rules/axe/3.2/image-alt)
- [axe-core, input-image-alt](https://dequeuniversity.com/rules/axe/3.2/input-image-alt)
- [axe-core, area-alt](https://dequeuniversity.com/rules/axe/3.2/area-alt)

View File

@@ -0,0 +1,91 @@
# jsx-a11y/anchor-ambiguous-text
🚫 This rule is _disabled_ in the ☑️ `recommended` config.
<!-- end auto-generated rule header -->
Enforces `<a>` values are not exact matches for the phrases "click here", "here", "link", "a link", or "learn more". Screenreaders announce tags as links/interactive, but rely on values for context. Ambiguous anchor descriptions do not provide sufficient context for users.
## Rule options
This rule takes one optional object argument with the parameter `words`.
```json
{
"rules": {
"jsx-a11y/anchor-ambiguous-text": [2, {
"words": ["click this"],
}],
}
}
```
The `words` option allows users to modify the strings that can be checked for in the anchor text. Useful for specifying other words in other languages. The default value is set by `DEFAULT_AMBIGUOUS_WORDS`:
```js
const DEFAULT_AMBIGUOUS_WORDS = ['click here', 'here', 'link', 'a link', 'learn more'];
```
The logic to calculate the inner text of an anchor is as follows:
- if an element has the `aria-label` property, its value is used instead of the inner text
- if an element has `aria-hidden="true`, it is skipped over
- if an element is `<img />` or configured to be interpreted like one, its `alt` value is used as its inner text
Note that this rule still disallows ambiguous `aria-label` or `alt` values.
Note that this rule is case-insensitive, trims whitespace, and ignores certain punctuation (`[,.?¿!‽¡;:]`). It only looks for **exact matches**.
### Succeed
```jsx
<a>read this tutorial</a> // passes since it is not one of the disallowed words
<a>${here}</a> // this is valid since 'here' is a variable name
<a aria-label="tutorial on using eslint-plugin-jsx-a11y">click here</a> // the aria-label supersedes the inner text
```
### Fail
```jsx
<a>here</a>
<a>HERE</a>
<a>link</a>
<a>click here</a>
<a>learn more</a>
<a>learn more.</a>
<a>learn more,</a>
<a>learn more?</a>
<a>learn more!</a>
<a>learn more:</a>
<a>learn more;</a>
<a>a link</a>
<a> a link </a>
<a><span> click </span> here</a> // goes through element children
<a>a<i></i> link</a>
<a><i></i>a link</a>
<a><span aria-hidden="true">more text</span>learn more</a> // skips over elements with aria-hidden=true
<a aria-label="click here">something</a> // the aria-label here is inaccessible
<a><img alt="click here"/></a> // the alt tag is still ambiguous
<a alt="tutorial on using eslint-plugin-jsx-a11y">click here</a> // the alt tag is only parsed on img
```
## Accessibility guidelines
Ensure anchor tags describe the content of the link, opposed to simply describing them as a link.
Compare
```jsx
<p><a href="#">click here</a> to read a tutorial by Foo Bar</p>
```
which can be more concise and accessible with
```jsx
<p>read <a href="#">a tutorial by Foo Bar</a></p>
```
### Resources
1. [WebAIM, Hyperlinks](https://webaim.org/techniques/hypertext/)
2. [Deque University, Link Checklist - 'Avoid "link" (or similar) in the link text'](https://dequeuniversity.com/checklists/web/links)

View File

@@ -0,0 +1,64 @@
# jsx-a11y/anchor-has-content
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
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.
Alternatively, you may use the `title` prop or the `aria-label` prop.
## Rule options
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' }} />
<a title='foo' />
<a aria-label='foo' />
```
### Fail
```jsx
<a />
<a><TextWrapper aria-hidden /></a>
```
## Accessibility guidelines
- [WCAG 2.4.4](https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context)
- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)
### Resources
- [axe-core, link-name](https://dequeuniversity.com/rules/axe/3.2/link-name)

View File

@@ -0,0 +1,270 @@
# jsx-a11y/anchor-is-valid
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
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 from a `<span>`.
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.
### Case: I use Next.js and I'm getting this error inside of `<Link>`s
This is a [known issue](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/402) with Next.js's decision to construct internal links by nesting an href-free `<a>` tag inside of a `<Link>` component. Next.js is also [aware of the issue](https://github.com/vercel/next.js/issues/5533) and has an [RFC](https://github.com/vercel/next.js/discussions/8207) working towards a solution.
Until the Next.js API can be updated to a more performant and standard setup, you have a few workaround options:
1. If you have only a few `Link`s, or they're clustered in just a few files like `nav.tsx`, you can use disable macros like `{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}` to turn off validation of this rule for those usages.
2. You can use the `Link` component's `passHref` prop to override a dummy `href` on the `<a>`:
```typescript
<Link href="/my-amazing-page" passHref>
<a href="replace">Go to my amazing page</a>
</Link>
```
3. You can invest in a custom component that wraps the creation of the `Link` and `a`. You can then add your new custom component to the list of components to validate to ensure that your links are all created with a navigable href. A sample custom component is shared [here](https://gist.github.com/zackdotcomputer/d7af9901e7db87364aad7fbfadb5c99b) and it would be used like this:
```typescript
// Internally, LinkTo handles the making of the Link and A, collecting the
// need for a lint workaround into a single file.
// Externally, LinkTo can be linted using this rule, ensuring it will always
// have a valid href prop.
<LinkTo href="/my-amazing-page">Go to my amazing page</LinkTo>
```
### Case: I understand the previous cases but still need an element resembling a link that is purely clickable
We recommend, without reserve, that elements resembling anchors should navigate. This will provide a superior user experience to a larger group of users out there.
However, we understand that developers are not always in total control of the visual design of web applications. In cases where it is imperative to provide an element resembling an anchor that purely acts as a click target with no navigation as result, we would like to recommend a compromise.
Again change the element to a `<button>`:
```jsx
<button
type="button"
className="link-button"
onClick={() => this.setState({ showSomething: true })}
>
Press me, I look like a link
</button>
```
Then use styling to change its appearance to that of a link:
```css
.link-button {
background-color: transparent;
border: none;
cursor: pointer;
text-decoration: underline;
display: inline;
margin: 0;
padding: 0;
}
```
This button element can now also be used inline in text.
Once again we stress that this is an inferior implementation and some users will encounter difficulty to use your website, however, it will allow a larger group of people to interact with your website than the alternative of ignoring the rule's warning.
## Rule options
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)`} />
```
## Accessibility guidelines
- [WCAG 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard)
### Resources
- [WebAIM - Introduction to Links and Hypertext](https://webaim.org/techniques/hypertext/)
- [Links vs. Buttons in Modern Web Applications](https://marcysutton.com/links-vs-buttons-in-modern-web-applications/)
- [Using ARIA - Notes on ARIA use in HTML](https://www.w3.org/TR/using-aria/#NOTES)

View File

@@ -0,0 +1,52 @@
# jsx-a11y/aria-activedescendant-has-tabindex
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
`aria-activedescendant` is used to manage focus within a [composite widget](https://www.w3.org/TR/wai-aria/#composite).
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` 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} />
<div aria-activedescendant={someID} tabIndex={-1} />
<div aria-activedescendant={someID} tabIndex="-1" />
<input aria-activedescendant={someID} />
<input aria-activedescendant={someID} tabIndex={0} />
<input aria-activedescendant={someID} tabIndex={-1} />
```
### Fail
```jsx
<div aria-activedescendant={someID} />
```
## Accessibility guidelines
General best practice (reference resources)
### Resources
- [MDN, Using the aria-activedescendant attribute](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-activedescendant_attribute)

View File

@@ -0,0 +1,29 @@
# jsx-a11y/aria-props
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
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/WAI/PF/aria-1.1/states_and_properties).
## 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">
```
## Accessibility guidelines
- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)

View File

@@ -0,0 +1,30 @@
# jsx-a11y/aria-proptypes
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
ARIA state and property values must be valid.
## 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>
```
## Accessibility guidelines
- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)
### Resources
- [ARIA Spec, States and Properties](https://www.w3.org/TR/wai-aria/#states_and_properties)
- [Chrome Audit Rules, AX_ARIA_04](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_04)

View File

@@ -0,0 +1,51 @@
# jsx-a11y/aria-role
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
Elements with ARIA roles must use a valid, non-abstract ARIA role. A reference to role definitions can be found at [WAI-ARIA](https://www.w3.org/TR/wai-aria/#role_definitions) site.
## Rule options
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/aria-role": [ 2, {
"allowedInvalidRoles": ["text"],
"ignoreNonDOM": true
}],
}
}
```
`allowedInvalidRules` is an optional string array of custom roles that should be allowed in addition to the ARIA spec, such as for cases when you [need to use a non-standard role](https://axesslab.com/text-splitting).
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 -->
```
## Accessibility guidelines
- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)
### Resources
- [Chrome Audit Rules, 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/)
- [MDN: Using ARIA: Roles, states, and properties](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques)

View File

@@ -0,0 +1,30 @@
# jsx-a11y/aria-unsupported-elements
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
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.
## 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" />
```
## Accessibility guidelines
- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)
### Resources
- [Chrome Audit Rules, AX_ARIA_12](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_12)
- [DPUB-ARIA roles](https://www.w3.org/TR/dpub-aria-1.0/)

View File

@@ -0,0 +1,49 @@
# jsx-a11y/autocomplete-valid
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
Ensure the autocomplete attribute is correct and suitable for the form field it is used with.
## Rule options
This rule takes one optional object argument of type object:
```
{
"rules": {
"jsx-a11y/autocomplete-valid": [ 2, {
"inputComponents": ["Input", "FormField"]
}],
}
}
```
### Succeed
```jsx
<!-- Good: the autocomplete attribute is used according to the HTML specification -->
<input type="text" autocomplete="name" />
<!-- Good: MyInput is not listed in inputComponents -->
<MyInput autocomplete="incorrect" />
```
### Fail
```jsx
<!-- Bad: the autocomplete attribute has an invalid value -->
<input type="text" autocomplete="incorrect" />
<!-- Bad: the autocomplete attribute is on an inappropriate input element -->
<input type="email" autocomplete="url" />
<!-- Bad: MyInput is listed in inputComponents -->
<MyInput autocomplete="incorrect" />
```
## Accessibility guidelines
- [WCAG 1.3.5](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose)
### Resources
- [axe-core, autocomplete-valid](https://dequeuniversity.com/rules/axe/3.2/autocomplete-valid)
- [HTML 5.2, Autocomplete requirements](https://www.w3.org/TR/html52/sec-forms.html#autofilling-form-controls-the-autocomplete-attribute)

View File

@@ -0,0 +1,28 @@
# jsx-a11y/click-events-have-key-events
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
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. This does not apply for interactive or hidden elements.
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<div onClick={() => {}} onKeyDown={this.handleKeyDown} />
<div onClick={() => {}} onKeyUp={this.handleKeyUp} />
<div onClick={() => {}} onKeyPress={this.handleKeyPress} />
<button onClick={() => {}} />
<div onClick{() => {}} aria-hidden="true" />
```
### Fail
```jsx
<div onClick={() => {}} />
```
## Accessibility guidelines
- [WCAG 2.1.1](https://www.w3.org/WAI/WCAG21/Understanding/keyboard)

View File

@@ -0,0 +1,113 @@
# jsx-a11y/control-has-associated-label
🚫 This rule is _disabled_ in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
Enforce that a control (an interactive element) has a text label.
There are two supported ways to supply a control with a text label:
- Provide text content inside the element.
- Use the `aria-label` attribute on the element, with a text value.
- Use the `aria-labelledby` attribute on the element, and point the IDREF value to an element with an accessible label.
- Alternatively, with an `img` tag, you may use the `alt` attribute to supply a text description of the image.
The rule is permissive in the sense that it will assume that expressions will eventually provide a label. So an element like this will pass.
```jsx
<button type="button">{maybeSomethingThatContainsALabel}</button>
```
## How do I resolve this error?
### Case: I have a simple button that requires a label.
Provide text content in the `button` element.
```jsx
<button type="button">Save</button>
```
### Case: I have an icon button and I don't want visible text.
Use the `aria-label` attribute and provide the text label as the value.
```jsx
<button type="button" aria-label="Save" class="icon-save" />
```
### Case: The label for my element is already located on the page and I don't want to repeat the text in my source code.
Use the `aria-labelledby` attribute and point the IDREF value to an element with an accessible label.
```jsx
<div id="js_1">Comment</div>
<textarea aria-labelledby="js_1"></textarea>
```
### Case: My label and input components are custom components, but I still want to require that they have an accessible text label.
You can configure the rule to be aware of your custom components. Refer to the Rule Details below.
```jsx
<CustomInput label="Surname" type="text" value={value} />
```
## Rule options
This rule takes one optional object argument of type object:
```json
{
"rules": {
"jsx-a11y/control-has-associated-label": [ 2, {
"labelAttributes": ["label"],
"controlComponents": ["CustomComponent"],
"ignoreElements": [
"audio",
"canvas",
"embed",
"input",
"textarea",
"tr",
"video",
],
"ignoreRoles": [
"grid",
"listbox",
"menu",
"menubar",
"radiogroup",
"row",
"tablist",
"toolbar",
"tree",
"treegrid",
],
"depth": 3,
}],
}
}
```
- `labelAttributes` is a list of attributes to check on the control component and its children for a label. Use this if you have a custom component that uses a string passed on a prop to render an HTML `label`, for example.
- `controlComponents` is a list of custom React Components names that will render down to an interactive element.
- `ignoreElements` is an array of elements that should not be considered control (interactive) elements and therefore they do not require a text label.
- `ignoreRoles` is an array of ARIA roles that should not be considered control (interactive) roles and therefore they do not require a text label.
- `depth` (default 2, max 25) is an integer that determines how deep within a `JSXElement` the rule should look for text content or an element with a label to determine if the interactive element will have an accessible label.
### Succeed
```jsx
<button type="button" aria-label="Save" class="icon-save" />
```
### Fail
```jsx
<button type="button" class="icon-save" />
```
## Accessibility guidelines
- [WCAG 1.3.1](https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships)
- [WCAG 3.3.2](https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions)
- [WCAG 4.1.2](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value)

View File

@@ -0,0 +1,67 @@
# jsx-a11y/heading-has-content
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
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.
## Rule options
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 />
```
## Accessibility guidelines
- [WCAG 2.4.6](https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-descriptive.html)
### Resources
- [axe-core, empty-heading](https://dequeuniversity.com/rules/axe/3.2/empty-heading)

View File

@@ -0,0 +1,31 @@
# jsx-a11y/html-has-lang
💼 This rule is enabled in the following configs: ☑️ `recommended`, 🔒 `strict`.
<!-- end auto-generated rule header -->
<html> elements must have the lang prop. This rule is largely superseded by the [`lang` rule](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/lang.md).
## Rule details
This rule takes no arguments.
### Succeed
```jsx
<html lang="en">
<html lang="en-US">
<html lang={language}>
```
### Fail
```jsx
<html>
```
## Accessibility guidelines
- [WCAG 3.1.1](https://www.w3.org/WAI/WCAG21/Understanding/language-of-page)
### Resources
- [axe-core, html-has-lang](https://dequeuniversity.com/rules/axe/3.2/html-has-lang)
- [axe-core, html-lang-valid](https://dequeuniversity.com/rules/axe/3.2/html-lang-valid)

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