Frontend a11y: focus-visible gaps on sort headers, breadcrumb links, combobox, page-size select; search result count unannounced #69

Closed
opened 2026-06-09 21:26:36 +00:00 by logaritmisk · 1 comment
Owner

Follow-up to #52 — that pass covered custom controls and route focus, but a sweep found a handful of interactive elements that still have no visible keyboard focus state, plus one unannounced async update. The focusRing helper (src/lib/focus-ring.ts) exists and is the established fix.

Missing focus-visible styling

  • web/src/objects/objects-table.tsx:131-134 — column-header sort buttons: className="flex items-center gap-1 hover:text-foreground". Hover-only feedback; keyboard focus is invisible (the file already imports focusRing and uses it on the row link). Add ${focusRing} rounded-sm.
  • web/src/shell/breadcrumb.tsx:19 — breadcrumb <Link>s: "truncate text-muted-foreground hover:text-foreground" — no focus ring. Add ${focusRing} rounded-sm.
  • web/src/components/external-uri-link.tsx — the external <a> has hover feedback only. Add ${focusRing} rounded-sm.
  • web/src/components/ui/combobox.tsx:23, 29-49ComboboxInput ("w-full rounded border px-2 py-1 pr-12 text-sm"), ComboboxClear, and ComboboxTrigger have no focus-visible: classes, unlike ui/input.tsx / ui/select.tsx which carry the standard focus-visible:ring-3 focus-visible:ring-ring/50.
  • web/src/objects/objects-table.tsx:286-291 — the native page-size <select> has no focus styling at all (default UA outline only, and inconsistent with everything around it). Fixing #68 by swapping to the ui Select resolves this too.

Unannounced async update

  • web/src/search/search-panel.tsx:105-107 — the result count <p>{t("search.resultCount", { count: total })}</p> updates as the user types but is not a live region; screen-reader users get no feedback that results arrived. Add role="status" (implicit aria-live="polite"), matching the pattern already used by the skeletons.

Acceptance

  • Tab through objects table header, breadcrumbs, a combobox picker, and the table footer: every stop shows the standard ring.
  • With VoiceOver/NVDA, typing in search announces the updated result count.
Follow-up to #52 — that pass covered custom controls and route focus, but a sweep found a handful of interactive elements that still have **no visible keyboard focus state**, plus one unannounced async update. The `focusRing` helper (`src/lib/focus-ring.ts`) exists and is the established fix. ## Missing focus-visible styling - **`web/src/objects/objects-table.tsx:131-134`** — column-header sort buttons: `className="flex items-center gap-1 hover:text-foreground"`. Hover-only feedback; keyboard focus is invisible (the file already imports `focusRing` and uses it on the row link). Add `${focusRing} rounded-sm`. - **`web/src/shell/breadcrumb.tsx:19`** — breadcrumb `<Link>`s: `"truncate text-muted-foreground hover:text-foreground"` — no focus ring. Add `${focusRing} rounded-sm`. - **`web/src/components/external-uri-link.tsx`** — the external `<a>` has hover feedback only. Add `${focusRing} rounded-sm`. - **`web/src/components/ui/combobox.tsx:23, 29-49`** — `ComboboxInput` (`"w-full rounded border px-2 py-1 pr-12 text-sm"`), `ComboboxClear`, and `ComboboxTrigger` have no `focus-visible:` classes, unlike `ui/input.tsx` / `ui/select.tsx` which carry the standard `focus-visible:ring-3 focus-visible:ring-ring/50`. - **`web/src/objects/objects-table.tsx:286-291`** — the native page-size `<select>` has no focus styling at all (default UA outline only, and inconsistent with everything around it). Fixing #68 by swapping to the ui `Select` resolves this too. ## Unannounced async update - **`web/src/search/search-panel.tsx:105-107`** — the result count `<p>{t("search.resultCount", { count: total })}</p>` updates as the user types but is not a live region; screen-reader users get no feedback that results arrived. Add `role="status"` (implicit `aria-live="polite"`), matching the pattern already used by the skeletons. ## Acceptance - Tab through objects table header, breadcrumbs, a combobox picker, and the table footer: every stop shows the standard ring. - With VoiceOver/NVDA, typing in search announces the updated result count.
Author
Owner

Fixed in ec11c9d (merged as 091a1a6).

  • Sort headers + page-size select (objects-table.tsx), breadcrumb links, and ExternalUriLink now use the shared focusRing helper (+ rounded-sm so the ring hugs the text).
  • ui/combobox.tsx input/clear/trigger got the kit's inline focus-visible:ring-3 focus-visible:ring-ring/50 classes (input also focus-visible:border-ring, matching ui/input.tsx).
  • Search result count is now role="status", so count updates are announced while typing; the existing search test asserts the count through getByRole("status").

Note: the page-size select kept its native element (got a focus ring rather than a swap to the ui Select) — if it should be swapped for visual consistency, that's #73-adjacent follow-up territory.

Fixed in ec11c9d (merged as 091a1a6). - Sort headers + page-size select (`objects-table.tsx`), breadcrumb links, and `ExternalUriLink` now use the shared `focusRing` helper (+ `rounded-sm` so the ring hugs the text). - `ui/combobox.tsx` input/clear/trigger got the kit's inline `focus-visible:ring-3 focus-visible:ring-ring/50` classes (input also `focus-visible:border-ring`, matching `ui/input.tsx`). - Search result count is now `role="status"`, so count updates are announced while typing; the existing search test asserts the count through `getByRole("status")`. Note: the page-size select kept its native element (got a focus ring rather than a swap to the ui `Select`) — if it should be swapped for visual consistency, that's #73-adjacent follow-up territory.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logaritmisk/biggus-dickus#69