From b3559cf89be6b5352cd77ffa025831b3d793d565 Mon Sep 17 00:00:00 2001 From: Daniel Harte Date: Wed, 5 Jun 2024 20:41:33 +0100 Subject: [PATCH 01/11] [Deps] unpin `axe-core` previously =4.7.0, now ^4.9.1 see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/989 and https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/792 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03be59c6..a06930d1 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "array-includes": "^3.1.7", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", - "axe-core": "=4.7.0", + "axe-core": "^4.9.1", "axobject-query": "^3.2.1", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", From 1271ac1d6e5dcf9a2bc2c086faaf062335629171 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 10 Jun 2024 09:27:21 -0700 Subject: [PATCH 02/11] [Dev Deps] update `@babel/cli`, `@babel/core`, `@babel/eslint-parser`, `@babel/plugin-transform-flow-strip-types`, `@babel/register`, `eslint-doc-generator`, `object.entries` --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index a06930d1..b39b0af2 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,11 @@ "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" }, "devDependencies": { - "@babel/cli": "^7.23.4", - "@babel/core": "^7.23.7", - "@babel/eslint-parser": "^7.23.3", - "@babel/plugin-transform-flow-strip-types": "^7.23.3", - "@babel/register": "^7.23.7", + "@babel/cli": "^7.24.7", + "@babel/core": "^7.24.7", + "@babel/eslint-parser": "^7.24.7", + "@babel/plugin-transform-flow-strip-types": "^7.24.7", + "@babel/register": "^7.24.6", "aud": "^2.0.4", "auto-changelog": "^2.4.0", "babel-jest": "^24.9.0", @@ -49,7 +49,7 @@ "babel-preset-airbnb": "^5.0.0", "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint-config-airbnb-base": "^15.0.0", - "eslint-doc-generator": "^1.6.1", + "eslint-doc-generator": "^1.7.1", "eslint-plugin-eslint-plugin": "^4.3.0", "eslint-plugin-flowtype": "^5.8.0 || ^8.0.3", "eslint-plugin-import": "^2.29.1", @@ -63,7 +63,7 @@ "minimist": "^1.2.8", "npmignore": "^0.3.1", "object.assign": "^4.1.5", - "object.entries": "^1.1.7", + "object.entries": "^1.1.8", "rimraf": "^3.0.2", "safe-publish-latest": "^2.0.0", "semver": "^6.3.1", From 5d1440825a8838ae10dc94cc3a4a7e1e967644b4 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 10 Jun 2024 09:28:24 -0700 Subject: [PATCH 03/11] [Deps] update `@babel/runtime`, `array-includes`, `es-iterator-helpers`, `hasown`, `object.fromentries`, `safe-regex-test` --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b39b0af2..382b6828 100644 --- a/package.json +++ b/package.json @@ -74,22 +74,22 @@ }, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.23.9", + "@babel/runtime": "^7.24.7", "aria-query": "^5.3.0", - "array-includes": "^3.1.7", + "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.9.1", "axobject-query": "^3.2.1", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.15", - "hasown": "^2.0.0", + "es-iterator-helpers": "^1.0.19", + "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "safe-regex-test": "^1.0.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.0" }, "peerDependencies": { From 51a1ca7b4d83d4fbd1ea62888f7f2dc21ece6788 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 19 Jun 2024 11:05:48 -0700 Subject: [PATCH 04/11] [actions] update actions/checkout --- .github/workflows/node-4+.yml | 4 ++-- .github/workflows/node-pretest.yml | 6 +++--- .github/workflows/readme.yml | 2 +- .github/workflows/rebase.yml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/node-4+.yml b/.github/workflows/node-4+.yml index c9bd83fa..0b0ec9ed 100644 --- a/.github/workflows/node-4+.yml +++ b/.github/workflows/node-4+.yml @@ -88,7 +88,7 @@ jobs: eslint: 3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: ljharb/actions/node/install@main @@ -109,4 +109,4 @@ jobs: needs: [latest] runs-on: ubuntu-latest steps: - - run: 'echo tests completed' + - run: true diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml index bf15f773..e501df6b 100644 --- a/.github/workflows/node-pretest.yml +++ b/.github/workflows/node-pretest.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ljharb/actions/node/install@main name: 'nvm install lts/* && npm install' with: @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ljharb/actions/node/install@main name: 'nvm install lts/* && npm install' with: @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ljharb/actions/node/install@main name: 'nvm install lts/* && npm install' with: diff --git a/.github/workflows/readme.yml b/.github/workflows/readme.yml index 6a60003a..75530d42 100644 --- a/.github/workflows/readme.yml +++ b/.github/workflows/readme.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ljharb/actions/node/install@main name: 'nvm install lts/* && npm install' with: diff --git a/.github/workflows/rebase.yml b/.github/workflows/rebase.yml index a4d47c74..ead8680f 100644 --- a/.github/workflows/rebase.yml +++ b/.github/workflows/rebase.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ljharb/rebase@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 6b5f096f10b47326d68e2893152a48a79c8555b4 Mon Sep 17 00:00:00 2001 From: michael faith Date: Mon, 17 Jun 2024 12:17:50 -0500 Subject: [PATCH 05/11] [New] add support for Flat Config This change adds support for ESLint's new Flat config system. It maintains backwards compatibility with eslintrc style configs as well. To achieve this, we're now dynamically creating four configs: two are the original `recommended` and `strict`, and the other two are the new `flat/recommended` and `flat/strict`. The two `flat` ones are setup with the new config format, while the original two have the same options as before. Usage Legacy ```json { "extends": ["plugin:jsx-a11y/recommended"] } ``` Flat ```js import globals from 'globals'; import js from '@eslint/js'; import jsxA11y from 'eslint-plugin-jsx-a11y'; export default [ js.configs.recommended, jsxA11y.flatConfigs.recommended, { files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], languageOptions: { ecmaVersion: 'latest', sourceType: 'module', globals: globals.browser, }, ignores: ['dist', 'eslint.config.js'], rules: { 'no-unused-vars': 'warn', 'jsx-a11y/anchor-ambiguous-text': 'warn', 'jsx-a11y/anchor-is-valid': 'warn', }, }, ]; ``` --- .eslintrc | 1 + .github/workflows/node-pretest.yml | 12 + README.md | 93 +++- examples/flat-cjs/eslint.config.cjs | 22 + examples/flat-cjs/index.html | 13 + examples/flat-cjs/package.json | 20 + examples/flat-cjs/public/vite.svg | 1 + examples/flat-cjs/src/App.css | 42 ++ examples/flat-cjs/src/App.jsx | 36 ++ examples/flat-cjs/src/assets/react.svg | 1 + examples/flat-cjs/src/index.css | 68 +++ examples/flat-cjs/src/main.jsx | 10 + examples/flat-esm/eslint.config.js | 22 + examples/flat-esm/index.html | 13 + examples/flat-esm/package.json | 20 + examples/flat-esm/public/vite.svg | 1 + examples/flat-esm/src/App.css | 42 ++ examples/flat-esm/src/App.jsx | 36 ++ examples/flat-esm/src/assets/react.svg | 1 + examples/flat-esm/src/index.css | 68 +++ examples/flat-esm/src/main.jsx | 10 + examples/legacy/.eslintrc.cjs | 17 + examples/legacy/index.html | 13 + examples/legacy/package.json | 18 + examples/legacy/public/vite.svg | 1 + examples/legacy/src/App.css | 42 ++ examples/legacy/src/App.jsx | 36 ++ examples/legacy/src/assets/react.svg | 1 + examples/legacy/src/index.css | 68 +++ examples/legacy/src/main.jsx | 10 + package.json | 10 +- src/configs/flat-config-base.js | 9 + src/configs/legacy-config-base.js | 7 + src/index.js | 602 +++++++++++++------------ 34 files changed, 1073 insertions(+), 293 deletions(-) create mode 100644 examples/flat-cjs/eslint.config.cjs create mode 100644 examples/flat-cjs/index.html create mode 100644 examples/flat-cjs/package.json create mode 100644 examples/flat-cjs/public/vite.svg create mode 100644 examples/flat-cjs/src/App.css create mode 100644 examples/flat-cjs/src/App.jsx create mode 100644 examples/flat-cjs/src/assets/react.svg create mode 100644 examples/flat-cjs/src/index.css create mode 100644 examples/flat-cjs/src/main.jsx create mode 100644 examples/flat-esm/eslint.config.js create mode 100644 examples/flat-esm/index.html create mode 100644 examples/flat-esm/package.json create mode 100644 examples/flat-esm/public/vite.svg create mode 100644 examples/flat-esm/src/App.css create mode 100644 examples/flat-esm/src/App.jsx create mode 100644 examples/flat-esm/src/assets/react.svg create mode 100644 examples/flat-esm/src/index.css create mode 100644 examples/flat-esm/src/main.jsx create mode 100644 examples/legacy/.eslintrc.cjs create mode 100644 examples/legacy/index.html create mode 100644 examples/legacy/package.json create mode 100644 examples/legacy/public/vite.svg create mode 100644 examples/legacy/src/App.css create mode 100644 examples/legacy/src/App.jsx create mode 100644 examples/legacy/src/assets/react.svg create mode 100644 examples/legacy/src/index.css create mode 100644 examples/legacy/src/main.jsx create mode 100644 src/configs/flat-config-base.js create mode 100644 src/configs/legacy-config-base.js diff --git a/.eslintrc b/.eslintrc index 7024a688..67859870 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,6 +7,7 @@ "ignorePatterns": [ "lib/", "reports/", + "examples/", ], "parser": "@babel/eslint-parser", "plugins": [ diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml index e501df6b..3a10687f 100644 --- a/.github/workflows/node-pretest.yml +++ b/.github/workflows/node-pretest.yml @@ -38,3 +38,15 @@ jobs: node-version: 'lts/*' skip-ls-check: true - run: npm run posttest + + examples: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ljharb/actions/node/install@main + name: 'nvm install lts/* && npm install' + with: + node-version: 'lts/*' + skip-ls-check: true + - run: npm run test:examples \ No newline at end of file diff --git a/README.md b/README.md index 02f1ed5c..5f66a744 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,8 @@ yarn add eslint-plugin-jsx-a11y --dev **Note:** If you installed ESLint globally (using the `-g` flag in npm, or the `global` prefix in yarn) then you must also install `eslint-plugin-jsx-a11y` globally. -## Usage + +## Usage - Legacy Config (`.eslintrc`) Add `jsx-a11y` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix: @@ -109,6 +110,94 @@ Add `plugin:jsx-a11y/recommended` or `plugin:jsx-a11y/strict` in `extends`: } ``` +## 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 config do 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 jsxA11yRecommended = 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. @@ -124,7 +213,7 @@ For example, if you set the `polymorphicPropName` setting to `as` then this elem will be evaluated as an `h3`. If no `polymorphicPropName` is set, then the component will be evaluated as `Box`. -⚠️ Polymorphic components can make code harder to maintain; please use this feature with caution. +⚠️ Polymorphic components can make code harder to maintain; please use this feature with caution. ## Supported Rules diff --git a/examples/flat-cjs/eslint.config.cjs b/examples/flat-cjs/eslint.config.cjs new file mode 100644 index 00000000..fb9c0715 --- /dev/null +++ b/examples/flat-cjs/eslint.config.cjs @@ -0,0 +1,22 @@ +const globals = require('globals'); +const js = require('@eslint/js'); +const jsxA11y = require('eslint-plugin-jsx-a11y'); + +module.exports = [ + js.configs.recommended, + jsxA11y.flatConfigs.recommended, + { + files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: globals.browser, + }, + ignores: ['dist', 'eslint.config.cjs'], + rules: { + 'no-unused-vars': 'off', + 'jsx-a11y/anchor-ambiguous-text': 'warn', + 'jsx-a11y/anchor-is-valid': 'warn', + }, + }, +]; diff --git a/examples/flat-cjs/index.html b/examples/flat-cjs/index.html new file mode 100644 index 00000000..0c589ecc --- /dev/null +++ b/examples/flat-cjs/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/examples/flat-cjs/package.json b/examples/flat-cjs/package.json new file mode 100644 index 00000000..d89c7f08 --- /dev/null +++ b/examples/flat-cjs/package.json @@ -0,0 +1,20 @@ +{ + "name": "flat-cjs", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint . --report-unused-disable-directives" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.5.0", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../..", + "globals": "^15.6.0" + } +} diff --git a/examples/flat-cjs/public/vite.svg b/examples/flat-cjs/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/flat-cjs/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/flat-cjs/src/App.css b/examples/flat-cjs/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/examples/flat-cjs/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/examples/flat-cjs/src/App.jsx b/examples/flat-cjs/src/App.jsx new file mode 100644 index 00000000..b2d55881 --- /dev/null +++ b/examples/flat-cjs/src/App.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import reactLogo from './assets/react.svg'; +import viteLogo from '/vite.svg'; +import './App.css'; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/examples/flat-cjs/src/assets/react.svg b/examples/flat-cjs/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/examples/flat-cjs/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/flat-cjs/src/index.css b/examples/flat-cjs/src/index.css new file mode 100644 index 00000000..6119ad9a --- /dev/null +++ b/examples/flat-cjs/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/flat-cjs/src/main.jsx b/examples/flat-cjs/src/main.jsx new file mode 100644 index 00000000..54b39dd1 --- /dev/null +++ b/examples/flat-cjs/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/examples/flat-esm/eslint.config.js b/examples/flat-esm/eslint.config.js new file mode 100644 index 00000000..ae5afaa7 --- /dev/null +++ b/examples/flat-esm/eslint.config.js @@ -0,0 +1,22 @@ +import globals from 'globals'; +import js from '@eslint/js'; +import jsxA11y from 'eslint-plugin-jsx-a11y'; + +export default [ + js.configs.recommended, + jsxA11y.flatConfigs.recommended, + { + files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: globals.browser, + }, + ignores: ['dist', 'eslint.config.js'], + rules: { + 'no-unused-vars': 'off', + 'jsx-a11y/anchor-ambiguous-text': 'warn', + 'jsx-a11y/anchor-is-valid': 'warn', + }, + }, +]; diff --git a/examples/flat-esm/index.html b/examples/flat-esm/index.html new file mode 100644 index 00000000..0c589ecc --- /dev/null +++ b/examples/flat-esm/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/examples/flat-esm/package.json b/examples/flat-esm/package.json new file mode 100644 index 00000000..cf10c817 --- /dev/null +++ b/examples/flat-esm/package.json @@ -0,0 +1,20 @@ +{ + "name": "flat-esm", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=true eslint . --report-unused-disable-directives" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.5.0", + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../..", + "globals": "^15.6.0" + } +} diff --git a/examples/flat-esm/public/vite.svg b/examples/flat-esm/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/flat-esm/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/flat-esm/src/App.css b/examples/flat-esm/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/examples/flat-esm/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/examples/flat-esm/src/App.jsx b/examples/flat-esm/src/App.jsx new file mode 100644 index 00000000..b2d55881 --- /dev/null +++ b/examples/flat-esm/src/App.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import reactLogo from './assets/react.svg'; +import viteLogo from '/vite.svg'; +import './App.css'; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/examples/flat-esm/src/assets/react.svg b/examples/flat-esm/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/examples/flat-esm/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/flat-esm/src/index.css b/examples/flat-esm/src/index.css new file mode 100644 index 00000000..6119ad9a --- /dev/null +++ b/examples/flat-esm/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/flat-esm/src/main.jsx b/examples/flat-esm/src/main.jsx new file mode 100644 index 00000000..54b39dd1 --- /dev/null +++ b/examples/flat-esm/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/examples/legacy/.eslintrc.cjs b/examples/legacy/.eslintrc.cjs new file mode 100644 index 00000000..1b743411 --- /dev/null +++ b/examples/legacy/.eslintrc.cjs @@ -0,0 +1,17 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: ['eslint:recommended', 'plugin:jsx-a11y/recommended'], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + settings: { react: { version: '18.2' } }, + plugins: ['jsx-a11y'], + rules: { + 'no-unused-vars': 'off', + 'jsx-a11y/anchor-ambiguous-text': 'warn', + 'jsx-a11y/anchor-is-valid': 'warn', + }, +}; diff --git a/examples/legacy/index.html b/examples/legacy/index.html new file mode 100644 index 00000000..0c589ecc --- /dev/null +++ b/examples/legacy/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/examples/legacy/package.json b/examples/legacy/package.json new file mode 100644 index 00000000..35be8148 --- /dev/null +++ b/examples/legacy/package.json @@ -0,0 +1,18 @@ +{ + "name": "legacy", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint . --ext js,jsx --report-unused-disable-directives" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "eslint": "^8.57.0", + "eslint-plugin-jsx-a11y": "file:../.." + } +} diff --git a/examples/legacy/public/vite.svg b/examples/legacy/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/examples/legacy/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/legacy/src/App.css b/examples/legacy/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/examples/legacy/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/examples/legacy/src/App.jsx b/examples/legacy/src/App.jsx new file mode 100644 index 00000000..b2d55881 --- /dev/null +++ b/examples/legacy/src/App.jsx @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import reactLogo from './assets/react.svg'; +import viteLogo from '/vite.svg'; +import './App.css'; + +function App() { + const [count, setCount] = useState(0); + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/examples/legacy/src/assets/react.svg b/examples/legacy/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/examples/legacy/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/legacy/src/index.css b/examples/legacy/src/index.css new file mode 100644 index 00000000..6119ad9a --- /dev/null +++ b/examples/legacy/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/examples/legacy/src/main.jsx b/examples/legacy/src/main.jsx new file mode 100644 index 00000000..54b39dd1 --- /dev/null +++ b/examples/legacy/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + , +) diff --git a/package.json b/package.json index 382b6828..f14e0b68 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,14 @@ "test": "npm run jest", "posttest": "aud --production", "test:ci": "npm run jest -- --ci --runInBand", + "pretest:examples": "npm run build", + "test:examples": "npm run test-example:legacy && npm run test-example:flat-esm && npm run test-example:flat-cjs", + "test-example:legacy": "cd examples/legacy && npm install && npm run lint", + "test-example:flat-esm": "cd examples/flat-esm && npm install && npm run lint", + "test-example:flat-cjs": "cd examples/flat-cjs && npm install && npm run lint", "jest": "jest --coverage __tests__/**/*", "pregenerate-list-of-rules": "npm run build", - "generate-list-of-rules": "eslint-doc-generator --rule-doc-title-format prefix-name --rule-doc-section-options false --config-emoji recommended,☑️", + "generate-list-of-rules": "eslint-doc-generator --rule-doc-title-format prefix-name --rule-doc-section-options false --config-emoji recommended,☑️ --ignore-config flat/recommended --ignore-config flat/strict", "generate-list-of-rules:check": "npm run generate-list-of-rules -- --check", "version": "auto-changelog && git add CHANGELOG.md", "postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\"" @@ -128,7 +133,8 @@ "/reports", "/flow", "scripts/", - "CONTRIBUTING.md" + "CONTRIBUTING.md", + "/examples" ] } } diff --git a/src/configs/flat-config-base.js b/src/configs/flat-config-base.js new file mode 100644 index 00000000..54b84e5f --- /dev/null +++ b/src/configs/flat-config-base.js @@ -0,0 +1,9 @@ +module.exports = { + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}; diff --git a/src/configs/legacy-config-base.js b/src/configs/legacy-config-base.js new file mode 100644 index 00000000..2d177961 --- /dev/null +++ b/src/configs/legacy-config-base.js @@ -0,0 +1,7 @@ +module.exports = { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, +}; diff --git a/src/index.js b/src/index.js index 752ff612..2fa185fa 100644 --- a/src/index.js +++ b/src/index.js @@ -1,296 +1,320 @@ /* eslint-disable global-require */ +const flatConfigBase = require('./configs/flat-config-base'); +const legacyConfigBase = require('./configs/legacy-config-base'); +const { name, version } = require('../package.json'); -module.exports = { - rules: { - 'accessible-emoji': require('./rules/accessible-emoji'), - 'alt-text': require('./rules/alt-text'), - 'anchor-ambiguous-text': require('./rules/anchor-ambiguous-text'), - 'anchor-has-content': require('./rules/anchor-has-content'), - 'anchor-is-valid': require('./rules/anchor-is-valid'), - 'aria-activedescendant-has-tabindex': require('./rules/aria-activedescendant-has-tabindex'), - 'aria-props': require('./rules/aria-props'), - 'aria-proptypes': require('./rules/aria-proptypes'), - 'aria-role': require('./rules/aria-role'), - 'aria-unsupported-elements': require('./rules/aria-unsupported-elements'), - 'autocomplete-valid': require('./rules/autocomplete-valid'), - 'click-events-have-key-events': require('./rules/click-events-have-key-events'), - 'control-has-associated-label': require('./rules/control-has-associated-label'), - 'heading-has-content': require('./rules/heading-has-content'), - 'html-has-lang': require('./rules/html-has-lang'), - 'iframe-has-title': require('./rules/iframe-has-title'), - 'img-redundant-alt': require('./rules/img-redundant-alt'), - 'interactive-supports-focus': require('./rules/interactive-supports-focus'), - 'label-has-associated-control': require('./rules/label-has-associated-control'), - 'label-has-for': require('./rules/label-has-for'), - lang: require('./rules/lang'), - 'media-has-caption': require('./rules/media-has-caption'), - 'mouse-events-have-key-events': require('./rules/mouse-events-have-key-events'), - 'no-access-key': require('./rules/no-access-key'), - 'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'), - 'no-autofocus': require('./rules/no-autofocus'), - 'no-distracting-elements': require('./rules/no-distracting-elements'), - 'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'), - 'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'), - 'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'), - 'no-noninteractive-tabindex': require('./rules/no-noninteractive-tabindex'), - 'no-onchange': require('./rules/no-onchange'), - 'no-redundant-roles': require('./rules/no-redundant-roles'), - 'no-static-element-interactions': require('./rules/no-static-element-interactions'), - 'prefer-tag-over-role': require('./rules/prefer-tag-over-role'), - 'role-has-required-aria-props': require('./rules/role-has-required-aria-props'), - 'role-supports-aria-props': require('./rules/role-supports-aria-props'), - scope: require('./rules/scope'), - 'tabindex-no-positive': require('./rules/tabindex-no-positive'), - }, - configs: { - recommended: { - plugins: [ - 'jsx-a11y', +const allRules = { + 'accessible-emoji': require('./rules/accessible-emoji'), + 'alt-text': require('./rules/alt-text'), + 'anchor-ambiguous-text': require('./rules/anchor-ambiguous-text'), + 'anchor-has-content': require('./rules/anchor-has-content'), + 'anchor-is-valid': require('./rules/anchor-is-valid'), + 'aria-activedescendant-has-tabindex': require('./rules/aria-activedescendant-has-tabindex'), + 'aria-props': require('./rules/aria-props'), + 'aria-proptypes': require('./rules/aria-proptypes'), + 'aria-role': require('./rules/aria-role'), + 'aria-unsupported-elements': require('./rules/aria-unsupported-elements'), + 'autocomplete-valid': require('./rules/autocomplete-valid'), + 'click-events-have-key-events': require('./rules/click-events-have-key-events'), + 'control-has-associated-label': require('./rules/control-has-associated-label'), + 'heading-has-content': require('./rules/heading-has-content'), + 'html-has-lang': require('./rules/html-has-lang'), + 'iframe-has-title': require('./rules/iframe-has-title'), + 'img-redundant-alt': require('./rules/img-redundant-alt'), + 'interactive-supports-focus': require('./rules/interactive-supports-focus'), + 'label-has-associated-control': require('./rules/label-has-associated-control'), + 'label-has-for': require('./rules/label-has-for'), + lang: require('./rules/lang'), + 'media-has-caption': require('./rules/media-has-caption'), + 'mouse-events-have-key-events': require('./rules/mouse-events-have-key-events'), + 'no-access-key': require('./rules/no-access-key'), + 'no-aria-hidden-on-focusable': require('./rules/no-aria-hidden-on-focusable'), + 'no-autofocus': require('./rules/no-autofocus'), + 'no-distracting-elements': require('./rules/no-distracting-elements'), + 'no-interactive-element-to-noninteractive-role': require('./rules/no-interactive-element-to-noninteractive-role'), + 'no-noninteractive-element-interactions': require('./rules/no-noninteractive-element-interactions'), + 'no-noninteractive-element-to-interactive-role': require('./rules/no-noninteractive-element-to-interactive-role'), + 'no-noninteractive-tabindex': require('./rules/no-noninteractive-tabindex'), + 'no-onchange': require('./rules/no-onchange'), + 'no-redundant-roles': require('./rules/no-redundant-roles'), + 'no-static-element-interactions': require('./rules/no-static-element-interactions'), + 'prefer-tag-over-role': require('./rules/prefer-tag-over-role'), + 'role-has-required-aria-props': require('./rules/role-has-required-aria-props'), + 'role-supports-aria-props': require('./rules/role-supports-aria-props'), + scope: require('./rules/scope'), + 'tabindex-no-positive': require('./rules/tabindex-no-positive'), +}; + +const recommendedRules = { + 'jsx-a11y/alt-text': 'error', + 'jsx-a11y/anchor-ambiguous-text': 'off', // TODO: error + 'jsx-a11y/anchor-has-content': 'error', + 'jsx-a11y/anchor-is-valid': 'error', + 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', + 'jsx-a11y/aria-props': 'error', + 'jsx-a11y/aria-proptypes': 'error', + 'jsx-a11y/aria-role': 'error', + 'jsx-a11y/aria-unsupported-elements': 'error', + 'jsx-a11y/autocomplete-valid': 'error', + 'jsx-a11y/click-events-have-key-events': 'error', + 'jsx-a11y/control-has-associated-label': [ + 'off', + { + ignoreElements: [ + 'audio', + 'canvas', + 'embed', + 'input', + 'textarea', + 'tr', + 'video', + ], + ignoreRoles: [ + 'grid', + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'row', + 'tablist', + 'toolbar', + 'tree', + 'treegrid', + ], + includeRoles: ['alert', 'dialog'], + }, + ], + 'jsx-a11y/heading-has-content': 'error', + 'jsx-a11y/html-has-lang': 'error', + 'jsx-a11y/iframe-has-title': 'error', + 'jsx-a11y/img-redundant-alt': 'error', + 'jsx-a11y/interactive-supports-focus': [ + 'error', + { + tabbable: [ + 'button', + 'checkbox', + 'link', + 'searchbox', + 'spinbutton', + 'switch', + 'textbox', + ], + }, + ], + 'jsx-a11y/label-has-associated-control': 'error', + 'jsx-a11y/label-has-for': 'off', + 'jsx-a11y/media-has-caption': 'error', + 'jsx-a11y/mouse-events-have-key-events': 'error', + 'jsx-a11y/no-access-key': 'error', + 'jsx-a11y/no-autofocus': 'error', + 'jsx-a11y/no-distracting-elements': 'error', + 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ + 'error', + { + tr: ['none', 'presentation'], + canvas: ['img'], + }, + ], + 'jsx-a11y/no-noninteractive-element-interactions': [ + 'error', + { + handlers: [ + 'onClick', + 'onError', + 'onLoad', + 'onMouseDown', + 'onMouseUp', + 'onKeyPress', + 'onKeyDown', + 'onKeyUp', + ], + alert: ['onKeyUp', 'onKeyDown', 'onKeyPress'], + body: ['onError', 'onLoad'], + dialog: ['onKeyUp', 'onKeyDown', 'onKeyPress'], + iframe: ['onError', 'onLoad'], + img: ['onError', 'onLoad'], + }, + ], + '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', + 'menuitemradio', + 'menuitemcheckbox', + 'option', + 'row', + 'tab', + 'treeitem', + ], + table: ['grid'], + td: ['gridcell'], + fieldset: ['radiogroup', 'presentation'], + }, + ], + 'jsx-a11y/no-noninteractive-tabindex': [ + 'error', + { + tags: [], + roles: ['tabpanel'], + allowExpressionValues: true, + }, + ], + 'jsx-a11y/no-redundant-roles': 'error', + 'jsx-a11y/no-static-element-interactions': [ + 'error', + { + allowExpressionValues: true, + handlers: [ + 'onClick', + 'onMouseDown', + 'onMouseUp', + 'onKeyPress', + 'onKeyDown', + 'onKeyUp', ], - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - rules: { - 'jsx-a11y/alt-text': 'error', - 'jsx-a11y/anchor-ambiguous-text': 'off', // TODO: error - 'jsx-a11y/anchor-has-content': 'error', - 'jsx-a11y/anchor-is-valid': 'error', - 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', - 'jsx-a11y/aria-props': 'error', - 'jsx-a11y/aria-proptypes': 'error', - 'jsx-a11y/aria-role': 'error', - 'jsx-a11y/aria-unsupported-elements': 'error', - 'jsx-a11y/autocomplete-valid': 'error', - 'jsx-a11y/click-events-have-key-events': 'error', - 'jsx-a11y/control-has-associated-label': ['off', { - ignoreElements: [ - 'audio', - 'canvas', - 'embed', - 'input', - 'textarea', - 'tr', - 'video', - ], - ignoreRoles: [ - 'grid', - 'listbox', - 'menu', - 'menubar', - 'radiogroup', - 'row', - 'tablist', - 'toolbar', - 'tree', - 'treegrid', - ], - includeRoles: [ - 'alert', - 'dialog', - ], - }], - 'jsx-a11y/heading-has-content': 'error', - 'jsx-a11y/html-has-lang': 'error', - 'jsx-a11y/iframe-has-title': 'error', - 'jsx-a11y/img-redundant-alt': 'error', - 'jsx-a11y/interactive-supports-focus': [ - 'error', - { - tabbable: [ - 'button', - 'checkbox', - 'link', - 'searchbox', - 'spinbutton', - 'switch', - 'textbox', - ], - }, - ], - 'jsx-a11y/label-has-associated-control': 'error', - 'jsx-a11y/label-has-for': 'off', - 'jsx-a11y/media-has-caption': 'error', - 'jsx-a11y/mouse-events-have-key-events': 'error', - 'jsx-a11y/no-access-key': 'error', - 'jsx-a11y/no-autofocus': 'error', - 'jsx-a11y/no-distracting-elements': 'error', - 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ - 'error', - { - tr: ['none', 'presentation'], - canvas: ['img'], - }, - ], - 'jsx-a11y/no-noninteractive-element-interactions': [ - 'error', - { - handlers: [ - 'onClick', - 'onError', - 'onLoad', - 'onMouseDown', - 'onMouseUp', - 'onKeyPress', - 'onKeyDown', - 'onKeyUp', - ], - alert: ['onKeyUp', 'onKeyDown', 'onKeyPress'], - body: ['onError', 'onLoad'], - dialog: ['onKeyUp', 'onKeyDown', 'onKeyPress'], - iframe: ['onError', 'onLoad'], - img: ['onError', 'onLoad'], - }, - ], - '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', 'menuitemradio', 'menuitemcheckbox', 'option', 'row', 'tab', 'treeitem'], - table: ['grid'], - td: ['gridcell'], - fieldset: ['radiogroup', 'presentation'], - }, - ], - 'jsx-a11y/no-noninteractive-tabindex': [ - 'error', - { - tags: [], - roles: ['tabpanel'], - allowExpressionValues: true, - }, - ], - 'jsx-a11y/no-redundant-roles': 'error', - 'jsx-a11y/no-static-element-interactions': [ - 'error', - { - allowExpressionValues: true, - handlers: [ - 'onClick', - 'onMouseDown', - 'onMouseUp', - 'onKeyPress', - 'onKeyDown', - 'onKeyUp', - ], - }, - ], - 'jsx-a11y/role-has-required-aria-props': 'error', - 'jsx-a11y/role-supports-aria-props': 'error', - 'jsx-a11y/scope': 'error', - 'jsx-a11y/tabindex-no-positive': 'error', - }, }, - strict: { - plugins: [ - 'jsx-a11y', + ], + 'jsx-a11y/role-has-required-aria-props': 'error', + 'jsx-a11y/role-supports-aria-props': 'error', + 'jsx-a11y/scope': 'error', + 'jsx-a11y/tabindex-no-positive': 'error', +}; + +const strictRules = { + 'jsx-a11y/alt-text': 'error', + 'jsx-a11y/anchor-has-content': 'error', + 'jsx-a11y/anchor-is-valid': 'error', + 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', + 'jsx-a11y/aria-props': 'error', + 'jsx-a11y/aria-proptypes': 'error', + 'jsx-a11y/aria-role': 'error', + 'jsx-a11y/aria-unsupported-elements': 'error', + 'jsx-a11y/autocomplete-valid': 'error', + 'jsx-a11y/click-events-have-key-events': 'error', + 'jsx-a11y/control-has-associated-label': [ + 'off', + { + ignoreElements: [ + 'audio', + 'canvas', + 'embed', + 'input', + 'textarea', + 'tr', + 'video', + ], + ignoreRoles: [ + 'grid', + 'listbox', + 'menu', + 'menubar', + 'radiogroup', + 'row', + 'tablist', + 'toolbar', + 'tree', + 'treegrid', ], - parserOptions: { - ecmaFeatures: { - jsx: true, - }, - }, - rules: { - 'jsx-a11y/alt-text': 'error', - 'jsx-a11y/anchor-has-content': 'error', - 'jsx-a11y/anchor-is-valid': 'error', - 'jsx-a11y/aria-activedescendant-has-tabindex': 'error', - 'jsx-a11y/aria-props': 'error', - 'jsx-a11y/aria-proptypes': 'error', - 'jsx-a11y/aria-role': 'error', - 'jsx-a11y/aria-unsupported-elements': 'error', - 'jsx-a11y/autocomplete-valid': 'error', - 'jsx-a11y/click-events-have-key-events': 'error', - 'jsx-a11y/control-has-associated-label': ['off', { - ignoreElements: [ - 'audio', - 'canvas', - 'embed', - 'input', - 'textarea', - 'tr', - 'video', - ], - ignoreRoles: [ - 'grid', - 'listbox', - 'menu', - 'menubar', - 'radiogroup', - 'row', - 'tablist', - 'toolbar', - 'tree', - 'treegrid', - ], - includeRoles: [ - 'alert', - 'dialog', - ], - }], - 'jsx-a11y/heading-has-content': 'error', - 'jsx-a11y/html-has-lang': 'error', - 'jsx-a11y/iframe-has-title': 'error', - 'jsx-a11y/img-redundant-alt': 'error', - 'jsx-a11y/interactive-supports-focus': [ - 'error', - { - tabbable: [ - 'button', - 'checkbox', - 'link', - 'progressbar', - 'searchbox', - 'slider', - 'spinbutton', - 'switch', - 'textbox', - ], - }, - ], - 'jsx-a11y/label-has-for': 'off', - 'jsx-a11y/label-has-associated-control': 'error', - 'jsx-a11y/media-has-caption': 'error', - 'jsx-a11y/mouse-events-have-key-events': 'error', - 'jsx-a11y/no-access-key': 'error', - 'jsx-a11y/no-autofocus': 'error', - 'jsx-a11y/no-distracting-elements': 'error', - 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error', - 'jsx-a11y/no-noninteractive-element-interactions': [ - 'error', - { - body: ['onError', 'onLoad'], - iframe: ['onError', 'onLoad'], - img: ['onError', 'onLoad'], - }, - ], - 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error', - 'jsx-a11y/no-noninteractive-tabindex': 'error', - 'jsx-a11y/no-redundant-roles': 'error', - 'jsx-a11y/no-static-element-interactions': 'error', - 'jsx-a11y/role-has-required-aria-props': 'error', - 'jsx-a11y/role-supports-aria-props': 'error', - 'jsx-a11y/scope': 'error', - 'jsx-a11y/tabindex-no-positive': 'error', - }, + includeRoles: ['alert', 'dialog'], + }, + ], + 'jsx-a11y/heading-has-content': 'error', + 'jsx-a11y/html-has-lang': 'error', + 'jsx-a11y/iframe-has-title': 'error', + 'jsx-a11y/img-redundant-alt': 'error', + 'jsx-a11y/interactive-supports-focus': [ + 'error', + { + tabbable: [ + 'button', + 'checkbox', + 'link', + 'progressbar', + 'searchbox', + 'slider', + 'spinbutton', + 'switch', + 'textbox', + ], + }, + ], + 'jsx-a11y/label-has-for': 'off', + 'jsx-a11y/label-has-associated-control': 'error', + 'jsx-a11y/media-has-caption': 'error', + 'jsx-a11y/mouse-events-have-key-events': 'error', + 'jsx-a11y/no-access-key': 'error', + 'jsx-a11y/no-autofocus': 'error', + 'jsx-a11y/no-distracting-elements': 'error', + 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error', + 'jsx-a11y/no-noninteractive-element-interactions': [ + 'error', + { + body: ['onError', 'onLoad'], + iframe: ['onError', 'onLoad'], + img: ['onError', 'onLoad'], }, - }, + ], + 'jsx-a11y/no-noninteractive-element-to-interactive-role': 'error', + 'jsx-a11y/no-noninteractive-tabindex': 'error', + 'jsx-a11y/no-redundant-roles': 'error', + 'jsx-a11y/no-static-element-interactions': 'error', + 'jsx-a11y/role-has-required-aria-props': 'error', + 'jsx-a11y/role-supports-aria-props': 'error', + 'jsx-a11y/scope': 'error', + 'jsx-a11y/tabindex-no-positive': 'error', }; + +/** Base plugin object */ +const jsxA11y = { + meta: { name, version }, + rules: { ...allRules }, +}; + +/** + * Given a ruleset and optionally a flat config name, generate a config. + * @param {object} rules - ruleset for this config + * @param {string} flatConfigName - name for the config if flat + * @returns Config for this set of rules. + */ +const createConfig = (rules, flatConfigName) => ({ + ...(flatConfigName + ? { + ...flatConfigBase, + name: `jsx-a11y/${flatConfigName}`, + plugins: { 'jsx-a11y': jsxA11y }, + } + : { ...legacyConfigBase, plugins: ['jsx-a11y'] }), + rules: { ...rules }, +}); + +// Create configs for the plugin object +const configs = { + recommended: createConfig(recommendedRules), + strict: createConfig(strictRules), +}; +const flatConfigs = { + recommended: createConfig(recommendedRules, 'recommended'), + strict: createConfig(strictRules, 'strict'), +}; + +module.exports = { ...jsxA11y, configs, flatConfigs }; From 0a98ad83ffa7f4b66458cc1c39db2ef32bb2c480 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 19 Jun 2024 14:37:16 -0700 Subject: [PATCH 06/11] [Deps] remove `@babel/runtime` the only use of it was for the interopRequireDefault helper, and its engines.node is incompatible with ours --- .babelrc | 3 ++- package.json | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.babelrc b/.babelrc index 42e95b05..058a9a5a 100644 --- a/.babelrc +++ b/.babelrc @@ -5,7 +5,8 @@ { "targets": { "node": 4 - } + }, + "transformRuntime": false } ] ], diff --git a/package.json b/package.json index f14e0b68..6e762ba7 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ }, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.24.7", "aria-query": "^5.3.0", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", From 32fd82c628d7f3e4ec8c06a1994f4eca1be2be4f Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 19 Jun 2024 14:41:43 -0700 Subject: [PATCH 07/11] [Deps] pin `aria-query` and `axobject-query`, add `ls-engines` test to CI Specifically, v5.2 of aria-query and v3.2 of axobject-query require node 6 via the dequal dependency, and neither have an engines declaration. This is a breaking change See https://github.com/A11yance/aria-query/pull/497#issuecomment-2179489722 --- .github/workflows/node-pretest.yml | 7 ++++++- package.json | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node-pretest.yml b/.github/workflows/node-pretest.yml index 3a10687f..6648b0e0 100644 --- a/.github/workflows/node-pretest.yml +++ b/.github/workflows/node-pretest.yml @@ -49,4 +49,9 @@ jobs: with: node-version: 'lts/*' skip-ls-check: true - - run: npm run test:examples \ No newline at end of file + - run: npm run test:examples + + engines: + runs-on: ubuntu-latest + steps: + - uses: ljharb/actions/node/engines@main diff --git a/package.json b/package.json index 6e762ba7..43e2148c 100644 --- a/package.json +++ b/package.json @@ -79,12 +79,12 @@ }, "license": "MIT", "dependencies": { - "aria-query": "^5.3.0", + "aria-query": "~5.1.3", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.9.1", - "axobject-query": "^3.2.1", + "axobject-query": "~3.1.1", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "es-iterator-helpers": "^1.0.19", From 75d5dd722bd67186d97afa7b151fd6fee5885c70 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 19 Jun 2024 14:47:00 -0700 Subject: [PATCH 08/11] Revert "[Fix] `isNonInteractiveElement`: Upgrade aria-query to 5.3.0 and axobject-query to 3.2.1" This reverts commit 64bfea6352a704470a760fa6ea25cfc5a50414db / #937 --- __mocks__/genInteractives.js | 15 ++++------- .../control-has-associated-label-test.js | 2 +- ...ive-element-to-noninteractive-role-test.js | 1 - ...oninteractive-element-interactions-test.js | 24 ++++++++--------- ...active-element-to-interactive-role-test.js | 26 ++++++++++--------- .../no-static-element-interactions-test.js | 24 ++++++++--------- .../src/rules/prefer-tag-over-role-test.js | 2 +- src/util/isNonInteractiveElement.js | 4 --- 8 files changed, 45 insertions(+), 53 deletions(-) diff --git a/__mocks__/genInteractives.js b/__mocks__/genInteractives.js index f12ebcb0..68b23926 100644 --- a/__mocks__/genInteractives.js +++ b/__mocks__/genInteractives.js @@ -45,13 +45,14 @@ const interactiveElementsMap = { '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: [], + // td: [], th: [], tr: [], textarea: [], @@ -60,27 +61,25 @@ const interactiveElementsMap = { const nonInteractiveElementsMap: {[string]: Array<{[string]: string}>} = { abbr: [], - address: [], aside: [], article: [], blockquote: [], + body: [], br: [], caption: [], - code: [], dd: [], - del: [], details: [], dfn: [], dialog: [], dir: [], dl: [], dt: [], - em: [], fieldset: [], figcaption: [], figure: [], footer: [], form: [], + frame: [], h1: [], h2: [], h3: [], @@ -88,10 +87,8 @@ const nonInteractiveElementsMap: {[string]: Array<{[string]: string}>} = { h5: [], h6: [], hr: [], - html: [], iframe: [], img: [], - ins: [], label: [], legend: [], li: [], @@ -110,11 +107,9 @@ const nonInteractiveElementsMap: {[string]: Array<{[string]: string}>} = { ruby: [], 'section[aria-label]': [{ prop: 'aria-label' }], 'section[aria-labelledby]': [{ prop: 'aria-labelledby' }], - strong: [], - sub: [], - sup: [], table: [], tbody: [], + td: [], tfoot: [], thead: [], time: [], diff --git a/__tests__/src/rules/control-has-associated-label-test.js b/__tests__/src/rules/control-has-associated-label-test.js index 73bf2be6..e175f316 100644 --- a/__tests__/src/rules/control-has-associated-label-test.js +++ b/__tests__/src/rules/control-has-associated-label-test.js @@ -157,6 +157,7 @@ const alwaysValid = [ { code: '
' }, { code: '' }, { code: '' }, + { code: '' }, { code: '' }, { code: '
' }, { code: '
', errors: [expectedError] }, { code: '', errors: [expectedError] }, // Interactive Roles { code: '
', errors: [expectedError] }, diff --git a/__tests__/src/rules/no-interactive-element-to-noninteractive-role-test.js b/__tests__/src/rules/no-interactive-element-to-noninteractive-role-test.js index 9edb85e5..d8b55159 100644 --- a/__tests__/src/rules/no-interactive-element-to-noninteractive-role-test.js +++ b/__tests__/src/rules/no-interactive-element-to-noninteractive-role-test.js @@ -170,7 +170,6 @@ const alwaysValid = [ { code: '