fix(react-native): migrate FormField, Radio, RadioCard, ToggleButtonCard, and Checkbox off gluestack - #1503
Conversation
…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>
🦋 Changeset detectedLatest commit: 870de5d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
There was a problem hiding this comment.
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 forCheckboxGroup. - Update affected components/stories to use the new state model and add web
aria-checkedwhere 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. ConsideraccessibilityRole="group"(or update items torole="radio"with checked state).
<View
{...props}
accessibilityRole="radiogroup"
style={[
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…, 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>
Summary
Part of UWDS-4789 (removing
@gluestack-ui/*frompackages/react-native). This sub-issue (UWDS-4800) removes the coupled trio:@gluestack-ui/form-control,@gluestack-ui/radio, and@gluestack-ui/checkbox.createRadio/createCheckboxprovided 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-ReactuseSingleSelectionhook (single-select:Radio/RadioCard/ToggleButtonCard) or local array state (multi-select:Checkbox), with no hidden<input>.createFormControl'suseFormControlProviderid/aria-describedbylinking was dead code — nothing downstream ever consumed the generated ids — soFormFielddrops it entirely in favour of its own existing context.selectedValue/selectedValuesthrough context instead of anisSelected(value)selector function, so items compare locally and noisX-named field appears anywhere in the (internal) API surface.Fixes found and addressed during the migration (called out explicitly in the changeset):
FormFieldLabelpreviously renderednullregardless of its children, due to a gluestack factory misconfiguration — it now renders correctly. This was silently broken in both theFormField"Advanced Usage" docs and theSwitch"Switch with Label" example.Radio,RadioCard, andCheckboxnow expose their checked state on web viaaria-checked(in addition to React Native'saccessibilityState), matching the pattern already used bySwitch/Accordion. Without this,accessibilityState.checkedalone isn't translated to ARIA byreact-native-web.ToggleButtonCardnow consistently disables its inner toggle button too, not just the outer card background — previouslydisabledwas never forwarded to the innerToggleButtonat all.RadioandRadioCardno 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.Radio,RadioCard,Checkbox,ToggleButtonCard, andToggleButton) where a disabled component still showed hover/pressed styling on mouseover. React Native Web'sPressableonly setspointer-events: box-noneon the disabled element itself, not its descendants, so the inner indicator's:hover/:activeCSS 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) — greenpackages/react-native/src/**/*.test.ts) — 260 passing<input type="radio">, andtoBeDisabled()against a non-native-form-control element) rather than public behaviour🧹 [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.disablednever gating it) — worth a follow-up ticket since it's not limited to this PR's components.🤖 Generated with Claude Code