Skip to content

feat(react): add the Radio primitive#10

Open
ivanbanov wants to merge 2 commits into
mainfrom
feat/radio
Open

feat(react): add the Radio primitive#10
ivanbanov wants to merge 2 commits into
mainfrom
feat/radio

Conversation

@ivanbanov

Copy link
Copy Markdown
Member

Summary

Adds the Radio primitive as two packages:

  • @dunky.dev/radio — the framework-free core: a radio-group state machine built on @dunky.dev/state-machine, following the WAI-ARIA APG Radio Group pattern. Owns value state (controlled and uncontrolled), item registration, focus/roving-tabindex decisions, and disabled gating.
  • @dunky.dev/react-radio — the thin React binding exposing the Radio compound component; parts carry the ARIA wiring and data-state / data-disabled attributes, styling stays with the consumer.

Anatomy

<Radio>                    - root; owns the checked value, renders nothing of its own
  |_ <Group>               - the radio group surface; carries the group semantics
     |_ <Item>             - one option; checked or unchecked, one item tabbable at a time
     |  |_ <ItemIndicator> - the visual checked mark; exists only while its item is checked
     |_ <ItemLabel>        - names its item; pressing it selects the item

Behavior

  • Controlled (value + onValueChange) and uncontrolled (defaultValue) checked value; null means no selection.
  • Roving tabindex: exactly one item tabbable at a time; arrow keys walk enabled items from the focused one, wrapping at both ends and skipping disabled items, and selection follows focus.
  • Space checks a focused unchecked item (the tab-in case); Enter deliberately does nothing, per the APG, so implicit form submission isn't triggered.
  • role="radiogroup" / role="radio" with aria-checked, aria-orientation from the orientation hint, and per-item label association via ItemLabel (wired by id, never leaving a dangling reference).
  • Disabled gating at group and item level: disabled items carry aria-disabled and leave the tab order (not native disabled), staying perceivable to assistive tech.
  • ItemIndicator renders only while its item is checked — presence is the styling contract, on top of data-state="checked" / "unchecked".
  • Form integration (hidden inputs) is intentionally out of scope for v0.

Test plan

  • pnpm test:ci — 131 tests, 9 files, all passing
  • pnpm typecheck
  • pnpm lint
  • pnpm format:check
  • pnpm build

🤖 Generated with Claude Code

ivanbanov and others added 2 commits July 18, 2026 10:58
Adds @dunky.dev/radio (core state machine) and @dunky.dev/react-radio
(React binding). Anatomy: Radio root, Group, Item, ItemIndicator, and
ItemLabel. A WAI-ARIA APG radio group: controlled/uncontrolled value,
roving tabindex with wrapping arrow-key navigation where selection
follows focus, Space to check, and disabled gating at group and item
level — all decided in the core machine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivanbanov ivanbanov changed the title feat: add the Radio primitive feat(react): add the Radio primitive Jul 18, 2026
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.

1 participant