Skip to content

refactor(segmentedcontrol): use shared useListFocus instead of inline roving#3485

Draft
cixzhang wants to merge 1 commit into
mainfrom
navi/refactor/segmentedcontrol-use-listfocus
Draft

refactor(segmentedcontrol): use shared useListFocus instead of inline roving#3485
cixzhang wants to merge 1 commit into
mainfrom
navi/refactor/segmentedcontrol-use-listfocus

Conversation

@cixzhang

@cixzhang cixzhang commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Refactors SegmentedControl to delegate its roving-tabindex and arrow-key navigation to the shared useListFocus primitive (hasRovingTabIndex: true), removing ~60 lines of duplicated inline logic. No behavior change — all existing tests pass unmodified.

What changed

Before, SegmentedControl hand-rolled:

  • A ~60-line handleKeyDown with ArrowLeft/ArrowRight/Home/End, wrap, disabled-skip, and selection-follows-focus (calling onChange on every move).
  • A useIsomorphicLayoutEffect tab-stop repair that queried [role="radio"]:not([aria-disabled="true"]) and promoted the first enabled radio to tabIndex=0 when none was tabbable.

After, that is replaced by:

const {listRef, handleKeyDown, handleFocus} = useListFocus<HTMLDivElement>({
  itemSelector: '[role="radio"]:not([aria-disabled="true"])',
  hasRovingTabIndex: true,
  wrap: true,
  orientation: 'horizontal',
});

useListFocus now owns the single roving tab stop (stamps tabIndex 0/-1, skips disabled radios, wraps, handles Home/End, and repairs the stop on mount/disable).

Selection-follows-focus (APG radiogroup) is preserved with a slim container onFocus handler. useListFocus only moves focus; the handler reads the newly-focused radio's data-value and calls onChange — keeping selection in lockstep with focus without duplicating navigation logic. It ignores disabled radios and skips the already-selected value (so an initial Tab-in / click on the current segment is a no-op, matching click behavior).

SegmentedControlItem is unchanged: it keeps tabIndex={isSelected && !isItemDisabled ? 0 : -1} as the source-of-truth hint. useListFocus's repair preserves an existing 0 and only repairs when none is tabbable.

Net line count

SegmentedControl.tsx: +33 / −76 (net −43 lines).

Verification

  • pnpm -F @astryxdesign/core typecheck — exit 0
  • npx eslint on both changed files — clean (exit 0)
  • npx vitest run packages/core/src/SegmentedControl23/23 pass (arrow nav + select, wrap both directions, Home/End, disabled-skip, roving tabindex assertions, tab-stop repair for unmatched value, first-enabled promotion when first is disabled, disabled-group no tab stop, individually-disabled selected item no tab stop)

Changeset

@astryxdesign/core patch, [refactor].

@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 3, 2026 5:06am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

github-actions Bot added a commit that referenced this pull request Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants