From 546680017dcb071b3bb34eb0762d4d819850ff13 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Mon, 8 Jun 2026 18:52:21 +0200 Subject: [PATCH] =?UTF-8?q?docs(plans):=20a11y=20defect=20bundle=20?= =?UTF-8?q?=E2=80=94=202-task=20plan=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-06-08-a11y-defect-bundle.md | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-08-a11y-defect-bundle.md diff --git a/docs/superpowers/plans/2026-06-08-a11y-defect-bundle.md b/docs/superpowers/plans/2026-06-08-a11y-defect-bundle.md new file mode 100644 index 0000000..a677ae5 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-a11y-defect-bundle.md @@ -0,0 +1,256 @@ +# Accessibility Defect Bundle — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix five remaining a11y defects — label-id collision, unnamed drawer/breadcrumb, untranslated combobox strings (Task 1); invalid table-row semantics, missing pill focus ring, unannounced table load/error states (Task 2). + +**Architecture:** Task 1 is a labelling/i18n cluster across four small components plus 5 new i18n keys. Task 2 reworks the objects-table data rows to use a real `` with `aria-current`, restores `focusRing` on the filter pills, and adds `aria-busy` + a live `` + `role="alert"` for load/error announcement. + +**Tech Stack:** React 19 + TS + pnpm, React Router 7, Base UI, react-i18next, Vitest 4 (jsdom) + RTL + MSW. + +**Conventions:** pnpm; **no `any`/`eslint-disable`/`@ts-ignore`**; no codename; en/sv parity; app source double-quote+semicolon; `components/ui/*` untouched; token classes only (`focusRing` is token-based). Run a single test pass. + +**Spec:** `docs/superpowers/specs/2026-06-08-a11y-defect-bundle-design.md` + +**Key facts:** +- Existing i18n keys: `common.noMatches` ("No matches"), `common.loading` ("Loading"), `nav.objects`, `objects.loadError` ("Could not load objects"), `actions.closeDetail`. NEW keys to add (en/sv): `common.clear`, `common.open`, `nav.breadcrumb`, `objects.detailTitle`, `objects.tableLabel`. +- `lib/focus-ring.ts` exports `focusRing` (a class string). Imported elsewhere as `import { focusRing } from "../lib/focus-ring";`. +- `components/label-editor.test.tsx`, `objects/options-combobox.test.tsx`, `shell/breadcrumb.test.tsx` exist. `objects/object-detail-drawer.test.tsx` does NOT. +- `objects/objects-table.test.tsx`: imports `renderApp`, `objectsPage` (from `../test/fixtures`), `ObjectsTable`, `ObjectDetail`, `i18n`, `Routes`/`Route`. Its `tree()` mounts `ObjectsTable` at `/objects` and `ObjectDetail` at `/objects/:id` as siblings. Fixtures: `objectsPage.items[0]` = `{ object_number: "LM-0042", object_name: "Amphora", … }`, `[1]` = `"LM-0043"`/`"Bronze fibula"`. The "clicking a row deep-links…" test clicks the **name** ("Amphora"), which stays plain text — it survives unchanged. +- `combobox.tsx` wrapper: `ComboboxClear`/`ComboboxTrigger` pass `aria-label` through to Base UI; `ComboboxEmpty` renders children. Do NOT modify `components/ui/combobox.tsx`. +- `object-detail-drawer.tsx`: `DrawerContent` spreads `...props` onto the Base UI `Drawer.Popup`, so `aria-label` passes through. + +--- + +# Task 1: Labelling + i18n cluster (label-editor, combobox, breadcrumb, drawer) + +**Files:** Modify `web/src/i18n/en.json`, `web/src/i18n/sv.json`, `web/src/components/label-editor.tsx`, `web/src/objects/options-combobox.tsx`, `web/src/shell/breadcrumb.tsx`, `web/src/objects/object-detail-drawer.tsx`; tests `web/src/components/label-editor.test.tsx`, `web/src/objects/options-combobox.test.tsx`, `web/src/shell/breadcrumb.test.tsx`. + +- [ ] **Step 1: Add the 5 i18n keys (both locales, parity).** In `web/src/i18n/en.json`, add to the relevant blocks: under `common` → `"clear": "Clear", "open": "Open"`; under `nav` → `"breadcrumb": "Breadcrumb"`; under `objects` → `"detailTitle": "Object detail", "tableLabel": "Objects"`. In `web/src/i18n/sv.json`, the same keys: `common.clear` = `"Rensa"`, `common.open` = `"Öppna"`, `nav.breadcrumb` = `"Brödsmulor"`, `objects.detailTitle` = `"Objektdetalj"`, `objects.tableLabel` = `"Objekt"`. (Valid JSON; mind commas. Place each new key beside its existing siblings in the same nested object.) + +- [ ] **Step 2: `label-editor.tsx` — `useId()`.** Add `useId` to the React import (`import { useId } from "react";`). Inside the component, add `const inputId = useId();` and change the two lines to: +```tsx + + set(e.target.value)} /> +``` + +- [ ] **Step 3: `options-combobox.tsx` — translate.** Add `import { useTranslation } from "react-i18next";` and `const { t } = useTranslation();` at the top of the component body. Change: +```tsx + + +``` +and +```tsx + {t("common.noMatches")} +``` + +- [ ] **Step 4: `breadcrumb.tsx` — translate the nav label.** Add `import { useTranslation } from "react-i18next";` and `const { t } = useTranslation();` inside `Breadcrumb` (before the `if (trail.length === 0)` guard). Change `