feat(checkbox): modernize for md3 spec compliance#4962
Conversation
|
Hey @fabriziocucci, thank you for your pull request 🤗. The documentation from this branch can be viewed here. |
90c056e to
9ec676c
Compare
There was a problem hiding this comment.
Pull request overview
Modernizes the Checkbox component to align with Material Design 3 interaction/visual specs, and (because this PR is stacked on #4952) includes foundational theme token reshaping plus new shared utilities (useFocusVisible, getStateLayer, token tree updates) that support MD3-compliant state layers and motion.
Changes:
- Reworks MD tokens into
md.ref.*(raw values) andmd.sys.*(semantic/system decisions), addingmd.sys.state.*and updating scheme builders/utilities accordingly. - Adds
useFocusVisibleand a sharedgetStateLayer(theme, role, state)helper; migrates several components/utilities to the new state-layer token source. - Rewrites
Checkboxrendering/animations to MD3: 40dp tap target + state layer overlay + focus ring + new checkmark/dash rendering, and updates related snapshots.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/useFocusVisible.ts | New hook to gate focus indicators on :focus-visible semantics. |
| src/types.tsx | Re-exports theme types and centralizes shared utility TS types. |
| src/theme/utils/state.ts | New helper for resolving {color, opacity} state layers from tokens + theme. |
| src/theme/types/utils.ts | Removes old theme-scoped utility type exports (moved to src/types.tsx). |
| src/theme/types/state.ts | Adds StateLayer / StateOpacityKey types for state-layer utilities. |
| src/theme/types/motion.ts | Adds RawSpring type used by motion helpers. |
| src/theme/types/index.ts | Exports new state types and drops utils export. |
| src/theme/types/color.ts | Adds ColorRole type to constrain valid color-role keys. |
| src/theme/tokens/sys/typography.ts | Re-homes typescale under md.sys.typescale and references new typeface tokens. |
| src/theme/tokens/sys/state.ts | Adds MD3 interaction state tokens (opacity + focus indicator sizing). |
| src/theme/tokens/sys/motion.ts | Updates toRawSpring to include MD3 mass + documents reanimated usage. |
| src/theme/tokens/sys/elevation.ts | Adds overload typings for animated vs static shadow styles. |
| src/theme/tokens/sys/color.ts | Updates scheme builder to use sys state tokens and ref palette directly. |
| src/theme/tokens/ref/typeface.ts | Adds ref typeface tokens (platform font families + weights). |
| src/theme/tokens/ref/palette.ts | Adds ref tonal palette tokens as a standalone module. |
| src/theme/tokens/index.ts | Rebuilds the token tree into md.ref + md.sys and re-exports palette/typescale. |
| src/theme/schemes/LightTheme.tsx | Updates scheme building call signature after token refactor. |
| src/theme/schemes/DarkTheme.tsx | Updates scheme building call signature after token refactor. |
| src/components/TextInput/helpers.tsx | Switches to md.sys.state.opacity for disabled/enabled opacity. |
| src/components/TextInput/Adornment/utils.ts | Uses new getStateLayer and sys state opacities for adornment colors. |
| src/components/SegmentedButtons/utils.ts | Switches to md.sys.state.opacity. |
| src/components/RadioButton/utils.ts | Switches to md.sys.state.opacity. |
| src/components/RadioButton/RadioButtonItem.tsx | Uses getStateLayer for label color/opacity. |
| src/components/Modal.tsx | Switches scrim alpha to md.sys.scrim.alpha. |
| src/components/Menu/utils.ts | Switches to md.sys.state.opacity. |
| src/components/IconButton/utils.ts | Switches to md.sys.state.opacity. |
| src/components/HelperText/utils.ts | Uses getStateLayer for error/disabled/info text colors. |
| src/components/Chip/helpers.tsx | Switches to md.sys.state.opacity. |
| src/components/Checkbox/utils.ts | Adds getSelectionVisualState for MD3 checkbox visuals; keeps legacy helper for radio. |
| src/components/Checkbox/CheckboxItem.tsx | Uses getStateLayer for label styling (opacity + color). |
| src/components/Checkbox/Checkbox.tsx | Full rewrite: Pressable tap target, state layer overlay, focus ring, new animations/glyph rendering. |
| src/components/Button/utils.tsx | Switches to md.sys.state.opacity. |
| src/components/tests/TextInput.test.tsx | Updates tests to read opacities from md.sys.state.opacity. |
| src/components/tests/SegmentedButton.test.tsx | Updates tests to read opacities from md.sys.state.opacity. |
| src/components/tests/RadioButton/utils.test.tsx | Updates tests to read opacities from md.sys.state.opacity. |
| src/components/tests/Modal.test.tsx | Updates tests to read scrim alpha from md.sys.scrim.alpha. |
| src/components/tests/MenuItem.test.tsx | Updates tests to read opacities from md.sys.state.opacity. |
| src/components/tests/IconButton.test.tsx | Updates tests to read opacities from md.sys.state.opacity. |
| src/components/tests/Chip.test.tsx | Updates tests to read opacities from md.sys.state.opacity. |
| src/components/tests/Checkbox/utils.test.tsx | Updates tests to read opacities from md.sys.state.opacity. |
| src/components/tests/Button.test.tsx | Updates tests to read opacities from md.sys.state.opacity. |
| src/components/tests/Checkbox/snapshots/CheckboxItem.test.tsx.snap | Snapshot updates for the new Checkbox render tree. |
| src/components/tests/Checkbox/snapshots/Checkbox.test.tsx.snap | Snapshot updates for the new Checkbox render tree. |
Comments suppressed due to low confidence (1)
src/components/Checkbox/Checkbox.tsx:21
Checkboxpreviously inheritedTouchableRippleprops (via$RemoveChildren<typeof TouchableRipple>). RedefiningPropsas a small custom object and not forwarding any other props toPressableis a breaking public API change (e.g.,style,hitSlop,accessibilityLabel,onLongPress,testID-adjacent props, etc.). Consider extendingPressable/TouchableRippleprops again and forwarding...restto the root element to preserve compatibility.
export type Props = {
/**
* Status of checkbox.
*/
status: 'checked' | 'unchecked' | 'indeterminate';
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| type FocusVisibleEvent = { | ||
| currentTarget: object; | ||
| }; |
| import { getTheme } from '../../../core/theming'; | ||
| import { tokens } from '../../../theme/tokens'; | ||
| import { getSelectionControlColor } from '../../Checkbox/utils'; | ||
| const { stateOpacity } = tokens.md.ref; | ||
|
|
||
| const stateOpacity = tokens.md.sys.state.opacity; | ||
|
|
There was a problem hiding this comment.
The helper is covered by the snapshot tests in Checkbox.test.tsx which exercise the full state matrix (unchecked/checked/indeterminate × disabled/error × hover/focus/pressed). The snapshots assert the exact RGBA values produced for each combination, so the helper's decision logic is verified. Dedicated unit tests for the helper would be valuable but aren't blocking for this PR — the snapshots already guard against regressions. Can add if you prefer.
9ec676c to
1ce4264
Compare
Rewrites the Checkbox renderer to match the Material Design 3 spec (https://m3.material.io/components/checkbox/specs): - 18dp container with 2dp outline (unselected) / 0dp outline + theme primary fill (selected), inside a 40dp state-layer tap target. - State-layer overlay renders hover (8%), focus (10%) and pressed (10%) layers in the color the spec defines for each (selected pressed flips to onSurface; error always wins). - Focus indicator: 3dp ring at theme.colors.secondary with the 2dp outer-offset from md.sys.state.focusIndicator. Gated on :focus-visible via the useFocusVisible hook added in callstack#4952. - Animations approximate Compose Material3 Checkbox.kt: 100ms fill transition and 150ms checkmark draw, sequenced short-leg then long-leg to suggest the stroke fraction. Indeterminate uses a scaleX-animated dash. - No new peer-deps: the checkmark is built from two rotated rectangles (View-based), not an SVG path. utils.ts: - New getSelectionVisualState helper returns the full color + opacity + outline-width picture for a given state combo. - Legacy getSelectionControlColor kept as a compatibility export for RadioButtonAndroid (radio button modernization is out of scope for this PR). 9 snapshots auto-updated to reflect the new render tree.
1ce4264 to
414277c
Compare
Summary
Stacked on #4952 (token reorg +
useFocusVisiblehook).Bring the
Checkboxcomponent up to the Material Design 3 spec, end-to-end:theme.colors.secondarywith the 2dp outer-offset frommd.sys.state.focusIndicator. Gated on:focus-visiblevia theuseFocusVisiblehook from feat: improve structure of ref and sys tokens; add useFocusVisible hook #4952.androidx.compose.material3.Checkbox.kt: 100ms fill transition + 150ms checkmark draw. The checkmark is sequenced short-leg then long-leg viascaleYto suggest the stroke fraction. Indeterminate uses ascaleX-animated dash.Why not SVG
Compose Material3 draws the checkmark as a
PathwithstrokeFraction0 → 1. Doing the same in RN would mean addingreact-native-svgas a Paper peer-dep, an ecosystem-level change. This PR uses a reveal-mask instead: a static L-shape (borderLeftWidth + borderBottomWidth rotated -45°) inside a left-anchored View whose width animates 0 → 18dp with a matching opacity fade. The visual suggests the stroke drawing left-to-right without the SVG dependency. Same precedent will apply to RadioButton when it's modernized. If you'd prefer the SVG-based approach for v6, happy to switch. Picking once now sets the precedent.Files
src/components/Checkbox/Checkbox.tsx: full rewrite (Pressable40dp tap target, state-layer overlay, animated 18dp container, focus ring, view-based checkmark + dash).src/components/Checkbox/utils.ts: newgetSelectionVisualStatereturns the full color + opacity + outline-width picture for any(selected × hovered × focused × pressed × disabled × error × customColor)combination. LegacygetSelectionControlColorkept as a back-compat export forRadioButtonAndroid.Out of scope
Animatedtoreact-native-reanimated.<StateLayer />as a shared primitive (can follow once Radio + Switch land and the pattern is proven).Test plan
yarn typescriptcleanyarn lintcleanyarn jest: 736/737 (1 skipped, pre-existing); 159/159 snapshots pass (9 updated for the new renderer).Visuals