Skip to content

refactor(status): one roving radiogroup primitive for all chip selectors#1482

Merged
kylebernhardy merged 3 commits into
stagefrom
kyle/selector-a11y
Jul 13, 2026
Merged

refactor(status): one roving radiogroup primitive for all chip selectors#1482
kylebernhardy merged 3 commits into
stagefrom
kyle/selector-a11y

Conversation

@kylebernhardy

Copy link
Copy Markdown
Member

Fixes #1449 — Consolidate chip/radiogroup selectors into one roving-tabindex primitive.

What changed

One shared primitive, four call sites

The roving-radiogroup keyboard pattern was implemented four different ways under src/features/instance/status/analytics/. This PR extracts hooks/useRovingRadioGroup.ts — the WAI-ARIA APG Radio Group pattern in one place (single tab stop on the selected radio with first-radio fallback, arrow keys move focus and selection with wrap-around, Enter/Space select) — and uses it in:

  • primitives/DimensionChipRow.tsx (was copy Project easy studio #1)
  • charts/TableSizeChipRow.tsx (was copy Stage #2 — also picks up a missing type="button" on its chips)
  • StackByToggle in primitives/TrafficByTypeRenderer.tsx (was copy Stage #3 — arrow keys now move DOM focus along with selection, not just selection)
  • the quantile picker in primitives/DimensionSelectorRenderer.tsxbehavior fix: it previously rendered role="radio" with no roving tabindex and no arrow-key handling (every radio was a tab stop, violating the APG pattern). It gains both for free from the hook.

Duplicate solo/Ctrl-toggle hook folded

useTypeFilter (in TypeFilterChipRow.tsx) was a rename-only copy of hooks/useNodeSelection.ts. Both now delegate to one generic hooks/useSoloToggleSelection.ts; useNodeSelection stays as a thin named wrapper so its many call sites (including the pipeline renderers) are untouched.

Two a11y fixes from the issue

  • DimensionCombobox.tsx — the trigger's bare aria-label ("Path"/"Dimension") masked the visible selected value, so screen-reader users never heard the current selection. The accessible name now composes both: "Path: /orders".
  • "Other" chip — in DimensionChipRow it was a role-less <span aria-disabled> (meaningless to AT). It is now a perceivable disabled radio in both chip rows: role="radio" aria-checked="false" aria-disabled="true", kept out of the roving order (tabIndex={-1}) so each group remains a single tab stop.

Tests

  • Updated: dimension-chip-row.test.tsx (Other chip's improved semantics + single-tab-stop invariant), dimension-combobox.test.tsx and dimension-selector-switch.test.tsx (composed accessible names).
  • Added: quantile-picker arrow-key navigation and single-tab-stop tests; new table-size-chip-row.test.tsx and stack-by-toggle.test.tsx covering the shared hook's contract at those call sites (single tab stop, wrap-around, Other-chip skip, null-selection fallback).
  • tsc -b, full vitest run (202 files / 1342 passed, 11 pre-existing skips), oxlint, dprint all clean.

Cross-model review

Gemini 3.1 Pro reviewed the diff for APG radiogroup conformance: no blockers/majors/minors. Its one concrete nit (missing accessible name on the StackBy radiogroup) is a false positive — aria-label="Stack by" was already present outside the diff. Codex review reported no correctness issues, though it repeatedly drifted to a sibling worktree's diff in this multi-worktree checkout, so treat that signal as weak.

Open concerns / future work

  • Combobox popover is still not portaled (position: absolute), so an overflow: hidden ancestor can clip it. Swapping the bespoke listbox for the app's Radix Popover/Command was not a clean drop-in (the current implementation is an aria-activedescendant filter-listbox, not a menu), so it's deferred per the issue's "consider" framing.
  • Arrow-key direction is LTR-only (ArrowRight = next); would need inverting if the app ever supports RTL.
  • The disabled "Other" radio is deliberately not arrow-reachable — it can never become enabled, and skipping a permanently-disabled aggregate keeps selection-follows-focus simple. It remains perceivable to AT via its role and aria-disabled.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV

Generated by kAIle (Claude Fable 5).

@github-actions

github-actions Bot commented Jul 13, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 48.64% 4588 / 9432
🔵 Statements 49.07% 4891 / 9967
🔵 Functions 40.36% 1091 / 2703
🔵 Branches 41.07% 2977 / 7248
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/status/analytics/charts/TableSizeChipRow.tsx 100% 100% 100% 100%
src/features/instance/status/analytics/hooks/useNodeSelection.ts 100% 100% 100% 100%
src/features/instance/status/analytics/hooks/useRovingRadioGroup.ts 91.66% 81.81% 100% 95.65% 40, 44
src/features/instance/status/analytics/hooks/useSoloToggleSelection.ts 28% 5.88% 40% 35% 22-38
src/features/instance/status/analytics/pipeline/replication-latency.tsx 70% 41.52% 58.33% 70.99% 44, 90-91, 198, 201, 246-284, 309-311, 315-432, 446, 453-457, 485-508
src/features/instance/status/analytics/primitives/DimensionChipRow.tsx 100% 90.9% 100% 100%
src/features/instance/status/analytics/primitives/DimensionCombobox.tsx 80% 73.33% 82.35% 83.6% 49-53, 92-94, 172-173
src/features/instance/status/analytics/primitives/DimensionSelectorRenderer.tsx 95.12% 84.78% 100% 100% 77, 98
src/features/instance/status/analytics/primitives/TrafficByTypeRenderer.tsx 96.15% 90.69% 100% 100% 85, 86
src/features/instance/status/analytics/primitives/TypeFilterChipRow.tsx 85.71% 55.55% 66.66% 83.33% 45
Generated in workflow #1460 for commit 76f0491 by the Vitest Coverage Report Action

gemini-code-assist[bot]

This comment was marked as resolved.

@kylebernhardy
kylebernhardy marked this pull request as ready for review July 13, 2026 14:28
@kylebernhardy
kylebernhardy requested a review from a team as a code owner July 13, 2026 14:28

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice consolidation — thanks for tackling this, @kylebernhardy. I traced the new useRovingRadioGroup (useRovingRadioGroup.ts:33) line-by-line against all four migrated call sites and it's faithfully behavior-preserving: roving tabindex (single tab stop = selected radio, first-radio fallback), arrow-key wrap moving focus + selection, and Enter/Space select all match the originals. Two sites even come out genuinely better — StackBy now moves DOM focus (not just selection), and the DimensionSelector quantile picker gains roving + arrow keys where it previously had onClick-only. I also confirmed the #1474 heatmap focus-clamp lives in a separate grid pattern (HeatmapMatrix.tsx) that doesn't touch this hook, so no regression there. The solo-toggle fold-in (useSoloToggleSelection.ts:33) and the two a11y fixes (DimensionCombobox.tsx:113 composed name, DimensionChipRow.tsx:71 "Other" chip) all check out too.

One thing worth raising (non-blocking): there's a fifth quantile radiogroup that didn't get migrated — QuantileSelector in replication-latency.tsx:483. It renders role="radiogroup"/role="radio" buttons with only onClick — no roving tabindex, no arrow keys — i.e. the exact APG anti-pattern this PR fixes everywhere else. It's pre-existing, not a regression, so nothing blocking here. But since the goal is "one roving radiogroup primitive for all chip selectors," it stands out as the last hold-out, and it looks like a near drop-in for the new hook (useRovingRadioGroup(fields, value, onChange) + spread getRadioProps(idx)). Worth folding this one in too while you're here, or is a follow-up ticket the better call?

— Claude (Sonnet 5), on behalf of Kris, via review-queue

@kylebernhardy

Copy link
Copy Markdown
Member Author

Good catch on the fifth radiogroup — folded in rather than ticketed, since the only reason replication-latency.tsx was skipped is that the parallel series-keys PR (#1484) owned that file during the batch; with it merged, the constraint is gone. QuantileSelector now uses useRovingRadioGroup via getRadioProps (b310a72) — roving tabindex + arrow keys + Enter/Space, same as the other four sites, with the visual classes and test ids unchanged. All 412 status-feature tests pass.

Comment generated by kAIle (Claude Fable 5)

@dawsontoth

Copy link
Copy Markdown
Contributor

needs a rebase

kylebernhardy and others added 3 commits July 13, 2026 10:19
Extract useRovingRadioGroup (WAI-ARIA APG Radio Group pattern: single tab
stop, arrow keys move focus + selection with wrap, Enter/Space select) and
use it in DimensionChipRow, TableSizeChipRow, StackByToggle, and the
quantile picker — which previously had role=radio with no roving tabindex
and no arrow-key handling and gains both.

Also:
- Fold useTypeFilter (rename-only copy of useNodeSelection) and
  useNodeSelection into one shared useSoloToggleSelection hook.
- DimensionCombobox trigger composes the current selection into its
  accessible name instead of masking it with a bare aria-label.
- The non-selectable "Other" chip is now a perceivable disabled radio
  (role=radio aria-checked=false aria-disabled=true, out of the roving
  order) in both chip rows, instead of a role-less span.

Fixes #1449

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZEaYdKFXuQw2Hf6XpPdEV
…table

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ingRadioGroup

Review follow-up: the fifth radiogroup, skipped during the parallel batch
because replication-latency.tsx was owned by the series-keys PR — that PR
has merged, so the last onClick-only holdout gets the shared roving
tabindex + arrow-key behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kylebernhardy

Copy link
Copy Markdown
Member Author

Rebased onto current stage (69b4630) — the one conflict was the import block in DimensionSelectorRenderer.tsx where #1484's useNodeFilteredSeries refactor met this PR's quantile radio-group changes; resolved keeping both (the merged body uses useNodeFilteredSeries for node filtering and useRovingRadioGroup for the quantile picker). Full suite green: 202 files, 427 status-feature tests. Also dropped the empty CI-retrigger commit from the earlier event-delivery stall.

Comment generated by kAIle (Claude Fable 5)

@kylebernhardy
kylebernhardy added this pull request to the merge queue Jul 13, 2026
Merged via the queue into stage with commit 9b1a654 Jul 13, 2026
2 checks passed
@kylebernhardy
kylebernhardy deleted the kyle/selector-a11y branch July 13, 2026 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consolidate chip/radiogroup selectors into one roving-tabindex primitive

3 participants