Skip to content

fix(react-native): migrate FormField, Radio, RadioCard, ToggleButtonCard, and Checkbox off gluestack - #1503

Merged
jordmccord merged 5 commits into
hearth-react-native-v1from
jordmccord-uwds-4800
Aug 18, 2026
Merged

fix(react-native): migrate FormField, Radio, RadioCard, ToggleButtonCard, and Checkbox off gluestack#1503
jordmccord merged 5 commits into
hearth-react-native-v1from
jordmccord-uwds-4800

Conversation

@jordmccord

@jordmccord jordmccord commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Part of UWDS-4789 (removing @gluestack-ui/* from packages/react-native). This sub-issue (UWDS-4800) removes the coupled trio: @gluestack-ui/form-control, @gluestack-ui/radio, and @gluestack-ui/checkbox.

  • createRadio/createCheckbox provided single/multi-select group state (via @react-native-aria) and a hidden native <input> as their web selection source of truth. Both are now backed by a shared, plain-React useSingleSelection hook (single-select: Radio/RadioCard/ToggleButtonCard) or local array state (multi-select: Checkbox), with no hidden <input>.
  • createFormControl's useFormControlProvider id/aria-describedby linking was dead code — nothing downstream ever consumed the generated ids — so FormField drops it entirely in favour of its own existing context.
  • Internal group-context wiring passes the raw selectedValue/selectedValues through context instead of an isSelected(value) selector function, so items compare locally and no isX-named field appears anywhere in the (internal) API surface.

Fixes found and addressed during the migration (called out explicitly in the changeset):

  • FormFieldLabel previously rendered null regardless of its children, due to a gluestack factory misconfiguration — it now renders correctly. This was silently broken in both the FormField "Advanced Usage" docs and the Switch "Switch with Label" example.
  • Radio, RadioCard, and Checkbox now expose their checked state on web via aria-checked (in addition to React Native's accessibilityState), matching the pattern already used by Switch/Accordion. Without this, accessibilityState.checked alone isn't translated to ARIA by react-native-web.
  • A disabled ToggleButtonCard now consistently disables its inner toggle button too, not just the outer card background — previously disabled was never forwarded to the inner ToggleButton at all.
  • Radio and RadioCard no longer render their selected-state dot in a fixed colour regardless of checked state — previously every item in a group looked selected at once. The dot now only renders when actually checked.
  • Fixed a web-only bug (affecting Radio, RadioCard, Checkbox, ToggleButtonCard, and ToggleButton) where a disabled component still showed hover/pressed styling on mouseover. React Native Web's Pressable only sets pointer-events: box-none on the disabled element itself, not its descendants, so the inner indicator's :hover/:active CSS still matched. Those rules now only exist when the component is enabled.

No prop, callback, or rendered-output change beyond the fixes above. No developer changes required.

Test plan

  • pnpm checks:ci (lint, format, typecheck across the whole monorepo) — green
  • Full unit test suite (packages/react-native/src/**/*.test.ts) — 260 passing
  • Manually ran every Storybook play-function test across the affected story files in the browser (Radio, RadioGroup, RadioCard, RadioCardGroup, ToggleButtonCard, ToggleButtonCardGroup, Checkbox, CheckboxGroup, FormField) — all pass, including several that needed rewriting because they characterized gluestack-specific internals (a hidden <input type="radio">, and toBeDisabled() against a non-native-form-control element) rather than public behaviour
  • Manually verified in-browser (hover + computed styles) that disabled components no longer show hover/active styling, while enabled ones still do
  • Changeset added (🧹 [HOUSEKEEPING], patch)

Known follow-up (out of scope for this PR)

