diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 663eff3a5..4d46985c6 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -138,5 +138,19 @@ module.exports = { ], }, }, + // Accessibility rules on /site + { + files: ['site/**/*.{jsx,js,tsx,ts}'], + plugins: ['jsx-a11y'], + extends: ['plugin:jsx-a11y/strict'], + rules: { + 'jsx-a11y/no-autofocus': 'warn', + 'jsx-a11y/alt-text': 'warn', + 'jsx-a11y/no-noninteractive-tabindex': 'warn', + 'jsx-a11y/iframe-has-title': 'warn', + 'jsx-a11y/click-events-have-key-events': 'warn', + 'jsx-a11y/no-static-element-interactions': 'warn', + }, + }, ], } diff --git a/package.json b/package.json index 57ff99779..539273349 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "lint:prettier": "yarn run prettier --check \"**/*.{js,jsx,ts,tsx,yaml,yml}\"", "lint:prettier:fix": "yarn lint:prettier --write", "lint:fix": "yarn lint:eslint:fix ; yarn lint:prettier:fix", + "lint:quiet": "yarn lint:eslintrc && yarn lint:eslint --quiet && yarn lint:prettier", "lint": "yarn lint:eslintrc && yarn lint:eslint && yarn lint:prettier", "postinstall": "yarn workspaces foreach -piv --exclude site run prepack", "test": "CI=true yarn workspaces foreach run test", @@ -46,6 +47,7 @@ "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-import": "^2.26.0", "eslint-plugin-jest": "^26.5.3", + "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-n": "^15.2.0", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.30.0", diff --git a/site/package.json b/site/package.json index 8d374c742..93cfdcca0 100644 --- a/site/package.json +++ b/site/package.json @@ -21,6 +21,7 @@ "build:yaml-to-dts": "ts-node-esm scripts/build-yaml-to-dts.ts", "postinstall": "node scripts/prepare.js", "start": "vite dev", + "start:axe-debugging": "VITE_AXE_CORE_ENABLED=true vite dev", "start:netlify": "sed 's|:SITE_EN|/infrance|g' netlify.base.toml | sed 's|:SITE_FR|/mon-entreprise|g' | sed 's|:API_URL|http://localhost:3004|g' | sed 's|\\[\\[redirects\\]\\]|\\[\\[redirects\\]\\]\\n force = true|g' > netlify.toml && HMR_CLIENT_PORT=8888 netlify dev", "build": "NODE_OPTIONS='--max-old-space-size=6144'; yarn build:sitemap && vite build && yarn build:iframe-script", "build:ssr": "NODE_OPTIONS='--max-old-space-size=4096'; vite build --ssr ./source/entry-server.tsx --outDir ./dist/server --emptyOutDir && echo '{\"module\": \"commonjs\"}' > dist/package.json", @@ -47,6 +48,7 @@ "build:storybook": "build-storybook" }, "dependencies": { + "@axe-core/react": "^4.4.4", "@internationalized/number": "^3.0.3", "@react-aria/accordion": "^3.0.0-alpha.5", "@react-aria/button": "^3.4.1", diff --git a/site/source/App.tsx b/site/source/App.tsx index 589300b1c..47d73d0b0 100644 --- a/site/source/App.tsx +++ b/site/source/App.tsx @@ -24,6 +24,7 @@ import { useSelector } from 'react-redux' import { Route, Routes } from 'react-router-dom' import styled, { css } from 'styled-components' import { useSaveAndRestoreScrollPosition } from './hooks/useSaveAndRestoreScrollPosition' +import { useAxeCoreAnalysis } from './hooks/useAxeCoreAnalysis' import Accessibilité from './pages/Accessibilité' import Budget from './pages/Budget/Budget' import Créer from './pages/Creer' @@ -112,6 +113,11 @@ const App = () => { useSaveAndRestoreScrollPosition() + if (!import.meta.env.PROD && import.meta.env.VITE_AXE_CORE_ENABLED) { + // eslint-disable-next-line react-hooks/rules-of-hooks + useAxeCoreAnalysis() + } + return ( {!isEmbedded &&
} diff --git a/site/source/hooks/useAxeCoreAnalysis.ts b/site/source/hooks/useAxeCoreAnalysis.ts new file mode 100644 index 000000000..67da5289c --- /dev/null +++ b/site/source/hooks/useAxeCoreAnalysis.ts @@ -0,0 +1,40 @@ +import React, { useEffect, useRef } from 'react' +import ReactDOM from 'react-dom' + +export const useAxeCoreAnalysis = () => { + const axeRef = useRef< + | { + default: ( + _React: any, + _ReactDOM: any, + _timeout: number + ) => Promise + } + | undefined + >() + + const triggerAxeCoreAnalysis = async () => { + await axeRef.current?.default(React, ReactDOM, 1000) + } + + // On importe axe-core/react uniquement si ce n'est pas déjà fait + if (!axeRef.current) { + import('@axe-core/react') + .then(async (axe) => { + axeRef.current = axe + await triggerAxeCoreAnalysis() + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error(err) + }) + } + + // useEffect déclenché à chaque re-render du composant ou des enfants du composant + // où il est placé + useEffect(() => { + if (!axeRef.current) return + + void triggerAxeCoreAnalysis() + }) +} diff --git a/yarn.lock b/yarn.lock index ccafae8d7..b92393d8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -287,6 +287,16 @@ __metadata: languageName: node linkType: hard +"@axe-core/react@npm:^4.4.4": + version: 4.4.4 + resolution: "@axe-core/react@npm:4.4.4" + dependencies: + axe-core: ^4.4.1 + requestidlecallback: ^0.3.0 + checksum: 1328066d39ddf6960c8affd43266e5c068c05a23b64dde8c5d3bad7c8742e93a86f355ffd16f38989bbfcb87ba719d4a26eff267410f3d9f2a2b9a8ffea93041 + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.5.5, @babel/code-frame@npm:^7.8.3": version: 7.16.7 resolution: "@babel/code-frame@npm:7.16.7" @@ -2880,6 +2890,16 @@ __metadata: languageName: node linkType: hard +"@babel/runtime-corejs3@npm:^7.10.2": + version: 7.19.0 + resolution: "@babel/runtime-corejs3@npm:7.19.0" + dependencies: + core-js-pure: ^3.20.2 + regenerator-runtime: ^0.13.4 + checksum: 810c983462430b948af83e1e6bcc06ca14dad59a257d7dc453779cdc1fbc7d9a6d75d608591a1eeb90c71c7fce1c2279a87c5bc848c5ac58eef831ecad596a9f + languageName: node + linkType: hard + "@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.16.4, @babel/runtime@npm:^7.17.0, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": version: 7.17.8 resolution: "@babel/runtime@npm:7.17.8" @@ -2889,6 +2909,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.18.9": + version: 7.19.0 + resolution: "@babel/runtime@npm:7.19.0" + dependencies: + regenerator-runtime: ^0.13.4 + checksum: fa69c351bb05e1db3ceb9a02fdcf620c234180af68cdda02152d3561015f6d55277265d3109815992f96d910f3db709458cae4f8df1c3def66f32e0867d82294 + languageName: node + linkType: hard + "@babel/runtime@npm:^7.17.8": version: 7.18.6 resolution: "@babel/runtime@npm:7.18.6" @@ -10117,6 +10146,16 @@ __metadata: languageName: node linkType: hard +"aria-query@npm:^4.2.2": + version: 4.2.2 + resolution: "aria-query@npm:4.2.2" + dependencies: + "@babel/runtime": ^7.10.2 + "@babel/runtime-corejs3": ^7.10.2 + checksum: 38401a9a400f26f3dcc24b84997461a16b32869a9893d323602bed8da40a8bcc0243b8d2880e942249a1496cea7a7de769e93d21c0baa439f01e1ee936fed665 + languageName: node + linkType: hard + "aria-query@npm:^5.0.0": version: 5.0.0 resolution: "aria-query@npm:5.0.0" @@ -10355,6 +10394,13 @@ __metadata: languageName: node linkType: hard +"ast-types-flow@npm:^0.0.7": + version: 0.0.7 + resolution: "ast-types-flow@npm:0.0.7" + checksum: a26dcc2182ffee111cad7c471759b0bda22d3b7ebacf27c348b22c55f16896b18ab0a4d03b85b4020dce7f3e634b8f00b593888f622915096ea1927fa51866c4 + languageName: node + linkType: hard + "ast-types@npm:^0.14.2": version: 0.14.2 resolution: "ast-types@npm:0.14.2" @@ -10467,6 +10513,13 @@ __metadata: languageName: node linkType: hard +"axe-core@npm:^4.4.1, axe-core@npm:^4.4.3": + version: 4.4.3 + resolution: "axe-core@npm:4.4.3" + checksum: c3ea000d9ace3ba0bc747c8feafc24b0de62a0f7d93021d0f77b19c73fca15341843510f6170da563d51535d6cfb7a46c5fc0ea36170549dbb44b170208450a2 + languageName: node + linkType: hard + "axios@npm:^0.25.0": version: 0.25.0 resolution: "axios@npm:0.25.0" @@ -10476,6 +10529,13 @@ __metadata: languageName: node linkType: hard +"axobject-query@npm:^2.2.0": + version: 2.2.0 + resolution: "axobject-query@npm:2.2.0" + checksum: 96b8c7d807ca525f41ad9b286186e2089b561ba63a6d36c3e7d73dc08150714660995c7ad19cda05784458446a0793b45246db45894631e13853f48c1aa3117f + languageName: node + linkType: hard + "babel-loader@npm:^8.0.0": version: 8.2.5 resolution: "babel-loader@npm:8.2.5" @@ -12548,6 +12608,13 @@ __metadata: languageName: node linkType: hard +"core-js-pure@npm:^3.20.2": + version: 3.25.0 + resolution: "core-js-pure@npm:3.25.0" + checksum: 041cef3c4fa03b30eea6aa8539db00a02ea264e8542b9b787428f43e727e67050c742f46dbd75bc9ab544524a54e1ee55d8b23602dc8a2da485a3741a5f95df7 + languageName: node + linkType: hard + "core-js-pure@npm:^3.8.1": version: 3.23.3 resolution: "core-js-pure@npm:3.23.3" @@ -13069,6 +13136,13 @@ __metadata: languageName: node linkType: hard +"damerau-levenshtein@npm:^1.0.8": + version: 1.0.8 + resolution: "damerau-levenshtein@npm:1.0.8" + checksum: d240b7757544460ae0586a341a53110ab0a61126570ef2d8c731e3eab3f0cb6e488e2609e6a69b46727635de49be20b071688698744417ff1b6c1d7ccd03e0de + languageName: node + linkType: hard + "dashdash@npm:^1.12.0": version: 1.14.1 resolution: "dashdash@npm:1.14.1" @@ -14878,6 +14952,29 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jsx-a11y@npm:^6.6.1": + version: 6.6.1 + resolution: "eslint-plugin-jsx-a11y@npm:6.6.1" + dependencies: + "@babel/runtime": ^7.18.9 + aria-query: ^4.2.2 + array-includes: ^3.1.5 + ast-types-flow: ^0.0.7 + axe-core: ^4.4.3 + axobject-query: ^2.2.0 + damerau-levenshtein: ^1.0.8 + emoji-regex: ^9.2.2 + has: ^1.0.3 + jsx-ast-utils: ^3.3.2 + language-tags: ^1.0.5 + minimatch: ^3.1.2 + semver: ^6.3.0 + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + checksum: baae7377f0e25a0cc9b34dc333a3dc6ead9ee8365e445451eff554c3ca267a0a6cb88127fe90395c578ab1b92cfed246aef7dc8d2b48b603389e10181799e144 + languageName: node + linkType: hard + "eslint-plugin-n@npm:^15.2.0": version: 15.2.4 resolution: "eslint-plugin-n@npm:15.2.4" @@ -19270,6 +19367,16 @@ __metadata: languageName: node linkType: hard +"jsx-ast-utils@npm:^3.3.2": + version: 3.3.3 + resolution: "jsx-ast-utils@npm:3.3.3" + dependencies: + array-includes: ^3.1.5 + object.assign: ^4.1.3 + checksum: a2ed78cac49a0f0c4be8b1eafe3c5257a1411341d8e7f1ac740debae003de04e5f6372bfcfbd9d082e954ffd99aac85bcda85b7c6bc11609992483f4cdc0f745 + languageName: node + linkType: hard + "junk@npm:^3.1.0": version: 3.1.0 resolution: "junk@npm:3.1.0" @@ -19504,6 +19611,22 @@ __metadata: languageName: node linkType: hard +"language-subtag-registry@npm:~0.3.2": + version: 0.3.22 + resolution: "language-subtag-registry@npm:0.3.22" + checksum: 8ab70a7e0e055fe977ac16ea4c261faec7205ac43db5e806f72e5b59606939a3b972c4bd1e10e323b35d6ffa97c3e1c4c99f6553069dad2dfdd22020fa3eb56a + languageName: node + linkType: hard + +"language-tags@npm:^1.0.5": + version: 1.0.5 + resolution: "language-tags@npm:1.0.5" + dependencies: + language-subtag-registry: ~0.3.2 + checksum: c81b5d8b9f5f9cfd06ee71ada6ddfe1cf83044dd5eeefcd1e420ad491944da8957688db4a0a9bc562df4afdc2783425cbbdfd152c01d93179cf86888903123cf + languageName: node + linkType: hard + "latest-version@npm:^5.1.0": version: 5.1.0 resolution: "latest-version@npm:5.1.0" @@ -21676,6 +21799,18 @@ __metadata: languageName: node linkType: hard +"object.assign@npm:^4.1.3": + version: 4.1.4 + resolution: "object.assign@npm:4.1.4" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.4 + has-symbols: ^1.0.3 + object-keys: ^1.1.1 + checksum: 76cab513a5999acbfe0ff355f15a6a125e71805fcf53de4e9d4e082e1989bdb81d1e329291e1e4e0ae7719f0e4ef80e88fb2d367ae60500d79d25a6224ac8864 + languageName: node + linkType: hard + "object.entries@npm:^1.1.0, object.entries@npm:^1.1.5": version: 1.1.5 resolution: "object.entries@npm:1.1.5" @@ -24372,6 +24507,13 @@ __metadata: languageName: node linkType: hard +"requestidlecallback@npm:^0.3.0": + version: 0.3.0 + resolution: "requestidlecallback@npm:0.3.0" + checksum: 2405aef711b516e326ff18849b24ad2c0e623d2b60397bdc7919fa40d8575fce0a16a563a53f94ec89d255325a99e5deee952e6024584a5179cbbabb4469f0e8 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -24724,6 +24866,7 @@ __metadata: eslint-plugin-cypress: ^2.12.1 eslint-plugin-import: ^2.26.0 eslint-plugin-jest: ^26.5.3 + eslint-plugin-jsx-a11y: ^6.6.1 eslint-plugin-n: ^15.2.0 eslint-plugin-promise: ^6.0.0 eslint-plugin-react: ^7.30.0 @@ -25208,6 +25351,7 @@ __metadata: version: 0.0.0-use.local resolution: "site@workspace:site" dependencies: + "@axe-core/react": ^4.4.4 "@internationalized/number": ^3.0.3 "@react-aria/accordion": ^3.0.0-alpha.5 "@react-aria/button": ^3.4.1