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 logo + + + React logo + + click here +
+

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 logo + + + React logo + + click here +
+

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 logo + + + React logo + + click here +
+

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 };