Skip to content

Commit f674da6

Browse files
committed
feat(checkbox): add error state via MD3 error tokens
Adds an `error?: boolean` prop to Checkbox, CheckboxAndroid, and CheckboxIOS. When true, the outline (unchecked) and container (checked / indeterminate) use `theme.colors.error`. The `disabled` state and explicit `color` / `uncheckedColor` overrides take precedence. Addresses one bullet from #4937 / #4949 (Checkbox section, "Error state not implemented"). Verified visually on iOS Simulator and Android Emulator across light and dark themes.
1 parent 5679298 commit f674da6

4 files changed

Lines changed: 50 additions & 3 deletions

File tree

src/components/Checkbox/Checkbox.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ export type Props = {
2727
* Custom color for checkbox.
2828
*/
2929
color?: string;
30+
/**
31+
* Whether the checkbox is in an error state. When true, the outline
32+
* (unchecked) and container (checked / indeterminate) use
33+
* `theme.colors.error`. `disabled` and explicit `color`/`uncheckedColor`
34+
* overrides take precedence.
35+
*/
36+
error?: boolean;
3037
/**
3138
* @optional
3239
*/

src/components/Checkbox/CheckboxAndroid.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ export type Props = $RemoveChildren<typeof TouchableRipple> & {
3434
* Custom color for checkbox.
3535
*/
3636
color?: ColorValue;
37+
/**
38+
* Whether the checkbox is in an error state. When true, the outline
39+
* (unchecked) and container (checked / indeterminate) use
40+
* `theme.colors.error`. `disabled` and explicit `color`/`uncheckedColor`
41+
* overrides take precedence.
42+
*/
43+
error?: boolean;
3744
/**
3845
* @optional
3946
*/
@@ -60,6 +67,7 @@ const CheckboxAndroid = ({
6067
disabled,
6168
onPress,
6269
testID,
70+
error,
6371
...rest
6472
}: Props) => {
6573
const theme = useInternalTheme(themeOverrides);
@@ -107,6 +115,7 @@ const CheckboxAndroid = ({
107115
checked,
108116
customColor: rest.color,
109117
customUncheckedColor: rest.uncheckedColor,
118+
error,
110119
});
111120

112121
const borderWidth = scaleAnim.interpolate({

src/components/Checkbox/CheckboxIOS.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export type Props = $RemoveChildren<typeof TouchableRipple> & {
2929
* Custom color for checkbox.
3030
*/
3131
color?: ColorValue;
32+
/**
33+
* Whether the checkbox is in an error state. When true, the checked /
34+
* indeterminate icon uses `theme.colors.error`. `disabled` and explicit
35+
* `color` overrides take precedence.
36+
*/
37+
error?: boolean;
3238
/**
3339
* @optional
3440
*/
@@ -52,16 +58,18 @@ const CheckboxIOS = ({
5258
onPress,
5359
theme: themeOverrides,
5460
testID,
61+
error,
5562
...rest
5663
}: Props) => {
5764
const theme = useInternalTheme(themeOverrides);
5865
const checked = status === 'checked';
5966
const indeterminate = status === 'indeterminate';
6067

61-
const { checkedColor } = getSelectionControlIOSColor({
68+
const { checkedColor, checkedColorOpacity } = getSelectionControlIOSColor({
6269
theme,
6370
disabled,
6471
customColor: rest.color,
72+
error,
6573
});
6674

6775
const icon = indeterminate ? 'minus' : 'check';

src/components/Checkbox/utils.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,40 @@ const { stateOpacity } = tokens.md.ref;
88
const getAndroidCheckedColor = ({
99
theme,
1010
customColor,
11+
error,
1112
}: {
1213
theme: InternalTheme;
1314
customColor?: ColorValue;
15+
error?: boolean;
1416
}) => {
1517
if (customColor) {
1618
return customColor;
1719
}
1820

21+
if (error) {
22+
return theme.colors.error;
23+
}
24+
1925
return theme.colors.primary;
2026
};
2127

2228
const getAndroidUncheckedColor = ({
2329
theme,
2430
customUncheckedColor,
31+
error,
2532
}: {
2633
theme: InternalTheme;
2734
customUncheckedColor?: ColorValue;
35+
error?: boolean;
2836
}) => {
2937
if (customUncheckedColor) {
3038
return customUncheckedColor;
3139
}
3240

41+
if (error) {
42+
return theme.colors.error;
43+
}
44+
3345
return theme.colors.onSurfaceVariant;
3446
};
3547

@@ -62,17 +74,20 @@ export const getAndroidSelectionControlColor = ({
6274
checked,
6375
customColor,
6476
customUncheckedColor,
77+
error,
6578
}: {
6679
theme: InternalTheme;
6780
checked: boolean;
6881
disabled?: boolean;
6982
customColor?: ColorValue;
7083
customUncheckedColor?: ColorValue;
84+
error?: boolean;
7185
}) => {
72-
const checkedColor = getAndroidCheckedColor({ theme, customColor });
86+
const checkedColor = getAndroidCheckedColor({ theme, customColor, error });
7387
const uncheckedColor = getAndroidUncheckedColor({
7488
theme,
7589
customUncheckedColor,
90+
error,
7691
});
7792
const selectionControlOpacity = disabled
7893
? stateOpacity.disabled
@@ -94,10 +109,12 @@ const getIOSCheckedColor = ({
94109
theme,
95110
disabled,
96111
customColor,
112+
error,
97113
}: {
98114
theme: InternalTheme;
99115
customColor?: ColorValue;
100116
disabled?: boolean;
117+
error?: boolean;
101118
}) => {
102119
if (disabled) {
103120
return theme.colors.primary;
@@ -107,19 +124,25 @@ const getIOSCheckedColor = ({
107124
return customColor;
108125
}
109126

127+
if (error) {
128+
return theme.colors.error;
129+
}
130+
110131
return theme.colors.primary;
111132
};
112133

113134
export const getSelectionControlIOSColor = ({
114135
theme,
115136
disabled,
116137
customColor,
138+
error,
117139
}: {
118140
theme: InternalTheme;
119141
disabled?: boolean;
120142
customColor?: ColorValue;
143+
error?: boolean;
121144
}) => {
122-
const checkedColor = getIOSCheckedColor({ theme, disabled, customColor });
145+
const checkedColor = getIOSCheckedColor({ theme, disabled, customColor, error });
123146
const checkedColorOpacity = disabled
124147
? stateOpacity.disabled
125148
: stateOpacity.enabled;

0 commit comments

Comments
 (0)