Severity: High (mixed).From a frontend deep audit, 2026-06-08 (post #48). The #52 a11y pass is verified correct; these are the remaining gaps it didn't cover.
Problems
[High] Duplicate id="label" collides across simultaneous editors.LabelEditor hard-codes <Label htmlFor="label"> + <Input id="label"> (web/src/components/label-editor.tsx:35-36). It renders simultaneously in the create form and any inline-edit row — authorities (authorities-page.tsx:125 + authority-row.tsx:30) and vocab (vocabulary-terms.tsx + term-row.tsx:29). With two id="label" inputs in the DOM, every <label for="label"> resolves to the first match, so the second editor's label points at the wrong field. WCAG 1.3.1 / 4.1.1 / 4.1.2.
[Med] Invalid role="link" + aria-selected on table rows.objects-table.tsx:247-259 — aria-selected is not a supported state for role="link" (AT ignores/warns). The segmented filter pills in the same toolbar (objects-table.tsx:173) also dropped the focusRing present on the equivalent pills in search-panel.tsx/authorities-page.tsx — a keyboard-focus regression. WCAG 4.1.2 / 2.4.7.
[Med] Drawer dialog has no accessible name. The narrow-viewport object-detail drawer (objects/object-detail-drawer.tsx, components/ui/drawer.tsx:35) supplies no Drawer.Title/aria-label, so it announces as an anonymous modal. WCAG 4.1.2 / 2.4.6.
[Med] Table loading/error states aren't announced.objects-table.tsx:207-228 renders bare <Skeleton> rows (no aria-busy/role="status") and a plain <td> error (no role="alert"), unlike the ListSkeleton/FormSkeleton pattern in components/ui/skeletons.tsx. WCAG 4.1.3.
[Low] Only untranslated UI strings in the app.objects/options-combobox.tsx:47-52 — aria-label="Clear", aria-label="Open", <ComboboxEmpty>No matches.</ComboboxEmpty> bypass t() (render English for Swedish users). common.noMatches already exists and is unused. Also shell/breadcrumb.tsx:10aria-label="Breadcrumb" is hardcoded.
Suggested fixes
LabelEditor: accept an id/idPrefix prop or use useId(); pass unique ids (label-create, label-${record.id}).
Table rows: drop role="link"/aria-selected; make the object-number cell a real <Link> (gains middle-click/open-in-new-tab) and keep <tr> plain — OR move to a valid role="grid"/role="row" structure. Restore focusRing on the filter pills.
Table states: aria-busy + role="status" while loading, role="alert" on error.
Translate the combobox strings (t("common.noMatches"), add common.clear/common.open) and the breadcrumb label.
Source: frontend deep audit (a11y dimension), 2026-06-08.
**Severity: High (mixed).** _From a frontend deep audit, 2026-06-08 (post #48). The #52 a11y pass is verified correct; these are the remaining gaps it didn't cover._
## Problems
- **[High] Duplicate `id="label"` collides across simultaneous editors.** `LabelEditor` hard-codes `<Label htmlFor="label">` + `<Input id="label">` (`web/src/components/label-editor.tsx:35-36`). It renders simultaneously in the create form **and** any inline-edit row — authorities (`authorities-page.tsx:125` + `authority-row.tsx:30`) and vocab (`vocabulary-terms.tsx` + `term-row.tsx:29`). With two `id="label"` inputs in the DOM, every `<label for="label">` resolves to the first match, so the second editor's label points at the wrong field. WCAG 1.3.1 / 4.1.1 / 4.1.2.
- **[Med] Invalid `role="link"` + `aria-selected` on table rows.** `objects-table.tsx:247-259` — `aria-selected` is not a supported state for `role="link"` (AT ignores/warns). The segmented filter pills in the same toolbar (`objects-table.tsx:173`) also **dropped the `focusRing`** present on the equivalent pills in `search-panel.tsx`/`authorities-page.tsx` — a keyboard-focus regression. WCAG 4.1.2 / 2.4.7.
- **[Med] Drawer dialog has no accessible name.** The narrow-viewport object-detail drawer (`objects/object-detail-drawer.tsx`, `components/ui/drawer.tsx:35`) supplies no `Drawer.Title`/`aria-label`, so it announces as an anonymous modal. WCAG 4.1.2 / 2.4.6.
- **[Med] Table loading/error states aren't announced.** `objects-table.tsx:207-228` renders bare `<Skeleton>` rows (no `aria-busy`/`role="status"`) and a plain `<td>` error (no `role="alert"`), unlike the `ListSkeleton`/`FormSkeleton` pattern in `components/ui/skeletons.tsx`. WCAG 4.1.3.
- **[Low] Only untranslated UI strings in the app.** `objects/options-combobox.tsx:47-52` — `aria-label="Clear"`, `aria-label="Open"`, `<ComboboxEmpty>No matches.</ComboboxEmpty>` bypass `t()` (render English for Swedish users). `common.noMatches` already exists and is unused. Also `shell/breadcrumb.tsx:10` `aria-label="Breadcrumb"` is hardcoded.
## Suggested fixes
- `LabelEditor`: accept an `id`/`idPrefix` prop or use `useId()`; pass unique ids (`label-create`, `label-${record.id}`).
- Table rows: drop `role="link"`/`aria-selected`; make the object-number cell a real `<Link>` (gains middle-click/open-in-new-tab) and keep `<tr>` plain — OR move to a valid `role="grid"`/`role="row"` structure. Restore `focusRing` on the filter pills.
- Drawer: add `aria-label`/visually-hidden `Drawer.Title`.
- Table states: `aria-busy` + `role="status"` while loading, `role="alert"` on error.
- Translate the combobox strings (`t("common.noMatches")`, add `common.clear`/`common.open`) and the breadcrumb label.
_Source: frontend deep audit (a11y dimension), 2026-06-08._
Label-id collision — LabelEditor now uses React useId() (unique per instance), so the create form + inline-edit rows no longer share id="label".
Invalid table-row semantics — the objects-table data row is now a plain <tr> with the object-number cell as a real React Router <Link aria-current="page"> (selected row); dropped role="link"/aria-selected/tabIndex/onKeyDown. Whole-row click preserved; gains keyboard/SR "link" + middle-click/open-in-new-tab. Filter pills regained the focusRing.
Unannounced table states — <table aria-busy> + an sr-only polite live <caption> (also names the table); the load-error cell is role="alert".
Unnamed drawer — the object-detail drawer dialog gets aria-label={t("objects.detailTitle")}.
Untranslated strings — combobox Clear/Open/empty + the breadcrumb <nav> label now go through t().
5 new en/sv i18n keys (parity). 253 tests pass; typecheck/lint/build clean; check:size 216.3 KB gz; check:colors clean; no new dependency; no codename.
Out of scope → #66: the combobox wrapper's raw-palette internals + the segmented-control extraction.
Fixed in merge `285a132`. All five gaps closed:
- **Label-id collision** — `LabelEditor` now uses React `useId()` (unique per instance), so the create form + inline-edit rows no longer share `id="label"`.
- **Invalid table-row semantics** — the objects-table data row is now a plain `<tr>` with the object-number cell as a real React Router `<Link aria-current="page">` (selected row); dropped `role="link"`/`aria-selected`/`tabIndex`/`onKeyDown`. Whole-row click preserved; gains keyboard/SR "link" + middle-click/open-in-new-tab. Filter pills regained the `focusRing`.
- **Unannounced table states** — `<table aria-busy>` + an `sr-only` polite live `<caption>` (also names the table); the load-error cell is `role="alert"`.
- **Unnamed drawer** — the object-detail drawer dialog gets `aria-label={t("objects.detailTitle")}`.
- **Untranslated strings** — combobox Clear/Open/empty + the breadcrumb `<nav>` label now go through `t()`.
5 new en/sv i18n keys (parity). 253 tests pass; typecheck/lint/build clean; check:size 216.3 KB gz; check:colors clean; no new dependency; no codename.
**Out of scope → #66:** the combobox wrapper's raw-palette internals + the segmented-control extraction.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Severity: High (mixed). From a frontend deep audit, 2026-06-08 (post #48). The #52 a11y pass is verified correct; these are the remaining gaps it didn't cover.
Problems
id="label"collides across simultaneous editors.LabelEditorhard-codes<Label htmlFor="label">+<Input id="label">(web/src/components/label-editor.tsx:35-36). It renders simultaneously in the create form and any inline-edit row — authorities (authorities-page.tsx:125+authority-row.tsx:30) and vocab (vocabulary-terms.tsx+term-row.tsx:29). With twoid="label"inputs in the DOM, every<label for="label">resolves to the first match, so the second editor's label points at the wrong field. WCAG 1.3.1 / 4.1.1 / 4.1.2.role="link"+aria-selectedon table rows.objects-table.tsx:247-259—aria-selectedis not a supported state forrole="link"(AT ignores/warns). The segmented filter pills in the same toolbar (objects-table.tsx:173) also dropped thefocusRingpresent on the equivalent pills insearch-panel.tsx/authorities-page.tsx— a keyboard-focus regression. WCAG 4.1.2 / 2.4.7.objects/object-detail-drawer.tsx,components/ui/drawer.tsx:35) supplies noDrawer.Title/aria-label, so it announces as an anonymous modal. WCAG 4.1.2 / 2.4.6.objects-table.tsx:207-228renders bare<Skeleton>rows (noaria-busy/role="status") and a plain<td>error (norole="alert"), unlike theListSkeleton/FormSkeletonpattern incomponents/ui/skeletons.tsx. WCAG 4.1.3.objects/options-combobox.tsx:47-52—aria-label="Clear",aria-label="Open",<ComboboxEmpty>No matches.</ComboboxEmpty>bypasst()(render English for Swedish users).common.noMatchesalready exists and is unused. Alsoshell/breadcrumb.tsx:10aria-label="Breadcrumb"is hardcoded.Suggested fixes
LabelEditor: accept anid/idPrefixprop or useuseId(); pass unique ids (label-create,label-${record.id}).role="link"/aria-selected; make the object-number cell a real<Link>(gains middle-click/open-in-new-tab) and keep<tr>plain — OR move to a validrole="grid"/role="row"structure. RestorefocusRingon the filter pills.aria-label/visually-hiddenDrawer.Title.aria-busy+role="status"while loading,role="alert"on error.t("common.noMatches"), addcommon.clear/common.open) and the breadcrumb label.Source: frontend deep audit (a11y dimension), 2026-06-08.
Fixed in merge
285a132. All five gaps closed:LabelEditornow uses ReactuseId()(unique per instance), so the create form + inline-edit rows no longer shareid="label".<tr>with the object-number cell as a real React Router<Link aria-current="page">(selected row); droppedrole="link"/aria-selected/tabIndex/onKeyDown. Whole-row click preserved; gains keyboard/SR "link" + middle-click/open-in-new-tab. Filter pills regained thefocusRing.<table aria-busy>+ ansr-onlypolite live<caption>(also names the table); the load-error cell isrole="alert".aria-label={t("objects.detailTitle")}.<nav>label now go throught().5 new en/sv i18n keys (parity). 253 tests pass; typecheck/lint/build clean; check:size 216.3 KB gz; check:colors clean; no new dependency; no codename.
Out of scope → #66: the combobox wrapper's raw-palette internals + the segmented-control extraction.