From 0c16dd88955bae3439dfdb1a5bbca4212262ee8b Mon Sep 17 00:00:00 2001 From: Anneke Sinnema Date: Tue, 18 Aug 2026 09:43:47 +0200 Subject: [PATCH 1/8] feat: add documentation pages in Storybook --- packages/angular/.storybook/main.ts | 1 + .../src/foundations/accessibility.stories.ts | 173 ++++++++++++++++ packages/react/.storybook/main.ts | 1 + .../src/foundations/accessibility.stories.tsx | 178 +++++++++++++++++ .../storybook-config/src/accessibility.ts | 188 ++++++++++++++++++ packages/storybook-config/src/index.ts | 14 ++ .../static/accessibility.skill.md | 140 +++++++++++++ 7 files changed, 695 insertions(+) create mode 100644 packages/angular/src/foundations/accessibility.stories.ts create mode 100644 packages/react/src/foundations/accessibility.stories.tsx create mode 100644 packages/storybook-config/src/accessibility.ts create mode 100644 packages/storybook-config/static/accessibility.skill.md diff --git a/packages/angular/.storybook/main.ts b/packages/angular/.storybook/main.ts index 8720f18d..520ca1d4 100644 --- a/packages/angular/.storybook/main.ts +++ b/packages/angular/.storybook/main.ts @@ -3,6 +3,7 @@ import type { StorybookConfig } from '@storybook/angular'; const config: StorybookConfig = { stories: ['../src/**/*.stories.@(ts|mdx)'], addons: ['@storybook/addon-a11y', '@storybook/addon-docs'], + staticDirs: [{ from: '../../storybook-config/static', to: 'downloads' }], framework: '@storybook/angular', }; diff --git a/packages/angular/src/foundations/accessibility.stories.ts b/packages/angular/src/foundations/accessibility.stories.ts new file mode 100644 index 00000000..91c9d646 --- /dev/null +++ b/packages/angular/src/foundations/accessibility.stories.ts @@ -0,0 +1,173 @@ +import { Component } from '@angular/core'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { phosphorDownloadSimple } from '@ng-icons/phosphor-icons/regular'; +import { type Meta, moduleMetadata, type StoryObj } from '@storybook/angular'; +import { + ACCESSIBILITY_LAYERS, + ACCESSIBILITY_PAGE_INTRO, + ACCESSIBILITY_SECTIONS, + ACCESSIBILITY_SKILL_FILE, + ACCESSIBILITY_SKILL_INSTALL, + ACCESSIBILITY_SKILL_SUMMARY, + type AccessibilityDocSection, + type AccessibilityInstallTarget, + type AccessibilityLayer, +} from '@surfnet/curve-storybook-config'; + +import { HlmButton } from '../lib/ui/button/src'; +import { HlmCardImports } from '../lib/ui/card/src'; + +@Component({ + selector: 'surf-accessibility-guide', + standalone: true, + imports: [NgIcon, HlmButton, ...HlmCardImports], + providers: [provideIcons({ phosphorDownloadSimple })], + template: ` +
+

Accessibility

+

{{ intro }}

+ + +
+

Agent skill for coding assistants

+

+ A markdown skill you can drop into Cursor, Claude Code, or another coding agent. It + teaches the agent to apply semantic HTML, accessible names, heading structure, keyboard + access, skip links, and focus styles whenever it writes or reviews UI. +

+
+
+ @for (paragraph of skillSummary; track paragraph) { +

{{ paragraph }}

+ } + + + + + + + + + + @for (row of skillInstall; track row.tool) { + + + + + } + +
+ Where to install the downloaded skill file +
ToolSave as
{{ row.tool }}{{ row.path }}
+

+ + + Download the accessibility skill + +

+
+
+ +
+

A layered process

+

+ No single tool covers accessibility. Stack a cheap check at write-time, a linter on save, + axe in Storybook and CI, and a short keyboard pass before you ship. +

+ + + + + + + + + + + @for (row of layers; track row.layer) { + + + + + + } + +
+ Accessibility checks by layer of the developer process +
LayerWhenWhat it catches
{{ row.layer }}{{ row.when }}{{ row.catches }}
+
+ + @for (section of sections; track section.id) { +
+

{{ section.title }}

+ @for (paragraph of section.paragraphs; track paragraph) { +

{{ paragraph }}

+ } + @if (section.bullets) { +
    + @for (item of section.bullets; track item) { +
  • {{ item }}
  • + } +
+ } + @if (section.links) { +
    + @for (link of section.links; track link.href) { +
  • + {{ link.label }} +
  • + } +
+ } +
+ } +
+ `, +}) +export class AccessibilityGuideComponent { + protected readonly intro = ACCESSIBILITY_PAGE_INTRO; + protected readonly skillFile = ACCESSIBILITY_SKILL_FILE; + protected readonly skillInstall: AccessibilityInstallTarget[] = ACCESSIBILITY_SKILL_INSTALL; + protected readonly skillSummary = ACCESSIBILITY_SKILL_SUMMARY; + protected readonly layers: AccessibilityLayer[] = ACCESSIBILITY_LAYERS; + protected readonly sections: AccessibilityDocSection[] = ACCESSIBILITY_SECTIONS; +} + +const meta: Meta = { + title: 'Foundations/Accessibility', + component: AccessibilityGuideComponent, + decorators: [ + moduleMetadata({ + imports: [AccessibilityGuideComponent], + }), + ], + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'How to weave accessibility into everyday development: a downloadable agent skill, ' + + 'Storybook’s a11y addon, linting, automated tests, CI, and the manual checks those ' + + 'tools cannot replace.', + }, + }, + }, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +/** Process guide plus a downloadable agent skill for Cursor, Claude Code, and similar tools. */ +export const Guide: Story = { + render: () => ({ + template: ``, + }), +}; diff --git a/packages/react/.storybook/main.ts b/packages/react/.storybook/main.ts index b8948c71..a16f5ee1 100644 --- a/packages/react/.storybook/main.ts +++ b/packages/react/.storybook/main.ts @@ -3,6 +3,7 @@ import type { StorybookConfig } from '@storybook/react-vite'; const config: StorybookConfig = { stories: ['../src/**/*.stories.@(ts|tsx)'], addons: ['@storybook/addon-a11y', '@storybook/addon-docs'], + staticDirs: [{ from: '../../storybook-config/static', to: 'downloads' }], framework: { name: '@storybook/react-vite', options: {}, diff --git a/packages/react/src/foundations/accessibility.stories.tsx b/packages/react/src/foundations/accessibility.stories.tsx new file mode 100644 index 00000000..ac534c71 --- /dev/null +++ b/packages/react/src/foundations/accessibility.stories.tsx @@ -0,0 +1,178 @@ +import { DownloadSimpleIcon } from '@phosphor-icons/react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { + ACCESSIBILITY_LAYERS, + ACCESSIBILITY_PAGE_INTRO, + ACCESSIBILITY_SECTIONS, + ACCESSIBILITY_SKILL_FILE, + ACCESSIBILITY_SKILL_INSTALL, + ACCESSIBILITY_SKILL_SUMMARY, +} from '@surfnet/curve-storybook-config'; + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card'; + +function AccessibilityGuide() { + return ( +
+

Accessibility

+

{ACCESSIBILITY_PAGE_INTRO}

+ + + +

+ Agent skill for coding assistants +

+ + A markdown skill you can drop into Cursor, Claude Code, or another coding agent. It + teaches the agent to apply semantic HTML, accessible names, heading structure, keyboard + access, skip links, and focus styles whenever it writes or reviews UI. + +
+ + {ACCESSIBILITY_SKILL_SUMMARY.map((paragraph) => ( +

+ {paragraph} +

+ ))} + + + + + + + + + + {ACCESSIBILITY_SKILL_INSTALL.map((row) => ( + + + + + ))} + +
Where to install the downloaded skill file
+ Tool + + Save as +
+ {row.tool} + {row.path}
+

+ +

+
+
+ +
+

+ A layered process +

+

+ No single tool covers accessibility. Stack a cheap check at write-time, a linter on save, + axe in Storybook and CI, and a short keyboard pass before you ship. +

+ + + + + + + + + + + {ACCESSIBILITY_LAYERS.map((row) => ( + + + + + + ))} + +
+ Accessibility checks by layer of the developer process +
+ Layer + + When + + What it catches +
+ {row.layer} + {row.when}{row.catches}
+
+ + {ACCESSIBILITY_SECTIONS.map((section) => ( +
+

+ {section.title} +

+ {section.paragraphs.map((paragraph) => ( +

+ {paragraph} +

+ ))} + {section.bullets ? ( +
    + {section.bullets.map((item) => ( +
  • {item}
  • + ))} +
+ ) : null} + {section.links ? ( + + ) : null} +
+ ))} +
+ ); +} + +const meta = { + title: 'Foundations/Accessibility', + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'How to weave accessibility into everyday development: a downloadable agent skill, ' + + 'Storybook’s a11y addon, linting, automated tests, CI, and the manual checks those ' + + 'tools cannot replace.', + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** Process guide plus a downloadable agent skill for Cursor, Claude Code, and similar tools. */ +export const Guide: Story = { + render: () => , +}; diff --git a/packages/storybook-config/src/accessibility.ts b/packages/storybook-config/src/accessibility.ts new file mode 100644 index 00000000..4e5f3c66 --- /dev/null +++ b/packages/storybook-config/src/accessibility.ts @@ -0,0 +1,188 @@ +/** + * Shared copy for the Foundations / Accessibility documentation stories. + * React and Angular both render from these helpers so the two Storybooks stay in + * sync. No JSX / no framework imports here. + */ + +export const ACCESSIBILITY_SKILL_FILE = { + filename: 'accessibility.skill.md', + /** Served via Storybook `staticDirs` from this package's `static/` folder. */ + href: './downloads/accessibility.skill.md', +} as const; + +export const ACCESSIBILITY_PAGE_INTRO = + 'Curve components ship with accessible primitives: correct roles, keyboard behaviour, and visible focus rings. That is necessary, not sufficient. The screens you compose still need semantic structure, accessible names, and a process that catches regressions before they reach production.'; + +export interface AccessibilityInstallTarget { + tool: string; + path: string; +} + +export const ACCESSIBILITY_SKILL_INSTALL: AccessibilityInstallTarget[] = [ + { tool: 'Cursor', path: '.cursor/skills/accessibility/SKILL.md' }, + { tool: 'Claude Code', path: '.claude/skills/accessibility/SKILL.md' }, + { tool: 'Codex / other agents', path: '.agents/skills/accessibility/SKILL.md' }, +]; + +export const ACCESSIBILITY_SKILL_SUMMARY = [ + 'Rename the download to SKILL.md and place it in an accessibility folder under your agent’s skills directory (see the table). The YAML frontmatter is required — agents use name and description to decide when to load it.', + 'Once installed, the skill applies whenever the agent writes or reviews UI. Point at a component with “review this for accessibility”, or keep it in the project so it runs without being asked.', +]; + +export interface AccessibilityLayer { + layer: string; + when: string; + catches: string; +} + +export const ACCESSIBILITY_LAYERS: AccessibilityLayer[] = [ + { + layer: 'Agent skill', + when: 'While writing and reviewing UI', + catches: 'Wrong element, missing name, skipped headings, stripped focus styles', + }, + { + layer: 'Lint', + when: 'On save and in pull requests', + catches: 'Missing labels and alt text, click handlers on non-controls, static ARIA mistakes', + }, + { + layer: 'Storybook a11y addon', + when: 'During visual review of a story', + catches: 'axe-core violations: contrast, names, ARIA, duplicate ids', + }, + { + layer: 'Automated tests + CI', + when: 'On every pull request', + catches: 'Regressions on critical flows (forms, navigation, dialogs)', + }, + { + layer: 'Keyboard and screen reader', + when: 'Before shipping a new view', + catches: 'Focus order, skip links, heading logic, real-world use — things axe cannot see', + }, +]; + +export interface AccessibilityDocLink { + label: string; + href: string; +} + +export interface AccessibilityDocSection { + id: string; + title: string; + paragraphs: string[]; + bullets?: string[]; + links?: AccessibilityDocLink[]; +} + +export const ACCESSIBILITY_SECTIONS: AccessibilityDocSection[] = [ + { + id: 'components', + title: 'Prefer design-system components', + paragraphs: [ + 'Reach for Curve (or your own design-system primitive) before restyling a div. Buttons, inputs, dialogs, and menus already expose the right role, keyboard behaviour, and focus-visible ring. Composing them incorrectly — wrapping a button in another button, stripping the title from a dialog, replacing a link with a clickable card — is how accessibility regresses even when the primitives are sound.', + ], + }, + { + id: 'storybook', + title: 'Storybook accessibility addon', + paragraphs: [ + 'Both Curve Storybooks ship @storybook/addon-a11y, which runs axe-core against the current story. Open the Accessibility panel in the addons tray while reviewing a component. Treat violations as bugs, not as noise to click away.', + 'Consuming apps should add the same addon to their own Storybook. Optionally fail CI by running the Storybook test runner with the a11y addon enabled, so a contrast or name regression cannot merge silently.', + ], + bullets: [ + 'Add @storybook/addon-a11y next to @storybook/addon-docs.', + 'Check the Accessibility panel on every new or changed story.', + 'Wire the Storybook test runner (or equivalent) so axe failures fail the build.', + ], + links: [ + { + label: 'Storybook accessibility tests', + href: 'https://storybook.js.org/docs/writing-tests/accessibility-testing', + }, + { label: 'axe-core', href: 'https://github.com/dequelabs/axe-core' }, + ], + }, + { + id: 'lint', + title: 'Lint while you type', + paragraphs: [ + 'A linter catches a class of mistakes at the cursor, before a browser is involved. Enable the accessibility ruleset that matches the template language, and do not disable those rules without a named, reviewed exception.', + ], + bullets: [ + 'React / JSX: eslint-plugin-jsx-a11y (recommended or strict).', + 'Angular: @angular-eslint template accessibility rules (@angular-eslint/template-accessibility-*).', + 'Vue: eslint-plugin-vuejs-accessibility.', + 'Editor: the axe Accessibility Linter extension for VS Code / Cursor highlights issues in HTML and JSX as you edit.', + ], + links: [ + { + label: 'eslint-plugin-jsx-a11y', + href: 'https://github.com/jsx-eslint/eslint-plugin-jsx-a11y', + }, + { + label: 'angular-eslint', + href: 'https://github.com/angular-eslint/angular-eslint', + }, + { + label: 'eslint-plugin-vuejs-accessibility', + href: 'https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility', + }, + ], + }, + { + id: 'tests', + title: 'Automated tests', + paragraphs: [ + 'Run axe against rendered UI in unit and end-to-end tests. Do not try to axe-scan every pixel of the app; cover the journeys that matter: sign-in, primary forms, navigation, and any dialog or overlay.', + ], + bullets: [ + 'Component tests: jest-axe or vitest-axe on a rendered snapshot of the component.', + 'End-to-end: @axe-core/playwright or cypress-axe on critical user journeys.', + 'Assert on more than axe when the behaviour is specific: focus moves into a dialog, Escape closes it, the trigger is focused again on close.', + ], + links: [ + { + label: '@axe-core/playwright', + href: 'https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright', + }, + { label: 'jest-axe', href: 'https://github.com/nickcolley/jest-axe' }, + ], + }, + { + id: 'ci', + title: 'Continuous integration', + paragraphs: [ + 'Accessibility checks that only run on a local machine will be skipped under deadline pressure. Put a thin, reliable subset on every pull request, and keep slower scans (full Lighthouse, visual regression) on main or a nightly job.', + ], + bullets: [ + 'PR pipeline: lint (including a11y rules) + unit tests that include jest-axe / equivalent.', + 'PR pipeline: Playwright (or Cypress) smoke with axe on the critical journeys.', + 'Optional on main or nightly: Lighthouse CI accessibility category, pa11y-ci, or a full Storybook test-runner pass.', + 'Do not fail the build on known backlog issues without a tracked exception; do fail it on new violations in touched flows.', + ], + links: [ + { label: 'Lighthouse CI', href: 'https://github.com/GoogleChrome/lighthouse-ci' }, + { label: 'pa11y-ci', href: 'https://github.com/pa11y/pa11y-ci' }, + ], + }, + { + id: 'manual', + title: 'Manual checks automation cannot replace', + paragraphs: [ + 'axe and linters do not know whether a heading outline makes sense, whether a control should have been a link, or whether Tab order matches the visual layout. After you build a view, walk it yourself.', + ], + bullets: [ + 'Tab (and Shift+Tab) through the view: every control is reachable, order matches the layout, focus is never lost.', + 'Chrome DevTools → Elements → Accessibility pane → Headings: one h1, no skipped levels, reads as a table of contents.', + 'The same pane → Name: every input, button, and link has an accessible name. Placeholder is not a name.', + 'Skip link appears on first Tab and jumps to main content.', + 'Screen-reader spot check (VoiceOver, NVDA, or TalkBack) on new flows — especially forms, dialogs, and navigation.', + ], + links: [ + { label: 'WCAG 2.2 quick reference', href: 'https://www.w3.org/WAI/WCAG22/quickref/' }, + { label: 'WebAIM', href: 'https://webaim.org/' }, + ], + }, +]; diff --git a/packages/storybook-config/src/index.ts b/packages/storybook-config/src/index.ts index ad051ae6..97951712 100644 --- a/packages/storybook-config/src/index.ts +++ b/packages/storybook-config/src/index.ts @@ -1,3 +1,17 @@ +export { + ACCESSIBILITY_LAYERS, + ACCESSIBILITY_PAGE_INTRO, + ACCESSIBILITY_SECTIONS, + ACCESSIBILITY_SKILL_FILE, + ACCESSIBILITY_SKILL_INSTALL, + ACCESSIBILITY_SKILL_SUMMARY, +} from './accessibility.js'; +export type { + AccessibilityDocLink, + AccessibilityDocSection, + AccessibilityInstallTarget, + AccessibilityLayer, +} from './accessibility.js'; export { FRAMEWORKS, frameworkGlobalTypes, frameworkSwitcher } from './frameworks.js'; export type { Framework, FrameworkTarget } from './frameworks.js'; export { THEME_NAMES, themeGlobalTypes, themeInitialGlobals, themeSwitcher } from './themes.js'; diff --git a/packages/storybook-config/static/accessibility.skill.md b/packages/storybook-config/static/accessibility.skill.md new file mode 100644 index 00000000..41c7f965 --- /dev/null +++ b/packages/storybook-config/static/accessibility.skill.md @@ -0,0 +1,140 @@ +--- +name: accessibility +description: >- + Guides frontend accessibility: semantic HTML, heading structure, links vs + buttons, form labels and accessible names, keyboard Tab order, skip links, + and hover/focus styles. Use when writing or reviewing React, Angular, Vue, + HTML, or CSS UI, or when the user mentions a11y, accessibility, WCAG, + keyboard navigation, skip links, headings, outline, or accessible names. +--- + +# Web accessibility + +Apply this skill whenever you **write, edit, or review** UI (React, Angular, Vue, HTML, CSS). Do not wait for the user to mention a11y. + +Prefer native HTML before ARIA. Do not "fix" accessibility with `role="button"` + `tabindex="0"` + a click handler on a `div` when a native ` -

- - - -
-

- A layered process -

-

- No single tool covers accessibility. Stack a cheap check at write-time, a linter on save, - axe in Storybook and CI, and a short keyboard pass before you ship. -

- - - - - - - - - - - {ACCESSIBILITY_LAYERS.map((row) => ( - - - - - - ))} - -
- Accessibility checks by layer of the developer process -
- Layer - - When - - What it catches -
- {row.layer} - {row.when}{row.catches}
-
- - {ACCESSIBILITY_SECTIONS.map((section) => ( -
-

- {section.title} -

- {section.paragraphs.map((paragraph) => ( -

- {paragraph} -

- ))} - {section.bullets ? ( -
    - {section.bullets.map((item) => ( -
  • {item}
  • - ))} -
- ) : null} - {section.links ? ( - - ) : null} -
- ))} - - ); -} - -const meta = { - title: 'Foundations/Accessibility', - parameters: { - layout: 'padded', - docs: { - description: { - component: - 'How to weave accessibility into everyday development: a downloadable agent skill, ' + - 'Storybook’s a11y addon, linting, automated tests, CI, and the manual checks those ' + - 'tools cannot replace.', - }, - }, - }, - tags: ['autodocs'], -} satisfies Meta; - -export default meta; - -type Story = StoryObj; - -/** Process guide plus a downloadable agent skill for Cursor, Claude Code, and similar tools. */ -export const Guide: Story = { - render: () => , -}; diff --git a/packages/storybook-config/docs/accessibility/01-introductie-overzicht.mdx b/packages/storybook-config/docs/accessibility/01-introductie-overzicht.mdx new file mode 100644 index 00000000..b4b75499 --- /dev/null +++ b/packages/storybook-config/docs/accessibility/01-introductie-overzicht.mdx @@ -0,0 +1,65 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Toegankelijkheid + +Curve-componenten zijn gebouwd op toegankelijke primitieven: de juiste rollen, werkende +toetsenbordbediening en een zichtbare focusring. Dat is noodzakelijk, maar niet genoeg. De schermen die +jij daarmee samenstelt hebben nog steeds een logische structuur nodig, toegankelijke namen, en een +werkwijze die fouten opmerkt vóórdat ze in productie staan. + +## Waarom het ertoe doet + +Ongeveer één op de zes mensen leeft met een beperking, en veel meer mensen hebben er tijdelijk of +situationeel mee te maken — een gebroken pols, een ooginfectie, fel zonlicht op een perron. Werken aan +toegankelijkheid is zelden werk voor een kleine minderheid: het is hetzelfde werk dat een interface +bruikbaar maakt met één hand, op een slechte verbinding, of aan het eind van een lange dag. + +Voor de organisaties waarvoor Curve gemaakt is, is het bovendien niet vrijblijvend. Nederlandse +overheidsinstellingen en publiek gefinancierd onderwijs vallen onder de Europese norm EN 301 549, die +de WCAG-succescriteria integraal overneemt en een gepubliceerde toegankelijkheidsverklaring vereist. De +European Accessibility Act legt vergelijkbare verplichtingen op aan veel private diensten. In de +praktijk komt iedereen op dezelfde plek uit: **WCAG 2.2 niveau AA is de norm**, en niveau A is geen +mijlpaal om trots op te zijn. + +Het gat tussen bedoeling en werkelijkheid is groot. WebAIM scant jaarlijks de homepages van een miljoen +websites en vindt op ongeveer 95% daarvan aantoonbare WCAG-fouten — en dat zijn alleen de fouten die +een machine kan zien. + +## De vier principes + +WCAG ordent alles onder vier begrippen, samen POUR genoemd. Ze zijn een bruikbare toets wanneer een +specifiek succescriterium niet direct van toepassing lijkt: + +- **Perceivable (waarneembaar)** — de informatie komt aan, wat iemand ook kan zien of horen. + Tekstalternatieven, ondertiteling, voldoende contrast, en betekenis die nooit alleen in kleur zit. +- **Operable (bedienbaar)** — de interface is te bedienen, hoe iemand dat ook doet. Toetsenbordtoegang, + voldoende grote klikdoelen, geen tijdslimieten die je klemzetten, geen beweging die je niet kunt + stoppen. +- **Understandable (begrijpelijk)** — gedrag en taal zijn voorspelbaar. Consistente navigatie, duidelijke + labels, en foutmeldingen die uitleggen hoe je verder komt. +- **Robust (robuust)** — hulpsoftware kan het resultaat interpreteren. Geldige, semantische opmaak met + kloppende namen, rollen en statussen. + +## Wat Curve je geeft, en wat niet + +Elk Curve-component is gebouwd op een headless primitief — Base UI in React, de Brain-laag van Spartan +in Angular — dat het WAI-ARIA-patroon voor dat component implementeert: rollen, toetsenbordbediening, +focusbeheer en status. De design tokens leveren kleurcombinaties met gecontroleerd contrast en een +zichtbare `focus-visible`-ring. + +Niets daarvan overleeft slordig samenstellen. Een knop in een knop, een dialoog zonder titel, een +icoonknop zonder toegankelijke naam, een kopniveau gekozen op lettergrootte — dat zijn allemaal fouten +die je bovenop correcte primitieven kunt introduceren. Over die categorie problemen gaat de rest van +deze sectie. + +## Hoe deze sectie is opgebouwd + +- **Introductie** — deze pagina, plus voor wie je bouwt en hoe andere design systems hetzelfde probleem + aanpakken. +- **Je werk testen** — de gelaagde aanpak, opgesplitst in wat je met de hand controleert en wat je + automatiseert. +- **Do's en don'ts** — de terugkerende fouten, ARIA in het bijzonder, en waar je op moet letten wanneer + je met AI werkt. +- **Meer leren** — normen, gereedschap en leesvoer die de moeite waard zijn. diff --git a/packages/storybook-config/docs/accessibility/02-introductie-beperkingen.mdx b/packages/storybook-config/docs/accessibility/02-introductie-beperkingen.mdx new file mode 100644 index 00000000..79538c62 --- /dev/null +++ b/packages/storybook-config/docs/accessibility/02-introductie-beperkingen.mdx @@ -0,0 +1,88 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Verschillende beperkingen (persona's) + +"Toegankelijk" gaat niet over één doelgroep. De persona's hieronder dekken de behoeften die de +WCAG-criteria daadwerkelijk sturen, en ze lopen elk op andere fouten vast. Eén keer doorlezen zorgt +ervoor dat de regels niet langer willekeurig aanvoelen: je kunt meestal voorspellen welk criterium een +ontwerp breekt door je voor te stellen wie er niet langs komt. + +| Persona | Hoe diegene een scherm gebruikt | Waar het misgaat | +| ---------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------- | +| Blind, screenreadergebruiker | Alleen toetsenbord, luistert; navigeert op kop en link | Naamloze knoppen, `div`-knoppen, betekenis die alleen zichtbaar is | +| Slechtziend, vergroting | 200–400% zoom, hoog contrast, grote letters | Vaste breedtes, te licht contrast, tekst in afbeeldingen | +| Kleurenblind | Ziet de opmaak, niet het kleurverschil | Status alleen in kleur, rood/groen-combinaties, ongelabelde grafieken | +| Doof of slechthorend | Leest in plaats van luistert | Video zonder ondertiteling, geluidssignalen, geen transcript | +| Beperkte motoriek | Toetsenbord, switch, spraak of oogbesturing | Kleine klikdoelen, alleen slepen, hover-menu's, korte time-outs | +| Cognitief of neurodivergent | Heeft rust, voorspelbaarheid en eenvoud nodig | Jargon, automatische beweging, tijdsdruk, vage foutmeldingen | +| Tijdelijk of situationeel | Van alles, kort en onhandig | Alles hierboven, op het slechtst denkbare moment | + +## Blind, screenreadergebruiker + +Gebruikt NVDA, JAWS, VoiceOver of TalkBack en ziet de opmaak nooit. De pagina wordt als structuur +opgenomen: een lijst met koppen, een lijst met links, landmarks, en dan pas inhoud. Visuele groepering +betekent niets als die niet in de opmaak zit. + +Wat dit van je vraagt: echte koppen in de juiste volgorde, één `h1`, landmarks (`main`, `nav`, +`header`) in plaats van anonieme `div`s, een toegankelijke naam op elk bedienbaar element, en +alternatieve tekst die vertelt wat een afbeelding overbrengt in plaats van wat erop staat. Een +icoonknop die wordt voorgelezen als "knop" is een doodlopende weg. + +## Slechtziend, vergroting + +Zoomt tot 200% of verder, of zet het besturingssysteem op grote letters. Bij 400% zoom op een viewport +van 1280px is jouw desktoplayout nog ongeveer 320px breed — het WCAG-criterium Reflow zegt dat de +inhoud daar moet blijven werken, in één scrollrichting, zonder dat er iets wegvalt. + +Wat dit van je vraagt: layouts die meebewegen in plaats van horizontaal scrollen, tekst die 200% +vergroting overleeft zonder over elkaar te vallen, contrast van minimaal 4,5:1 voor lopende tekst en +3:1 voor interface-elementen en iconen, en geen essentiële tekst die in een afbeelding zit. + +## Kleurenblind + +Ongeveer 8% van de mannen en 0,5% van de vrouwen heeft een vorm van kleurenblindheid, meestal +rood/groen. Zij zien jouw opmaak prima; wat wegvalt is het onderscheid dat je in kleur hebt gestopt. + +Wat dit van je vraagt: laat kleur nooit het enige signaal zijn. Combineer het met een icoon, een label, +een patroon of een positie. Een invoerveld dat rood kleurt is onzichtbaar; een veld dat rood kleurt, +een waarschuwingsicoon krijgt en de zin "Vul een geldig e-mailadres in" toont, werkt voor iedereen. + +## Doof of slechthorend + +Leest in plaats van luistert. Beschrijft ook iedereen in een kantoortuin zonder koptelefoon. + +Wat dit van je vraagt: ondertiteling bij alles waarin gesproken wordt, transcripten bij audio, en geen +statuswijziging die alleen met geluid wordt aangekondigd. + +## Beperkte motoriek + +Een brede groep: tremor, RSI, bediening met één hand, een switch, spraakbesturing, oogbesturing. Wat ze +delen is dat nauwkeurig aanwijzen duur of onmogelijk is. + +Wat dit van je vraagt: alles moet bereikbaar en bedienbaar zijn met het toetsenbord, in een volgorde +die overeenkomt met de opmaak, met focus die altijd zichtbaar is. Klikdoelen zijn minimaal 24×24 +CSS-pixels (44×44 is comfortabel). Alles wat je met slepen kunt doen, moet ook met één klik of met het +toetsenbord kunnen. Inhoud die alleen bij hover verschijnt is onbereikbaar — koppel die ook aan focus, +en zorg dat je hem kunt wegklikken zonder de muis te verplaatsen. + +## Cognitief of neurodivergent + +Dyslexie, ADHD, autisme, verschillen in geheugen of verwerkingssnelheid, angst — en iedereen onder +druk. Dit is de grootste groep en de groep die het minst geholpen wordt door automatische controles, +omdat niets ervan in de opmaak te zien is. + +Wat dit van je vraagt: gewone taal in plaats van slimme taal, navigatie en knoppen die steeds op +dezelfde plek staan, geen onverwachte beweging of automatisch afspelen, respect voor +`prefers-reduced-motion`, ruime of verlengbare tijdslimieten, foutmeldingen die het probleem én de +oplossing noemen, en niets dat afhangt van wat iemand zich uit een vorige stap herinnert. + +## Tijdelijk of situationeel + +Een gebroken arm, een ooginfectie, een kind op de arm, een gebarsten scherm, fel zonlicht, een tunnel. +Niemand hiervan noemt zichzelf beperkt, en ze lopen allemaal tegen dezelfde drempels aan. + +Dit is het curb-cut-effect: ondertiteling wordt gebruikt in stille kantoren, sneltoetsen door ervaren +gebruikers, hoog contrast door mensen buiten. Werk dat je voor de persona's hierboven doet, is zelden +alleen voor hen. diff --git a/packages/storybook-config/docs/accessibility/03-introductie-andere-design-systems.mdx b/packages/storybook-config/docs/accessibility/03-introductie-andere-design-systems.mdx new file mode 100644 index 00000000..1021c8d0 --- /dev/null +++ b/packages/storybook-config/docs/accessibility/03-introductie-andere-design-systems.mdx @@ -0,0 +1,56 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Toegankelijkheid in andere design systems + +Elk volwassen design system heeft dezelfde vraag moeten beantwoorden: hoe voorkom je dat kennis over +toegankelijkheid in het hoofd van één specialist blijft zitten? Hun antwoorden zijn openbaar, en het is +verstandiger om ervan te lenen dan het opnieuw te bedenken. + +| Design system | Hoe toegankelijkheid er terugkomt | +| ---------------------------- | ------------------------------------------------------------------------------------------------------- | +| GOV.UK Design System | Acceptatiecriteria per component, gepubliceerd gebruikersonderzoek, openlijk vermelde bekende problemen | +| NL Design System | Door de community onderhouden richtlijnen per component, met het WCAG-criterium erbij vermeld | +| U.S. Web Design System | Een toegankelijkheidsparagraaf bij elk component, gekoppeld aan de verplichtingen uit Section 508 | +| IBM Carbon | Een apart tabblad per component met volledige tabellen voor toetsenbordbediening | +| Adobe Spectrum / React Aria | Gedrag ondergebracht in getoetste headless hooks, getest op een matrix van echte screenreaders | +| Atlassian Design System | Richtlijnen per component plus een definition of done op teamniveau | +| Shopify Polaris | Toegankelijkheid als expliciet fundament, met do's en don'ts per component | +| W3C ARIA Authoring Practices | Geen design system: de referentie-implementatie voor elk widgetpatroon | + +## Wat de moeite waard is om over te nemen + +**Leg de criteria per component vast, niet per systeem.** Het sterkste idee van GOV.UK zijn de +acceptatiecriteria voor toegankelijkheid: een korte lijst, gekoppeld aan het component, van wat waar +moet zijn voordat het af is. Daarmee wordt "is dit toegankelijk?" van een inschatting een checklist die +iemand zonder specialistische kennis kan aflopen. + +**Documenteer het toetsenbordcontract.** Zowel Carbon als de ARIA Authoring Practices Guide publiceren +een tabel met elke toets waarop een component reageert. Dat is het nuttigste dat de documentatie van een +component kan bevatten: het is in tien seconden te testen, en het is het eerste dat misgaat wanneer +iemand het component zelf namaakt. + +**Publiceer bekende problemen in plaats van ze te verbergen.** GOV.UK vermeldt per component welke +toegankelijkheidsproblemen nog niet zijn opgelost. Dat is ongemakkelijk en enorm bruikbaar: gebruikers +kunnen een geïnformeerde keuze maken in plaats van aan te nemen dat het systeem alles heeft afgedekt. + +**Stop gedrag in een getoetste laag.** React Aria van Adobe en de Brain-laag van Spartan delen hetzelfde +uitgangspunt: interactielogica is te subtiel om per product opnieuw te bouwen, dus hoort die in een +primitief dat tegen echte hulpsoftware is getest. Curve maakt dezelfde scheiding — Base UI in React, +Brain in Angular — en daarom gaan deze richtlijnen vooral over samenstellen en niet over het bouwen van +widgets. + +**Noem het WCAG-criterium waar een regel bij hoort.** Het NL Design System is hier goed in, en het +verandert hoe richtlijnen overkomen: "geef het invoerveld een label" is een mening, terwijl "1.3.1 Info +en relaties" een eis is met een bijbehorende test. + +## Waar Curve nu staat + +Curve erft zijn componentgedrag van Base UI en Spartan Brain, en zijn kleurcombinaties van design +tokens waarvan het contrast is gecontroleerd. Wat nog ontbreekt is de laag per component waar de +systemen hierboven naartoe zijn gegroeid: toetsenbordtabellen, acceptatiecriteria voor toegankelijkheid, +en een eerlijke lijst met bekende problemen op de documentatiepagina van elk component. + +Voeg je een component toe of beoordeel je er een, dan is dat de waardevolste documentatie die je kunt +achterlaten — waardevoller dan nog een gebruiksvoorbeeld. diff --git a/packages/storybook-config/docs/accessibility/04-testen-overzicht.mdx b/packages/storybook-config/docs/accessibility/04-testen-overzicht.mdx new file mode 100644 index 00000000..18c7a80f --- /dev/null +++ b/packages/storybook-config/docs/accessibility/04-testen-overzicht.mdx @@ -0,0 +1,50 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Je werk testen + +Eén tool dekt toegankelijkheid niet af. Afhankelijk van welk onderzoek je gelooft, vindt automatische +controle tussen een derde en de helft van de echte WCAG-fouten — en in de helft die wordt gemist zitten +de meeste problemen die iemand daadwerkelijk tegenhouden. Een pagina kan nul axe-meldingen geven en toch +onbruikbaar zijn. + +Het doel is dus niet om één tool te vinden. Het is om goedkope controles vroeg te stapelen, en de dure, +menselijke controles klein genoeg te houden dat ze ook echt gebeuren. + +## Een gelaagde aanpak + +| Laag | Wanneer | Wat het opmerkt | +| --------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------- | +| Agent skill | Tijdens het schrijven en reviewen | Verkeerd element, ontbrekende naam, overgeslagen koppen, weggehaalde focusstijlen | +| Linter | Bij opslaan en in pull requests | Ontbrekende labels en alt-teksten, klikafhandelaars op niet-knoppen, ARIA-fouten | +| Storybook a11y-addon | Bij het visueel beoordelen van een story | axe-core-meldingen: contrast, namen, ARIA, dubbele id's | +| Geautomatiseerde tests + CI | Bij elke pull request | Terugval op belangrijke flows (formulieren, navigatie, dialogen) | +| Toetsenbord en screenreader | Vlak voor je een nieuw scherm oplevert | Focusvolgorde, skiplinks, koplogica, echt gebruik — dingen die axe niet ziet | + +Elke laag is goedkoper dan de laag eronder en vindt minder. Dat is precies de bedoeling: tegen de tijd +dat je aan de handmatige controle toekomt, zijn de mechanische problemen al weg en houd je aandacht over +voor de afwegingen. + +## Waar je begint als je nog geen werkwijze hebt + +1. Installeer + [de agent skill](?path=/docs/toegankelijkheid-do-s-en-don-ts--wanneer-je-gebruik-maakt-van-ai) zodat nieuwe + code dichter bij goed begint. +2. Zet de toegankelijkheidsregels van je linter aan — één middag werk, blijvend resultaat. +3. Open het **Accessibility**-paneel in Storybook bij het component waar je nu aan werkt, en los op wat + er staat. +4. Loop je belangrijkste scherm één keer met het toetsenbord door. Je vindt gegarandeerd iets. +5. Hang axe pas daarna in CI, zodat je niet begint met een muur aan bestaande meldingen. + +Alle vijf tegelijk invoeren op een bestaand product levert een lijst op die niemand leest. In deze +volgorde toevoegen betekent dat elke nieuwe pull request iets beter is dan de vorige. + +## Twee regels om het eerlijk te houden + +**Behandel meldingen als bugs, niet als ruis.** Een toegankelijkheidspaneel dat altijd rood staat, leert +het team om het te negeren. Dat is erger dan het niet hebben. + +**Laat de build niet struikelen over je bestaande achterstand.** Laat hem falen op nieuwe fouten in de +code die wordt aangeraakt, en houd bekende problemen apart bij. Een geblokkeerde pipeline die iedereen +omzeilt, beschermt niemand. diff --git a/packages/storybook-config/docs/accessibility/05-testen-handmatig.mdx b/packages/storybook-config/docs/accessibility/05-testen-handmatig.mdx new file mode 100644 index 00000000..b18f2159 --- /dev/null +++ b/packages/storybook-config/docs/accessibility/05-testen-handmatig.mdx @@ -0,0 +1,90 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Handmatig testen + +axe en linters weten niet of een kopstructuur ergens op slaat, of iets een link had moeten zijn, of de +tabvolgorde overeenkomt met wat je ziet. Loop een scherm daarom zelf na nadat je het gebouwd hebt. De +hele ronde hieronder duurt ongeveer tien minuten en vraagt geen software die je nog niet hebt. + +## De toetsenbordronde + +Laat de muis los. Klik één keer in de adresbalk en druk op Tab. + +- Elk bedienbaar element is bereikbaar, en verder niets. +- De volgorde volgt de opmaak. Springt de focus van de header naar een link in de footer en weer terug, + dan lopen de DOM-volgorde en de visuele volgorde uiteen. +- De focus is **altijd zichtbaar**. Raak jij het spoor kwijt, dan iedereen. +- Shift + Tab loopt dezelfde route terug. +- Enter activeert links en knoppen; Spatie activeert knoppen en schakelaars. +- Binnen een samengesteld component — tabs, menu, keuzelijst, radiogroep — verplaatsen de pijltoetsen de + selectie en verlaat Tab het geheel. Eén stop voor het hele component, niet één per item. +- Esc sluit elke dialoog, popover of menu, en de focus keert terug naar het element dat + het opende. +- Je kunt nergens vast komen te zitten. Blijft Tab eindeloos rondgaan in iets dat geen modale + dialoog is, dan heb je een focusval. +- De skiplink verschijnt bij de eerste Tab en verplaatst de focus daadwerkelijk naar de + hoofdinhoud. + +## Zoomen en meebewegen + +- Zoom naar **200%**. Niets overlapt, niets valt weg, geen tekst wordt onleesbaar. +- Zoom naar **400%** (of maak het venster 320px smal). De opmaak vouwt terug naar één kolom en scrollt + alleen verticaal. Horizontaal scrollen is hier een Reflow-fout. +- Vergroot alleen de tekst (Firefox doet dat netjes). Containers met een vaste hoogte die hun tekst + afknippen, vallen meteen op. + +## Structuur en namen + +Open DevTools → **Elements** → het paneel **Accessibility**, of gebruik een browserextensie die de +structuur toont. + +- **Koppen**: precies één `h1`, geen overgeslagen niveaus, en als je alleen de koppen leest krijg je een + inhoudsopgave die klopt. Een kopniveau is structuur, nooit een keuze voor lettergrootte. +- **Toegankelijke namen**: elk invoerveld, elke knop en elke link heeft er één. Een placeholder is geen + naam — die verdwijnt zodra iemand begint te typen. Een icoonknop heeft een label nodig. Zes keer "Lees + meer" op één pagina zegt een screenreadergebruiker niets. +- **Landmarks**: `header`, `nav`, `main` en `footer` bestaan, en er is precies één `main`. +- **Afbeeldingen**: decoratieve hebben `alt=""` zodat ze worden overgeslagen; betekenisvolle beschrijven + wat ze overbrengen, niet hoe ze eruitzien. + +## Kleur en beweging + +- Controleer het contrast van tekst én interface-elementen: 4,5:1 voor lopende tekst, 3:1 voor grote + tekst, iconen en randen. Elke kleurkiezer in DevTools laat dit zien. +- Maak een schermafbeelding en bekijk hem in grijstinten. Alles wat je dan niet meer kunt onderscheiden, + leunde op kleur alleen. +- Zet **Verminder beweging** aan in je systeeminstellingen en herlaad. Animaties horen korter te worden + of te stoppen, niet gewoon door te gaan. + +## Formulieren in het bijzonder + +Bij formulieren kost een toegankelijkheidsfout echt geld, dus loop die een tweede keer na. + +- Klikken op het label zet de focus in het veld (dat bewijst dat het label gekoppeld is en niet alleen + ernaast staat). +- Fouten worden voorgelezen, niet alleen gekleurd. Na een mislukte verzending gaat de focus naar een + zinnige plek en is de melding bereikbaar vanaf het veld waar hij bij hoort. +- De fouttekst zegt wat je moet doen: "Vul een datum in als DD-MM-JJJJ", niet "Ongeldige invoer". +- Verplichte velden zijn in tekst gemarkeerd, niet met kleur of een onverklaarde asterisk. +- Niets hangt af van hoveren, en er gaat niets verloren als iemand er twintig minuten over doet. + +## Een steekproef met een screenreader + +Je hoeft er niet vloeiend in te zijn. Vijf minuten luisteren zegt meer dan een uur opmaak lezen, omdat +je hoort wat de browser werkelijk doorgeeft in plaats van wat jij bedoelde. + +| Screenreader | Starten | Basis | +| ---------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| VoiceOver (macOS) | Cmd + F5 | Ctrl+Option+pijltjes om te lezen, +U voor de rotor | +| NVDA (Windows, gratis) | Ctrl+Alt+N | Pijltjes om te lezen, H voor koppen, Insert+F7 voor de elementenlijst | +| TalkBack (Android) | Instellingen voor toegankelijkheid | Veeg naar rechts om verder te gaan, dubbeltik om te activeren | + +Waar je op let: noemt elk element een naam, een rol en zijn status ("Filters, knop, ingeklapt")? Komt de +koppenlijst overeen met de pagina? Wordt er bij het openen van een dialoog gezegd dat het een dialoog is, +en gaat de focus naar binnen? Word je op de hoogte gebracht als er iets verandert zonder dat de pagina +herlaadt? + +Zet hem uit met dezelfde toetsencombinatie. Doe dit één keer per nieuw scherm en je vindt dingen die geen +enkele tool meldt. diff --git a/packages/storybook-config/docs/accessibility/06-testen-geautomatiseerd.mdx b/packages/storybook-config/docs/accessibility/06-testen-geautomatiseerd.mdx new file mode 100644 index 00000000..c6d4e93d --- /dev/null +++ b/packages/storybook-config/docs/accessibility/06-testen-geautomatiseerd.mdx @@ -0,0 +1,92 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Geautomatiseerd testen + +Automatisering bestaat om te voorkomen dat je dezelfde fout steeds opnieuw ontdekt. Het vertelt je niet +of je interface ergens op slaat, maar het vindt wél elke keer, op elke branch en gratis het ontbrekende +label, de tekst met 3:1 contrast en de dubbele `id`. + +Vier lagen, van goedkoop naar duur. + +## 1. Linten tijdens het typen + +Een linter vangt een hele categorie fouten op bij de cursor, nog voordat er een browser aan te pas komt. +Zet de regelset aan die bij jouw templatetaal hoort, en schakel regels niet uit zonder een benoemde, +beoordeelde uitzondering. + +- **React / JSX**: [`eslint-plugin-jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y), + `recommended` of `strict`. +- **Angular**: de templateregels van + [`angular-eslint`](https://github.com/angular-eslint/angular-eslint) + (`@angular-eslint/template-accessibility-*`). +- **Editor**: de axe Accessibility Linter-extensie voor VS Code / Cursor markeert problemen in HTML en + JSX terwijl je typt. + +Linters zien alleen statische opmaak, dus ze kunnen niet weten dat bij `role="tab"` het bijbehorende +`tabpanel` ontbreekt. Het blijft het waardevolste uur dat je eraan besteedt. + +## 2. De Storybook-addon voor toegankelijkheid + +Beide Curve-Storybooks bevatten +[`@storybook/addon-a11y`](https://storybook.js.org/docs/writing-tests/accessibility-testing), die +axe-core loslaat op de weergegeven story. Open het paneel **Accessibility** in de addonbalk terwijl je +een component bekijkt. + +Dit is de beste plek om contrast- en naamgevingsproblemen te vinden, omdat een story het component +isoleert van de ruis van een volledige pagina. Projecten die Curve gebruiken, doen er goed aan dezelfde +addon in hun eigen Storybook te zetten. + +Een component los bekijken legt bovendien iets bloot dat een volledige pagina verbergt: een component +dat alleen toegankelijk is dankzij opmaak die zijn ouder toevallig meelevert. + +## 3. Component- en end-to-endtests + +Laat axe los op weergegeven UI in je tests. Probeer niet elke pixel van de applicatie te scannen — dek de +routes af die ertoe doen: inloggen, de belangrijkste formulieren, navigatie, en elke dialoog of overlay. + +- **Componenttests**: [`jest-axe`](https://github.com/nickcolley/jest-axe) of `vitest-axe` op een + weergegeven component. +- **End-to-end**: [`@axe-core/playwright`](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/playwright) + of `cypress-axe` op de belangrijkste gebruikersroutes. +- **Storybook**: de test runner kan de a11y-controles voor elke story in CI uitvoeren, wat je met + nauwelijks schrijfwerk brede dekking oplevert. + +Test op meer dan axe wanneer het gedrag specifiek is. axe kan niet vaststellen dat de focus in de dialoog +belandde, dat Esc hem sloot en dat de focus terugkeerde naar de knop — een test wel: + +```ts +await userEvent.click(screen.getByRole('button', { name: 'Instellingen openen' })); +const dialog = screen.getByRole('dialog', { name: 'Instellingen' }); +expect(dialog).toContainElement(document.activeElement); + +await userEvent.keyboard('{Escape}'); +expect(dialog).not.toBeInTheDocument(); +expect(screen.getByRole('button', { name: 'Instellingen openen' })).toHaveFocus(); +``` + +Zoeken op rol en toegankelijke naam, zoals hierboven, is zelf al een toegankelijkheidstest: kan +`getByRole('button', { name: ... })` jouw knop niet vinden, dan een screenreader ook niet. + +## 4. Continuous integration + +Controles die alleen lokaal draaien, worden overgeslagen zodra de druk oploopt. Zet een dunne, betrouwbare +selectie op elke pull request en laat de tragere scans op `main` of 's nachts draaien. + +- **Elke pull request**: linten inclusief a11y-regels, plus unittests met axe-asserties. +- **Elke pull request**: een korte Playwright- of Cypress-run met axe op de belangrijkste routes. +- **'s Nachts of op `main`**: de toegankelijkheidscategorie van + [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci), + [`pa11y-ci`](https://github.com/pa11y/pa11y-ci), of een volledige run van de Storybook test runner. + +Laat de build falen op nieuwe fouten in de code die wordt aangeraakt. Houd bekende problemen bij als +issues met een eigenaar, niet als een permanent rode pipeline die iedereen heeft leren negeren. + +## Wat hier allemaal niet uit komt + +Alles wat een afweging vraagt: of de kopstructuur een verhaal vertelt, of iets een link had moeten zijn, +of de tabvolgorde het ontwerp volgt, of de foutmelding helpt, of de flow werkt met een screenreader. +Automatisering verkleint de handmatige ronde — zie +[Handmatig testen](?path=/docs/toegankelijkheid-je-werk-testen--handmatig-testen) — maar +schaft hem nooit af. diff --git a/packages/storybook-config/docs/accessibility/07-dos-en-donts-overzicht.mdx b/packages/storybook-config/docs/accessibility/07-dos-en-donts-overzicht.mdx new file mode 100644 index 00000000..05844d54 --- /dev/null +++ b/packages/storybook-config/docs/accessibility/07-dos-en-donts-overzicht.mdx @@ -0,0 +1,66 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Do's en don'ts + +De meeste toegankelijkheidsfouten zijn niet bijzonder. Een handvol terugkerende missers verklaart het +overgrote deel van wat audits vinden, en ze zijn allemaal goedkoop te voorkomen als je weet waar je op +moet letten. + +## Pak eerst een component uit het design system + +Voordat je een `div` gaat opmaken: kijk of Curve het al heeft. Knoppen, invoervelden, dialogen en menu's +hebben de juiste rol, toetsenbordbediening en focusring al, omdat hun gedrag uit een getoetst primitief +komt — Base UI in React, de Brain-laag van Spartan in Angular. + +Ze verkeerd samenstellen is hoe toegankelijkheid alsnog wegglijdt, ook als de primitieven kloppen: een +knop in een knop, een dialoog zonder titel, een link vervangen door een klikbare kaart, of een kopniveau +kiezen op lettergrootte. + +## De korte lijst + +| Wel | Niet | +| -------------------------------------------------------------- | ----------------------------------------------------- | +| `button` voor acties, `a href` voor navigatie | Een klikafhandelaar op een `div` of `span` | +| Elk element een zichtbaar tekstlabel geven | Een placeholder of `title` als label gebruiken | +| De focusring behouden, of een betere ontwerpen | `outline: none` zonder vervanging | +| Linkteksten die los van hun context kloppen | Acht keer "Lees meer" of "Klik hier" op één pagina | +| Kopniveaus kiezen op basis van structuur | `h4` kiezen omdat dat het beste formaat is | +| Kleur combineren met tekst, een icoon of een patroon | Status alleen met kleur aangeven | +| Beschrijven wat een afbeelding overbrengt | `alt="afbeelding"`, `alt="icoon"` of een bestandsnaam | +| Vertellen wat er misging én hoe je het oplost | "Ongeldige invoer" | +| Inhoud laten meebewegen bij 400% zoom | Breedtes vastzetten in `px` en hopen | +| Een native `dialog`/`details` of een Curve-component gebruiken | Een modaal venster namaken met `div`s en `z-index` | +| Tabvolgorde gelijk houden aan de visuele volgorde | Visueel herschikken met CSS en de DOM laten staan | +| Hover-inhoud ook bij focus tonen | Essentiële informatie achter `:hover` verstoppen | + +## De drie die het meest kosten + +**Niet-semantische elementen.** `
` is geen knop. Hij is niet focusbaar, reageert niet op + +Enter of Spatie, kondigt niets aan, en wordt niet gevonden door een screenreader die +naar knoppen zoekt. Elke poging om dat te repareren met `tabindex`, `role` en toetsafhandelaars komt neer +op het opnieuw bouwen van ` +``` + +De native variant is focusbaar, staat in de tabvolgorde, reageert op Enter en Spatie, +wordt aangekondigd als knop, werkt met spraakbesturing, respecteert de hoog-contrastmodus van het +besturingssysteem, en verschijnt wanneer een screenreadergebruiker om een lijst met knoppen vraagt. + +## De andere vier + +De vijf regels van het W3C zijn de moeite waard om helemaal te kennen, want ze dekken de fouten die +volgen zodra je hebt besloten dat ARIA onvermijdelijk is. + +**2. Verander de native semantiek niet, tenzij het echt moet.** Heb je een kop nodig die als tab werkt, +zet de tab dan in de kop in plaats van de rol van de kop te overschrijven. + +```html + +

Facturatie

+ + +

Facturatie

+``` + +**3. Alle bedienbare ARIA-elementen moeten met het toetsenbord werken.** Geef je iets `role="slider"`, dan +ben je pijltoetsen, Home en End verschuldigd. Een rol zonder het bijbehorende +toetsenbordcontract is een leugen tegen een screenreadergebruiker, die nu gedrag verwacht dat er niet is. + +**4. Zet nooit `role="presentation"` of `aria-hidden="true"` op een focusbaar element.** Dat levert een +element op dat een toetsenbordgebruiker wel kan bereiken en een screenreader niet kan beschrijven — de +focus landt op niets. + +```html + + + + + +``` + +**5. Elk bedienbaar element heeft een toegankelijke naam nodig.** Uit de eigen tekst, een `label`, +`aria-label` of `aria-labelledby`. Een element zonder naam wordt voorgelezen als zijn rol en verder niets. + +## Hoe dit in Curve uitpakt + +Vrijwel alle ARIA die je anders zou schrijven, is al geregeld. Base UI (React) en de Brain-laag van +Spartan (Angular) implementeren het WAI-ARIA-patroon per component: de rollen, de statussen, het +toetsenbordcontract en het focusbeheer. + +Twee praktische gevolgen: + +- Betrap je jezelf erop dat je `role`, `aria-expanded` of `aria-selected` aan een Curve-component + toevoegt, stop dan — of het component zet het al (en je zit er nu tegenin te werken), of je gebruikt het + verkeerde component. +- Bouw je iets dat Curve niet heeft, gebruik dan het patroon uit de + [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/) in plaats van zelf een set + attributen te verzinnen. De APG vermeldt ook het toetsenbordcontract waar je je aan verbindt. + +Wat er legitiem overblijft — benoemen, beschrijven, live regions en status op werkelijk eigen componenten — +staat in +[ARIA correct gebruiken](?path=/docs/toegankelijkheid-do-s-en-don-ts--aria-correct-gebruiken). diff --git a/packages/storybook-config/docs/accessibility/09-dos-en-donts-aria-gebruiken.mdx b/packages/storybook-config/docs/accessibility/09-dos-en-donts-aria-gebruiken.mdx new file mode 100644 index 00000000..c6b90c08 --- /dev/null +++ b/packages/storybook-config/docs/accessibility/09-dos-en-donts-aria-gebruiken.mdx @@ -0,0 +1,120 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# ARIA correct gebruiken + +Als je native HTML echt hebt uitgeput, is ARIA het juiste gereedschap. Er zijn vier taken die het goed +doet: dingen benoemen, dingen beschrijven, status doorgeven en verandering aankondigen. Alles hieronder +valt onder een van die vier. + +## Benoemen + +Een toegankelijke naam is wat een screenreader voorleest bij een element. HTML biedt meerdere manieren om +die te zetten, en ze verdringen elkaar in een vaste volgorde: + +**`aria-labelledby` → `aria-label` → de eigen inhoud → `title`** + +`aria-labelledby` wint van alles, ook van zichtbare tekst — en dat is de meest voorkomende oorzaak van +"er staat Opslaan maar hij zegt Annuleren". Gebruik bij voorkeur zichtbare tekst die je niet hoefde te +verdubbelen: + +```html + + + + + + + +
+

Totalen

+
+``` + +Twee regels die veel zoekwerk schelen: `aria-label` wordt genegeerd op elementen zonder rol — op een `div` +of `span` doet het niets — en de tekst van een zichtbaar label moet in de toegankelijke naam voorkomen, +anders kunnen gebruikers van spraakbesturing niet uitspreken wat ze zien. + +Bij formuliervelden wint een echt `