|
| 1 | +# Accessibility (a11y) |
| 2 | + |
| 3 | +## Standards |
| 4 | + |
| 5 | +WCAG 2.1 AA compliance target. |
| 6 | + |
| 7 | +## What's implemented |
| 8 | + |
| 9 | +### Landmarks & structure |
| 10 | +- `<header>`, `<main id="main-content">`, `<section>` with `aria-labelledby` throughout `App.jsx` |
| 11 | +- `<h1>` → `<h2>` heading hierarchy (screen-reader-only `<h2>` where visual heading isn't needed) |
| 12 | + |
| 13 | +### Skip navigation |
| 14 | +- "Skip to main content" link at the top of the page — visible on focus, hidden otherwise (`.skip-link` in `index.css`) |
| 15 | + |
| 16 | +### ARIA |
| 17 | +- All interactive elements have `aria-label` or associated `<label>` |
| 18 | +- Form inputs use `aria-invalid` + `aria-describedby` pointing to inline error messages |
| 19 | +- Buttons use `aria-busy` during async operations |
| 20 | +- Disclosure buttons use `aria-expanded` + `aria-controls` |
| 21 | +- Modals use `role="dialog"`, `aria-modal="true"`, `aria-labelledby` |
| 22 | +- Decorative icons marked `aria-hidden="true"` |
| 23 | +- Status messages use `role="alert"` / `aria-live="polite"` / `aria-live="assertive"` as appropriate |
| 24 | +- Global loading state announced via a hidden `aria-live="polite"` region |
| 25 | + |
| 26 | +### Focus management |
| 27 | +- `useFocusTrap` hook (`src/hooks/useFocusTrap.js`) traps focus inside modals (QRCodeModal, TxModal) |
| 28 | +- Focus returns to the triggering element when a modal closes |
| 29 | +- `Escape` closes all modals |
| 30 | + |
| 31 | +### Keyboard navigation |
| 32 | +- All interactive elements reachable by `Tab` |
| 33 | +- Transaction rows are `role="button"` with `tabIndex={0}` and respond to `Enter`/`Space` |
| 34 | +- Global shortcuts: `Ctrl+N` (create account), `Escape` (close modals), `?` (shortcuts help) |
| 35 | + |
| 36 | +### Screen reader support |
| 37 | +- `.sr-only` utility class for visually hidden but announced text |
| 38 | +- `<time dateTime="…">` for message timestamps |
| 39 | +- `role="list"` / `role="listitem"` on balance and transaction lists |
| 40 | +- `role="log"` on message history panel |
| 41 | +- `role="status"` on PWA banners and WebSocket indicator |
| 42 | + |
| 43 | +### Color contrast |
| 44 | +- CSS custom properties (`--text`, `--primary`, `--danger`, `--success`) maintain ≥ 4.5:1 contrast ratio in both light and dark themes |
| 45 | +- Focus ring: `outline: 3px solid var(--primary)` via `:focus-visible` |
| 46 | + |
| 47 | +## Testing |
| 48 | + |
| 49 | +### Manual |
| 50 | +1. Tab through the entire app — every interactive element must be reachable and have a visible focus ring |
| 51 | +2. Activate all buttons/links with `Enter` and `Space` |
| 52 | +3. Open QR modal and transaction detail modal — confirm focus is trapped and returns on close |
| 53 | +4. Test with a screen reader (NVDA/JAWS on Windows, VoiceOver on macOS/iOS) |
| 54 | + |
| 55 | +### Automated |
| 56 | +The existing `frontend/tests/accessibility.test.jsx` uses `@testing-library/jest-dom` assertions. |
| 57 | +Run with: |
| 58 | +```bash |
| 59 | +cd frontend && npm test |
| 60 | +``` |
| 61 | + |
| 62 | +To add axe-core automated checks, install `@axe-core/react` and add to `src/setupTests.js`: |
| 63 | +```js |
| 64 | +import { configureAxe } from '@axe-core/react'; |
| 65 | +configureAxe(React, ReactDOM); |
| 66 | +``` |
0 commit comments