Skip to content

Commit 6d92d77

Browse files
fabriziocucciadrcotfas
authored andcommitted
refactor(checkbox): unify platform-specific files (use Android impl)
Per maintainer feedback on #4955, drop the iOS-specific Checkbox renderer and inline the unified MD3 implementation directly into Checkbox.tsx. The same MaterialCommunityIcon-based control now renders on both platforms. Changes: - Checkbox.tsx: inlines what was the Android implementation; renders the same control on both platforms. Drops 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: drops the `mode`-based branching; always renders `<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). - docs/docusaurus.config.js: removed entries for the now-deleted CheckboxAndroid and CheckboxIOS files. - 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 6d92d77

17 files changed

Lines changed: 622 additions & 928 deletions

docs/docusaurus.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@ const config = {
9292
},
9393
Checkbox: {
9494
Checkbox: 'Checkbox/Checkbox',
95-
CheckboxAndroid: 'Checkbox/CheckboxAndroid',
96-
CheckboxIOS: 'Checkbox/CheckboxIOS',
9795
CheckboxItem: 'Checkbox/CheckboxItem',
9896
},
9997
Chip: {

example/src/Examples/CheckboxItemExample.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import ScreenWrapper from '../ScreenWrapper';
77

88
const CheckboxExample = () => {
99
const [checkedDefault, setCheckedDefault] = React.useState<boolean>(true);
10-
const [checkedAndroid, setCheckedAndroid] = React.useState<boolean>(true);
11-
const [checkedIOS, setCheckedIOS] = React.useState<boolean>(true);
1210
const [checkedLeadingControl, setCheckedLeadingControl] =
1311
React.useState<boolean>(true);
1412
const [checkedDisabled, setCheckedDisabled] = React.useState<boolean>(true);
@@ -17,27 +15,14 @@ const CheckboxExample = () => {
1715
return (
1816
<ScreenWrapper style={styles.container}>
1917
<Checkbox.Item
20-
label="Default (will look like whatever system this is running on)"
18+
label="Default"
2119
status={checkedDefault ? 'checked' : 'unchecked'}
2220
onPress={() => setCheckedDefault(!checkedDefault)}
2321
/>
24-
<Checkbox.Item
25-
label="Material Design"
26-
mode="android"
27-
status={checkedAndroid ? 'checked' : 'unchecked'}
28-
onPress={() => setCheckedAndroid(!checkedAndroid)}
29-
/>
30-
<Checkbox.Item
31-
label="iOS"
32-
mode="ios"
33-
status={checkedIOS ? 'checked' : 'unchecked'}
34-
onPress={() => setCheckedIOS(!checkedIOS)}
35-
/>
3622
<Checkbox.Item
3723
label="Default with leading control"
3824
status={checkedLeadingControl ? 'checked' : 'unchecked'}
3925
onPress={() => setCheckedLeadingControl(!checkedLeadingControl)}
40-
mode="ios"
4126
position="leading"
4227
/>
4328
<Checkbox.Item

src/components/Checkbox/Checkbox.tsx

Lines changed: 137 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 { getSelectionControlColor } 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,6 +51,8 @@ export type Props = {
4451
testID?: string;
4552
};
4653

54+
const ANIMATION_DURATION = 100;
55+
4756
/**
4857
* Checkboxes allow the selection of multiple options from a set.
4958
*
@@ -68,15 +77,131 @@ export type Props = {
6877
* export default MyComponent;
6978
* ```
7079
*/
71-
const Checkbox = ({ theme: themeOverrides, ...props }: Props) => {
80+
const Checkbox = ({
81+
status,
82+
theme: themeOverrides,
83+
disabled,
84+
onPress,
85+
testID,
86+
error,
87+
...rest
88+
}: Props) => {
7289
const theme = useInternalTheme(themeOverrides);
73-
return Platform.OS === 'ios' ? (
74-
<CheckboxIOS {...props} theme={theme} />
75-
) : (
76-
<CheckboxAndroid {...props} theme={theme} />
90+
const { current: scaleAnim } = React.useRef<Animated.Value>(
91+
new Animated.Value(1)
92+
);
93+
const isFirstRendering = React.useRef<boolean>(true);
94+
95+
const {
96+
animation: { scale },
97+
} = theme;
98+
99+
React.useEffect(() => {
100+
// Do not run animation on very first rendering
101+
if (isFirstRendering.current) {
102+
isFirstRendering.current = false;
103+
return;
104+
}
105+
106+
const checked = status === 'checked';
107+
108+
Animated.sequence([
109+
Animated.timing(scaleAnim, {
110+
toValue: 0.85,
111+
duration: checked ? ANIMATION_DURATION * scale : 0,
112+
useNativeDriver: false,
113+
}),
114+
Animated.timing(scaleAnim, {
115+
toValue: 1,
116+
duration: checked
117+
? ANIMATION_DURATION * scale
118+
: ANIMATION_DURATION * scale * 1.75,
119+
useNativeDriver: false,
120+
}),
121+
]).start();
122+
}, [status, scaleAnim, scale]);
123+
124+
const checked = status === 'checked';
125+
const indeterminate = status === 'indeterminate';
126+
127+
const { selectionControlColor, selectionControlOpacity } =
128+
getSelectionControlColor({
129+
theme,
130+
disabled,
131+
checked,
132+
customColor: rest.color,
133+
customUncheckedColor: rest.uncheckedColor,
134+
error,
135+
});
136+
137+
const borderWidth = scaleAnim.interpolate({
138+
inputRange: [0.8, 1],
139+
outputRange: [7, 0],
140+
});
141+
142+
const icon = indeterminate
143+
? 'minus-box'
144+
: checked
145+
? 'checkbox-marked'
146+
: 'checkbox-blank-outline';
147+
148+
return (
149+
<TouchableRipple
150+
{...rest}
151+
borderless
152+
onPress={onPress}
153+
disabled={disabled}
154+
accessibilityRole="checkbox"
155+
accessibilityState={{ disabled, checked }}
156+
accessibilityLiveRegion="polite"
157+
style={styles.container}
158+
testID={testID}
159+
theme={theme}
160+
>
161+
<Animated.View
162+
style={{
163+
transform: [{ scale: scaleAnim }],
164+
opacity: selectionControlOpacity,
165+
}}
166+
>
167+
<MaterialCommunityIcon
168+
allowFontScaling={false}
169+
name={icon}
170+
size={24}
171+
color={selectionControlColor}
172+
direction="ltr"
173+
/>
174+
<View style={[StyleSheet.absoluteFill, styles.fillContainer]}>
175+
<Animated.View
176+
style={[
177+
styles.fill,
178+
{ borderColor: selectionControlColor },
179+
{ borderWidth },
180+
]}
181+
/>
182+
</View>
183+
</Animated.View>
184+
</TouchableRipple>
77185
);
78186
};
79187

188+
const styles = StyleSheet.create({
189+
container: {
190+
borderRadius: 18,
191+
width: 36,
192+
height: 36,
193+
padding: 6,
194+
},
195+
fillContainer: {
196+
alignItems: 'center',
197+
justifyContent: 'center',
198+
},
199+
fill: {
200+
height: 14,
201+
width: 14,
202+
},
203+
});
204+
80205
export default Checkbox;
81206

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

0 commit comments

Comments
 (0)