Skip to content

Commit 13be77d

Browse files
committed
refactor(checkbox): unify platform-specific files (use Android impl)
Per maintainer feedback on #4955, replace the iOS-specific Checkbox implementation with the Android one (which is closer to the MD3 spec) and inline the unified implementation directly into Checkbox.tsx. Changes: - Checkbox.tsx: inline what was the Android implementation; render the same MD3-compliant control on both platforms. Drop the Platform.OS dispatching. - CheckboxAndroid.tsx, CheckboxIOS.tsx: deleted. - Checkbox/index.ts: `Checkbox.Android` and `Checkbox.IOS` are kept as back-compat aliases of `Checkbox` itself, so existing imports keep working with no breaking change. - CheckboxItem.tsx: drop the `mode`-based branching; always render `<Checkbox />`. The `mode` prop is kept and marked deprecated. - src/index.tsx: `CheckboxAndroidProps` and `CheckboxIOSProps` now alias the unified `Props` from Checkbox.tsx (no breaking change). - Snapshot tests updated: the renderer now uses the unified container (36x36 ripple) and MaterialCommunityIcon glyphs on both platforms. `getSelectionControlIOSColor` in utils.ts is intentionally left in place because RadioButtonIOS still depends on it. A similar unification for RadioButton can follow in a separate PR. As a side benefit, callers using `<Checkbox.IOS uncheckedColor=...>` will now see the prop applied (the old iOS variant silently dropped it), addressing one of the API-parity gaps noted in #4949.
1 parent e860605 commit 13be77d

8 files changed

Lines changed: 555 additions & 354 deletions

File tree

src/components/Checkbox/Checkbox.tsx

Lines changed: 142 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import * as React from 'react';
2-
import { GestureResponderEvent, Platform } from 'react-native';
2+
import {
3+
Animated,
4+
ColorValue,
5+
GestureResponderEvent,
6+
StyleSheet,
7+
View,
8+
} from 'react-native';
39

4-
import CheckboxAndroid from './CheckboxAndroid';
5-
import CheckboxIOS from './CheckboxIOS';
10+
import { getAndroidSelectionControlColor } from './utils';
611
import { useInternalTheme } from '../../core/theming';
7-
import type { ThemeProp } from '../../types';
12+
import type { $RemoveChildren, ThemeProp } from '../../types';
13+
import MaterialCommunityIcon from '../MaterialCommunityIcon';
14+
import TouchableRipple from '../TouchableRipple/TouchableRipple';
815