Button/IconButton/ToggleButton (migrated off gluestack in #1501, already merged) have the same "disabled still shows hover/active" root cause as described above, since it stems from a shared, pre-existing pattern (_web: { _hover, _active } declared unconditionally, Pressable.disabled never gating it) — worth a follow-up ticket since it's not limited to this PR's components.

🤖 Generated with Claude Code

…ard, and Checkbox off gluestack

Removes @gluestack-ui/form-control, @gluestack-ui/radio, and @gluestack-ui/checkbox.
Single-select group state (Radio/RadioCard/ToggleButtonCard) now runs through a
shared useSingleSelection hook; Checkbox uses local array state. FormField drops
createFormControl's dead id/aria-describedby linking in favour of its own existing
context.

Fixes found and addressed along the way: FormFieldLabel previously rendered null
regardless of children (a gluestack factory misconfiguration); Radio/RadioCard/
Checkbox now expose aria-checked on web (matching Switch/Accordion); a disabled
ToggleButtonCard now consistently disables its inner toggle button too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 14:44
@linear-code

linear-code Bot commented Aug 6, 2026

Copy link
Copy Markdown

UWDS-4800

UWDS-4789

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 870de5d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@utilitywarehouse/hearth-react-native Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Removes the remaining @gluestack-ui/* dependencies from @utilitywarehouse/hearth-react-native for FormField, Radio/RadioCard/ToggleButtonCard, and Checkbox, replacing gluestack’s selection/form-control internals with local state/hooks while preserving the public API (plus the explicitly called-out fixes).

Changes:

  • Drop @gluestack-ui/form-control, @gluestack-ui/radio, and @gluestack-ui/checkbox (and related transitives) from the RN package.
  • Introduce shared single-select state (useSingleSelection) and new multi-select utils for CheckboxGroup.
  • Update affected components/stories to use the new state model and add web aria-checked where needed.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm-lock.yaml Removes gluestack packages and orphaned transitive dependencies from the lockfile.
packages/react-native/src/hooks/useSingleSelection.ts Adds shared controlled/uncontrolled single-select state for radio-like groups.
packages/react-native/src/hooks/useSingleSelection.test.ts Unit tests for controlled vs uncontrolled selection resolution.
packages/react-native/src/components/ToggleButtonCard/ToggleButtonCardRoot.tsx Replaces gluestack-driven state/DOM hacks with local pressed-state + explicit toggle handling.
packages/react-native/src/components/ToggleButtonCard/ToggleButtonCardGroup.tsx Uses useSingleSelection + group context to coordinate selection across cards.
packages/react-native/src/components/ToggleButtonCard/ToggleButtonCardGroup.stories.tsx Updates play test to assert via callbacks rather than hidden <input>s.
packages/react-native/src/components/ToggleButtonCard/ToggleButtonCardGroup.context.ts New context for passing selectedValue/select to cards.
packages/react-native/src/components/ToggleButtonCard/ToggleButtonCard.tsx Removes createRadio usage; wires card selection via group context.
packages/react-native/src/components/ToggleButtonCard/ToggleButtonCard.stories.tsx Updates play test to assert via onChange rather than hidden <input>.
packages/react-native/src/components/ToggleButtonCard/index.ts Adjusts exports so ToggleButtonCardGroup is exported from its own module.
packages/react-native/src/components/RadioCard/RadioCardRoot.tsx Adds pressed-state tracking and web aria-checked; removes gluestack state plumbing.
packages/react-native/src/components/RadioCard/RadioCardGroup.tsx Replaces gluestack group selection with useSingleSelection + context.
packages/react-native/src/components/RadioCard/RadioCardGroup.stories.tsx Updates disabled characterization for RNW (aria-disabled expectations).
packages/react-native/src/components/RadioCard/RadioCardGroup.context.ts Extends context with selectedValue + select.
packages/react-native/src/components/RadioCard/RadioCard.tsx Removes createRadio; computes checked/disabled from context and selects via select.
packages/react-native/src/components/RadioCard/index.ts Adjusts exports so RadioCardGroup comes from its own module.
packages/react-native/src/components/Radio/RadioRoot.tsx Adds pressed-state tracking and web aria-checked; removes gluestack state plumbing.
packages/react-native/src/components/Radio/RadioGroup.tsx Replaces gluestack group selection with useSingleSelection + context + new root wrapper.
packages/react-native/src/components/Radio/RadioGroup.context.ts Extends context with selectedValue + select.
packages/react-native/src/components/Radio/Radio.tsx Removes createRadio; computes checked/disabled from context and selects via select.
packages/react-native/src/components/Radio/Radio.stories.tsx Updates disabled characterization for RNW (aria-disabled expectations).
packages/react-native/src/components/FormField/FormField.tsx Removes createFormControl; exports label/helper primitives directly and uses existing context/root.
packages/react-native/src/components/Checkbox/CheckboxRoot.tsx Adds pressed-state tracking and web aria-checked; removes gluestack state plumbing.
packages/react-native/src/components/Checkbox/CheckboxGroup.utils.ts Adds selection resolution + toggle utility for checkbox group state.
packages/react-native/src/components/Checkbox/CheckboxGroup.utils.test.ts Unit tests for checkbox group selection resolution and toggle semantics.
packages/react-native/src/components/Checkbox/CheckboxGroup.tsx Replaces gluestack group selection with local array state + context.
packages/react-native/src/components/Checkbox/CheckboxGroup.context.ts Extends context with selectedValues + select.
packages/react-native/src/components/Checkbox/Checkbox.tsx Removes createCheckbox; computes checked/disabled from context and toggles via select.
packages/react-native/src/components/Checkbox/Checkbox.stories.tsx Updates disabled characterization for RNW (aria-disabled expectations).
packages/react-native/package.json Removes direct gluestack deps (checkbox, form-control, radio).
.changeset/radio-checkbox-formfield-remove-gluestack.md Changeset documenting dependency removal and the user-visible fixes.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

packages/react-native/src/components/ToggleButtonCard/ToggleButtonCardGroup.tsx:41

  • The container is marked accessibilityRole="radiogroup", but the children are exposed as buttons rather than radios. This can produce incorrect announcements/keyboard expectations for assistive tech. Consider accessibilityRole="group" (or update items to role="radio" with checked state).
      <View
        {...props}
        accessibilityRole="radiogroup"
        style={[

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react-native/src/components/Radio/RadioGroup.tsx
Comment thread packages/react-native/src/components/Checkbox/CheckboxGroup.tsx
@robphoenix
robphoenix marked this pull request as draft August 10, 2026 07:50
jordmccord and others added 3 commits August 18, 2026 11:10
…, and always-visible radio dot

Two bugs surfaced while reviewing the gluestack removal:

- React Native Web's Pressable only sets pointer-events: box-none on a
  disabled element itself, not its descendants, so a disabled Radio,
  RadioCard, Checkbox, ToggleButtonCard, or ToggleButton still matched
  :hover/:active on its inner indicator and showed hover/pressed styling.
  The _web hover/active rules now only exist when the component is enabled
  (gated via a disabled variant/compound variant), rather than trying to
  block pointer-events on every descendant.
- Radio's and RadioCard's selected-state dot was rendered unconditionally
  in a fixed colour, so unselected items in a group looked identical to
  the selected one. The dot now only renders when actually checked.

ToggleButtonCard also wasn't forwarding `disabled` to its inner
ToggleButton at all, so a disabled card's toggle button remained fully
interactive - it's now forwarded through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…roup/ToggleButtonCardGroup

- RadioGroup/CheckboxGroup: fix stale-closure bug in the memoized context
  value by wrapping the select handler in useCallback with complete deps,
  instead of omitting it from useMemo's dependency array
- ToggleButtonCardGroup: drop accessibilityRole="radiogroup" — its items
  render as accessibilityRole="button" (ToggleButton), not "radio", so the
  role was mismatched; there's no "group" role in React Native's
  AccessibilityRole type to substitute, so this reverts to no role (the
  pre-migration behaviour)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@jordmccord
jordmccord marked this pull request as ready for review August 18, 2026 14:08
@jordmccord
jordmccord merged commit 4998fd7 into hearth-react-native-v1 Aug 18, 2026
8 of 9 checks passed
@jordmccord
jordmccord deleted the jordmccord-uwds-4800 branch August 18, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants