Skip to content

Commit 1fe92ec

Browse files
committed
feat(checkbox): modernize for md3 spec compliance
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 #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.
1 parent 8720eac commit 1fe92ec

11 files changed

Lines changed: 1788 additions & 838 deletions

File tree

example/src/Examples/CheckboxExample.tsx

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,27 @@ import ScreenWrapper from '../ScreenWrapper';
88
const CheckboxExample = () => {
99
const [checkedNormal, setCheckedNormal] = React.useState<boolean>(true);
1010
const [checkedCustom, setCheckedCustom] = React.useState<boolean>(true);
11-
const [indeterminate, setIndeterminate] = React.useState<boolean>(true);
11+
const [checkedError, setCheckedError] = React.useState<boolean>(false);
12+
13+
// Indeterminate parent: aggregates the state of its children.
14+
const [child1, setChild1] = React.useState<boolean>(true);
15+
const [child2, setChild2] = React.useState<boolean>(false);
16+
const [child3, setChild3] = React.useState<boolean>(true);
17+
18+
const allChecked = child1 && child2 && child3;
19+
const noneChecked = !child1 && !child2 && !child3;
20+
const parentStatus: 'checked' | 'unchecked' | 'indeterminate' = allChecked
21+
? 'checked'
22+
: noneChecked
23+
? 'unchecked'
24+
: 'indeterminate';
25+
26+
const toggleAllChildren = () => {
27+
const next = !allChecked;
28+
setChild1(next);
29+
setChild2(next);
30+
setChild3(next);
31+
};
1232

1333
return (
1434
<ScreenWrapper style={styles.container}>
@@ -33,11 +53,11 @@ const CheckboxExample = () => {
3353
</View>
3454
</TouchableRipple>
3555

36-
<TouchableRipple onPress={() => setIndeterminate(!indeterminate)}>
56+
<TouchableRipple onPress={() => setCheckedError(!checkedError)}>
3757
<View style={styles.row}>
38-
<Text>Indeterminate</Text>
58+
<Text>Error</Text>
3959
<View pointerEvents="none">
40-
<Checkbox status={indeterminate ? 'indeterminate' : 'unchecked'} />
60+
<Checkbox error status={checkedError ? 'checked' : 'unchecked'} />
4161
</View>
4262
</View>
4363
</TouchableRipple>
@@ -54,6 +74,42 @@ const CheckboxExample = () => {
5474
<Text>Indeterminate (Disabled)</Text>
5575
<Checkbox status="indeterminate" disabled />
5676
</View>
77+
78+
<View style={styles.sectionLabel}>
79+
<Text variant="labelLarge">Parent / children (indeterminate)</Text>
80+
</View>
81+
<TouchableRipple onPress={toggleAllChildren}>
82+
<View style={styles.row}>
83+
<Text>Select all</Text>
84+
<View pointerEvents="none">
85+
<Checkbox status={parentStatus} />
86+
</View>
87+
</View>
88+
</TouchableRipple>
89+
<TouchableRipple onPress={() => setChild1(!child1)}>
90+
<View style={[styles.row, styles.indent]}>
91+
<Text>Apples</Text>
92+
<View pointerEvents="none">
93+
<Checkbox status={child1 ? 'checked' : 'unchecked'} />
94+
</View>
95+
</View>
96+
</TouchableRipple>
97+
<TouchableRipple onPress={() => setChild2(!child2)}>
98+
<View style={[styles.row, styles.indent]}>
99+
<Text>Bananas</Text>
100+
<View pointerEvents="none">
101+
<Checkbox status={child2 ? 'checked' : 'unchecked'} />
102+
</View>
103+
</View>
104+
</TouchableRipple>
105+
<TouchableRipple onPress={() => setChild3(!child3)}>
106+
<View style={[styles.row, styles.indent]}>
107+
<Text>Cherries</Text>
108+
<View pointerEvents="none">
109+
<Checkbox status={child3 ? 'checked' : 'unchecked'} />
110+
</View>
111+
</View>
112+
</TouchableRipple>
57113
</ScreenWrapper>
58114
);
59115
};
@@ -71,6 +127,14 @@ const styles = StyleSheet.create({
71127
paddingVertical: 8,
72128
paddingHorizontal: 16,
73129
},
130+
indent: {
131+
paddingLeft: 32,
132+
},
133+
sectionLabel: {
134+
paddingHorizontal: 16,
135+
paddingTop: 16,
136+
paddingBottom: 4,
137+
},
74138
});
75139

76140
export default CheckboxExample;

0 commit comments

Comments
 (0)