9-
export type Props = {
16+
export type Props = $RemoveChildren<typeof TouchableRipple> & {
1017
/**
1118
* Status of checkbox.
1219
*/
@@ -22,11 +29,11 @@ export type Props = {
2229
/**
2330
* Custom color for unchecked checkbox.
2431
*/
25-
uncheckedColor?: string;
32+
uncheckedColor?: ColorValue;
2633
/**
2734
* Custom color for checkbox.
2835
*/
29-
color?: string;
36+
color?: ColorValue;
3037
/**
3138
* Whether the checkbox is in an error state. When true, the outline
3239
* (unchecked) and container (checked / indeterminate) use
@@ -44,9 +51,16 @@ export type Props = {
4451
testID?: string;
4552
};
4653

54+
// From https://material.io/design/motion/speed.html#duration
55+
const ANIMATION_DURATION = 100;
56+
4757
/**
4858
* Checkboxes allow the selection of multiple options from a set.
4959
*
60+
* The component renders a single MD3-compliant control on both platforms,
61+
* using the MaterialCommunityIcon glyphs that match the Material Design
62+
* specification.
63+
*
5064
* ## Usage
5165
* ```js
5266
* import * as React from 'react';
@@ -68,15 +82,131 @@ export type Props = {
6882
* export default MyComponent;
6983
* ```
7084
*/
71-
const Checkbox = ({ theme: themeOverrides, ...props }: Props) => {
85+
const Checkbox = ({
86+
status,
87+
theme: themeOverrides,
88+
disabled,
89+
onPress,
90+
testID,
91+
error,
92+
...rest
93+
}: Props) => {
7294
const theme = useInternalTheme(themeOverrides);
73-
return Platform.OS === 'ios' ? (
74-
<CheckboxIOS {...props} theme={theme} />
75-
) : (
76-
<CheckboxAndroid {...props} theme={theme} />
95+
const { current: scaleAnim } = React.useRef<Animated.Value>(
96+
new Animated.Value(1)
97+
);
98+
const isFirstRendering = React.useRef<boolean>(true);
99+
100+
const {
101+
animation: { scale },
102+
} = theme;
103+
104+
React.useEffect(() => {
105+
// Do not run animation on very first rendering
106+
if (isFirstRendering.current) {
107+
isFirstRendering.current = false;
108+
return;
109+
}
110+
111+
const checked = status === 'checked';
112+
113+
Animated.sequence([
114+
Animated.timing(scaleAnim, {
115+
toValue: 0.85,
116+
duration: checked ? ANIMATION_DURATION * scale : 0,
117+
useNativeDriver: false,
118+
}),
119+
Animated.timing(scaleAnim, {
120+
toValue: 1,
121+
duration: checked
122+
? ANIMATION_DURATION * scale
123+
: ANIMATION_DURATION * scale * 1.75,
124+
useNativeDriver: false,
125+
}),
126+
]).start();
127+
}, [status, scaleAnim, scale]);
128+
129+
const checked = status === 'checked';
130+
const indeterminate = status === 'indeterminate';
131+
132+
const { selectionControlColor, selectionControlOpacity } =
133+
getAndroidSelectionControlColor({
134+
theme,
135+
disabled,
136+
checked,
137+
customColor: rest.color,
138+
customUncheckedColor: rest.uncheckedColor,
139+
error,
140+
});
141+
142+
const borderWidth = scaleAnim.interpolate({
143+
inputRange: [0.8, 1],
144+
outputRange: [7, 0],
145+
});
146+
147+
const icon = indeterminate
148+
? 'minus-box'
149+
: checked
150+
? 'checkbox-marked'
151+
: 'checkbox-blank-outline';
152+
153+
return (
154+
<TouchableRipple
155+
{...rest}
156+
borderless
157+
onPress={onPress}
158+
disabled={disabled}
159+
accessibilityRole="checkbox"
160+
accessibilityState={{ disabled, checked }}
161+
accessibilityLiveRegion="polite"
162+
style={styles.container}
163+
testID={testID}
164+
theme={theme}
165+
>
166+
<Animated.View
167+
style={{
168+
transform: [{ scale: scaleAnim }],
169+
opacity: selectionControlOpacity,
170+
}}
171+
>
172+
<MaterialCommunityIcon
173+
allowFontScaling={false}
174+
name={icon}
175+
size={24}
176+
color={selectionControlColor}
177+
direction="ltr"
178+
/>
179+
<View style={[StyleSheet.absoluteFill, styles.fillContainer]}>
180+
<Animated.View
181+
style={[
182+
styles.fill,
183+
{ borderColor: selectionControlColor },
184+
{ borderWidth },
185+
]}
186+
/>
187+
</View>
188+
</Animated.View>
189+
</TouchableRipple>
77190
);
78191
};
79192

193+
const styles = StyleSheet.create({
194+
container: {
195+
borderRadius: 18,
196+
width: 36,
197+
height: 36,
198+
padding: 6,
199+
},
200+
fillContainer: {
201+
alignItems: 'center',
202+
justifyContent: 'center',
203+
},
204+
fill: {
205+
height: 14,
206+
width: 14,
207+
},
208+
});
209+
80210
export default Checkbox;
81211

82212
// @component-docs ignore-next-line

src/components/Checkbox/CheckboxAndroid.tsx

Lines changed: 0 additions & 194 deletions
This file was deleted.

0 commit comments

Comments
 (0)