Skip to content

Epic: A11y app-wide — WCAG 2.2 AA: keyboard, screen reader, contrast (Fable-5 audit 2026-07-05) #91

Description

@Santisoutoo

Epic tracking the fixes from an app-wide accessibility audit (WCAG 2.2 AA) (Fable-5, 2026-07-05, static). Full findings (file:line + WCAG SC) in memory/fable_a11y_audit_2026-07-05.md.

Verdict

LexFlow is partially operable by keyboard and only marginally usable with a screen reader. The shell fundamentals are genuinely good (skip link, a global :focus-visible ring, a real useFocusTrap used by CommandPalette/ConfirmDialog (VforVitorio#714), an exemplary SpainMap, reduced-motion respected). But the two headline surfaces fail at the core: the Explorer desktop browse table — the primary way into 12k laws — is clickable <tr>s with no tabIndex/role/key handler, so a keyboard user cannot open a single law from it; and the streaming chat reply has no live region anywhere, so a screen-reader user who sends a question is met with total silence. Around those, a pattern repeats: hand-rolled overlays that declare aria-modal but never trap/move focus (only 2 of ~10 use the focus-trap), toggle controls whose state is colour-only, status changes never announced, and a set of non-existent Tailwind classes that silently no-op (amber-200/400/600 + shadcn tokens in SplashGate) breaking dark-mode contrast and the splash progress bar.

Top 3 highest-impact fixes

  1. Make the primary data surfaces keyboard-real (S1) — law links inside Explorer rows (P0-1), Card renders a button when clickable (P1-13), keyboard path through tag autocomplete (P1-4). One small PR unlocks the core loop.
  2. A live-region layer (S3) — a permanently-mounted polite region + role="log" on the chat thread + announced result counts. Converts chat and every error toast from silent to usable.
  3. A shared dialog contract on the five remaining overlays (S2)useFocusTrap + Escape + initial focus is already written; adoption is mechanical and kills the largest focus-barrier cluster (incl. the first-run ModelWizard gate).

Sprints (findings P0-1 … P3)

Sprint 1 — Keyboard reachability (the P0 core):

  • S1.1 a11y(explorer): browse table rows keyboard-operable (law name as <a>, or role=link+Enter/Space) [M] [P0]<tr onClick> with no tabIndex/role/handler (ExplorerPage.tsx:571-576); search-mode rows already do it right (:444-449). WCAG 2.1.1. (P0-1)
  • S1.2 a11y(ui): Card renders a button/anchor when onClick/href is passed [S] — Home "Recent laws" are clickable <div>s (HomePage.tsx:180, ui/Card.tsx:8-21). WCAG 2.1.1. (P1-13)
  • S1.3 a11y(explorer): keyboard path for #tag autocomplete (Arrow/Enter, listbox+activedescendant) [S] — suggestions bind onMouseDown only + vanish on blur (ExplorerPage.tsx:305-325). WCAG 2.1.1/4.1.2. (P1-4)
  • S1.4 a11y(chat): thread drawer inert/aria-hidden when closed (mobile) [S] — off-screen via -translate-x-full but focusable → tabbing through dozens of invisible thread buttons (ChatPage.tsx:266-272). WCAG 2.4.3. (P1-2)
  • S1.5 a11y(shell): single-key "g x" shortcut disable/remap setting [S] — no off mechanism (lib/hotkeys.ts:49-70); WCAG 2.1.4 requires it. + Space on role=button search rows (ExplorerPage.tsx:449). (P2-5, P3)

Sprint 2 — Dialog & menu focus contract:

  • S2.1 a11y(overlays): trap + Escape + initial focus on the 5 remaining aria-modal overlays [M] [P1] — Explorer mobile filter sheet (ExplorerPage.tsx:239-287), RightRail sheet (RightRail.tsx:61-95), ModelWizard first-run gate (ModelWizard.tsx:166-203), SemanticExplainDialog (SettingsPage.tsx:645-689), MCP add-server dialog (McpServersSection.tsx:436-478). useFocusTrap already exists + restores focus. (a11y(app): AlertDialogs, accessible names & focus management VforVitorio/LexFlow#714 did CommandPalette/ConfirmDialog; editor 4 overlays → Epic: Editor drafting flow — safe persistence, precise insertion, visible annotations (Fable-5 audit 2026-07-05) VforVitorio/LexFlow#865 S4.4.) WCAG 2.4.3/2.1.2. (P1-1)
  • S2.2 a11y(help): HelpDrawer focus trap (focus-move + Escape already done in #714) [S] — Tab still exits into the page (HelpDrawer.tsx:179-181,219-229). (P3)
  • S2.3 a11y(menus): ModelChip + ExportMenu menu semantics + Toaster Escape scoping [S] — ModelChip no aria-haspopup/aria-expanded/Escape (ModelChip.tsx:21-52); Toaster global Escape clears toasts while a dialog consumes Escape (Toaster.tsx:40-46). (P2-14, P3)

Sprint 3 — Names, states & live regions (SR semantics):

  • S3.1 a11y(chat): role="log" aria-live on the thread + typing/completion announcements [M] [P0] — no live region anywhere (ChatPage.tsx:366-386, ChatMessage.tsx:49-77); announce whole messages on commit, not per-token. WCAG 4.1.3. (P0-2)
  • S3.2 a11y(toaster): mount the aria-live container permanently (only children come/go) [S] [P1]if (toasts.length===0) return null before the live region → first toast of a burst not announced; toasts are the app's primary error channel (Toaster.tsx:48-54). WCAG 4.1.3. (P1-6)
  • S3.3 a11y(palette): combobox wiring (label input, aria-activedescendant, live count) [S] — no accessible name/role/activedescendant; arrowing announces nothing (CommandPalette.tsx:175-207). WCAG 4.1.2. (P1-5)
  • S3.4 a11y(forms): name every control + aria-invalid in Input [M] [P1] — unnamed icon buttons (SettingsPage.tsx:327, LawDetailPage.tsx:309-311); placeholder-only/unlabeled inputs/select/slider/textarea (P1-10 list); Input.error is colour-only, no aria-invalid/aria-describedby (ui/Input.tsx:11-17). WCAG 3.3.1/3.3.2/4.1.2. (P1-3, P1-10, P1-11)
  • S3.5 a11y(primitives): aria-pressed/aria-current/radiogroup/fieldset sweep + result-count live regions + <html lang> sync [M] — segmented Tabs (P2-1), Chip active (P2-3), disabled Switch/Checkbox/Radio + radiogroup (P2-4), toggles (P2-6), FilterGroup legend (P2-12), nav aria-current (P2-19), result counts (P2-7), documentElement.lang on language change (P2-8). WCAG 1.3.1/4.1.2/4.1.3/3.1.1. (P2 cluster)
  • S3.6 a11y(reading): law heading levels + citation sup names (file AC into #793) [S] — LawMarkdown flattens h1-h6 to <strong> (LawMarkdown.tsx:24-36), gutter "Art. N" visual-only (ArticleBlock.tsx:26-31); citation sups named only "1"/"2" (ArticleBlock.tsx:69-92). Coordinate [epic] Law reading experience (Fable 5 audit) VforVitorio/LexFlow#793 (hierarchy rendering). WCAG 1.3.1/2.4.6. (P1-12, P2-16)

Sprint 4 — Contrast, tokens & non-text alternatives:

  • S4.1 a11y(tokens): fix the non-existent Tailwind utilities that silently no-op + rewrite SplashGate tokens [M] [P2]tailwind.config.ts:50-54 replaces the amber scale with only 300/500/700 → amber-200/400/600 no-op (call sites: CitationCard.tsx:22, ArticleBlock.tsx:28, DiffPage.tsx:69,98, LawHeader.tsx:136, CommandPalette.tsx:212, SettingsPage.tsx:1071,1083, CommentsPanel.tsx:133, ExplorerPage.tsx:665, EditorPage.tsx:324); SplashGate uses foreign shadcn tokens (bg-background/text-foreground/bg-primarySplashGate.tsx:65-116) → progress-bar fill invisible + retry button unstyled. Verify with a build. WCAG 1.4.3/1.4.11. (P2-10)
  • S4.2 a11y(contrast): amber-500/white chips, Badge amber, tiny muted text, dark focus ring [S] — white-on-amber-500 ≈2.0:1 (ExplorerPage.tsx:389-399, FilterRail.tsx:350-351); Badge amber ≈3.5-3.8:1 (ui/Badge.tsx:8); focus ring ≈2.6:1 on dark bg (index.css:105-109). WCAG 1.4.3/1.4.11/2.4.7. (P1-7, P2-9, P2-11)
  • S4.3 a11y(dataviz): Recharts accessibilityLayer/table fallback + graph canvas name + "ver como lista" [M] — Dashboards chart values hover-only, no text equivalent (DashboardPage.tsx:166-213); graph <canvas> nameless, nodes unreachable (GraphCanvas.tsx:247-328, GraphPage.tsx:172). Coordinate [epic] Graph VISUALIZATION — design, performance & UX (Fable 5 audit, 2026-07-03) VforVitorio/LexFlow#782 (viewer) + Epic: Trust & uncertainty communication (Fable-5 audit 2026-07-05) VforVitorio/LexFlow#847 (inferido). WCAG 1.1.1/2.1.1. (P1-8, P1-9)
  • S4.4 a11y(misc): VersionTimeline colour-only state + non-interactive when no onSelect [S] — current=ring only, kind=dot colour only, dead focusable dots on LawDetail (VersionTimeline.tsx:30-43, LawDetailPage.tsx:112). WCAG 1.4.1/4.1.2. (P2-17)

Cross-references (not re-filed): CommandPalette trap/ConfirmDialog/focus-visible/editor input aria/HelpDrawer+TopBar aria → VforVitorio#714; editor overlay dialog-focus → VforVitorio#865 S4.4; EN/ES + label wording → VforVitorio#852; help-content lies + dead controls → VforVitorio#832 (S1.5/misc reference two more instances); law hierarchy rendering → VforVitorio#793 (S3.6 files a11y AC there); graph viewer/inferido → VforVitorio#782/VforVitorio#847 (S4.3).


Upstream: VforVitorio#889

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: frontendReflex pages and componentsepicAgrupa un conjunto de tareas relacionadas

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions