Frontend a11y: label-id collision, invalid table row semantics, unnamed drawer, unannounced table states #62
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.