diff --git a/docs/development.md b/docs/development.md index 1e7cad0..eae857e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -107,3 +107,20 @@ Add primitives with `pnpm dlx shadcn@latest add `. `components.json` selects Base UI, Nova, and Lucide. Theme defaults live in `src/styles/globals.css`; components own their Tailwind classes. See [architecture](architecture.md) for module ownership and the current layout. + +## Accessibility + +Oxlint enables `jsx-a11y` correctness rules. The semantic-tag preference is disabled +because SVG/canvas images and live regions legitimately use ARIA. Documentation +table regions retain keyboard focus for scrolling. Shared links and labels forward +content and associations explicitly so lint can inspect them. + +The browser suite runs axe on Home, About, API, settings tabs, and editor/export +dialogs. Run just these checks with `pnpm run test:browser --grep 'accessibility:'` +after building. Tests check the rendered DOM, including generated ARIA references +that static lint cannot validate. Base UI owns tab-panel IDs; use `data-panel` for +stable test selectors rather than overriding those IDs. + +For release review, also run Lighthouse on the production build and manually check +keyboard navigation, focus restoration, and screen-reader labels. A clean automated +audit does not establish complete accessibility. diff --git a/oxlint.config.ts b/oxlint.config.ts index bc87051..ca4d28a 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "oxlint"; export default defineConfig({ - plugins: ["eslint", "unicorn", "oxc", "vitest", "typescript"], + plugins: ["eslint", "unicorn", "oxc", "vitest", "typescript", "jsx-a11y"], categories: { correctness: "error" }, env: { node: true }, ignorePatterns: [ @@ -16,6 +16,8 @@ export default defineConfig({ "test-results/**", ], rules: { + // SVG/canvas images and live status regions intentionally use ARIA roles. + "jsx-a11y/prefer-tag-over-role": "off", "typescript/consistent-type-definitions": ["error", "type"], "no-undef": "error", "no-var": "error", @@ -23,6 +25,11 @@ export default defineConfig({ "one-var": ["error", "never"], }, overrides: [ + { + files: ["src/components/docs-page.tsx"], + // Keyboard users must be able to scroll wide documentation tables. + rules: { "jsx-a11y/no-noninteractive-tabindex": "off" }, + }, { files: ["src/**/*.js", "src/**/*.mjs", "src/**/*.ts", "src/**/*.tsx"], env: { node: false, browser: true }, diff --git a/package.json b/package.json index 2253fc2..946e88d 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "tw-animate-css": "^1.4.0" }, "devDependencies": { + "@axe-core/playwright": "^4.13.0", "@playwright/test": "^1.63.0", "@tailwindcss/vite": "^4.3.3", "@types/d3-color": "^3.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5e5413..02d7535 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: specifier: ^1.4.0 version: 1.4.0 devDependencies: + '@axe-core/playwright': + specifier: ^4.13.0 + version: 4.13.0(playwright-core@1.63.0) '@playwright/test': specifier: ^1.63.0 version: 1.63.0 @@ -228,6 +231,11 @@ importers: packages: + '@axe-core/playwright@4.13.0': + resolution: {integrity: sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1565,6 +1573,10 @@ packages: resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} engines: {node: '>=10.12.0'} + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} @@ -3598,6 +3610,11 @@ packages: snapshots: + '@axe-core/playwright@4.13.0(playwright-core@1.63.0)': + dependencies: + axe-core: 4.13.0 + playwright-core: 1.63.0 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -4785,6 +4802,8 @@ snapshots: atomically@1.7.0: {} + axe-core@4.13.0: {} + babel-dead-code-elimination@1.0.12: dependencies: '@babel/core': 7.29.7 diff --git a/src/components/dialogs/algorithm-dialog.tsx b/src/components/dialogs/algorithm-dialog.tsx index f78d439..5613741 100644 --- a/src/components/dialogs/algorithm-dialog.tsx +++ b/src/components/dialogs/algorithm-dialog.tsx @@ -119,7 +119,12 @@ export function AlgorithmDialog({ )} {!adding && ( - + {( @@ -150,7 +155,7 @@ export function AlgorithmDialog({ )}
diff --git a/src/components/docs-page.tsx b/src/components/docs-page.tsx index 8bfe484..5dd0f79 100644 --- a/src/components/docs-page.tsx +++ b/src/components/docs-page.tsx @@ -20,7 +20,7 @@ export function DocsPage({ components?: Components; }) { return ( -
@@ -51,6 +51,6 @@ export function DocsPage({ > {content} -
+ ); } diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index 9db0985..c57d6d5 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -39,7 +39,7 @@ export function Footer() { skratchdot{" "} skratchdot diff --git a/src/components/playground/settings-controls.tsx b/src/components/playground/settings-controls.tsx index 88862a0..7fa5d3d 100644 --- a/src/components/playground/settings-controls.tsx +++ b/src/components/playground/settings-controls.tsx @@ -46,7 +46,7 @@ export function Settings({ runtime }: Props) { ))}
- + {( [ ["volume", "Volume", selected.volume.toFixed(2)], @@ -102,10 +102,10 @@ export function Settings({ runtime }: Props) { {selected.audioType} settings - + - + update({ key: "scale", value })} /> - + ) { +export function TextLink({ className, children, ...props }: ComponentProps<"a">) { return ( ) { className, )} {...props} - /> + > + {children} + ); } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index 8eda3f6..55c921a 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -3,10 +3,11 @@ import * as React from "react"; import { cn } from "cn"; -function Label({ className, ...props }: React.ComponentProps<"label">) { +function Label({ className, htmlFor, ...props }: React.ComponentProps<"label">) { return (