Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ export type Props = {
* Custom color for checkbox.
*/
color?: string;
/**
* Whether the checkbox is in an error state. When true, the outline
* (unchecked) and container (checked / indeterminate) use
* `theme.colors.error`. `disabled` and explicit `color`/`uncheckedColor`
* overrides take precedence.
*/
error?: boolean;
/**
* @optional
*/
Expand Down
9 changes: 9 additions & 0 deletions src/components/Checkbox/CheckboxAndroid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export type Props = $RemoveChildren<typeof TouchableRipple> & {
* Custom color for checkbox.
*/
color?: ColorValue;
/**
* Whether the checkbox is in an error state. When true, the outline
* (unchecked) and container (checked / indeterminate) use
* `theme.colors.error`. `disabled` and explicit `color`/`uncheckedColor`
* overrides take precedence.
*/
error?: boolean;
/**
* @optional
*/
Expand All @@ -60,6 +67,7 @@ const CheckboxAndroid = ({
disabled,
onPress,
testID,
error,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
Expand Down Expand Up @@ -107,6 +115,7 @@ const CheckboxAndroid = ({
checked,
customColor: rest.color,
customUncheckedColor: rest.uncheckedColor,
error,
});

const borderWidth = scaleAnim.interpolate({
Expand Down
8 changes: 8 additions & 0 deletions src/components/Checkbox/CheckboxIOS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export type Props = $RemoveChildren<typeof TouchableRipple> & {
* Custom color for checkbox.
*/
color?: ColorValue;
/**
* Whether the checkbox is in an error state. When true, the checked /
* indeterminate icon uses `theme.colors.error`. `disabled` and explicit
* `color` overrides take precedence.
*/
error?: boolean;
/**
* @optional
*/
Expand All @@ -52,6 +58,7 @@ const CheckboxIOS = ({
onPress,
theme: themeOverrides,
testID,
error,
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
Expand All @@ -62,6 +69,7 @@ const CheckboxIOS = ({
theme,
disabled,
customColor: rest.color,
error,
});

const icon = indeterminate ? 'minus' : 'check';
Expand Down
32 changes: 30 additions & 2 deletions src/components/Checkbox/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,40 @@ const { stateOpacity } = tokens.md.ref;
const getAndroidCheckedColor = ({
theme,
customColor,
error,
}: {
theme: InternalTheme;
customColor?: ColorValue;
error?: boolean;
}) => {
if (customColor) {
return customColor;
}

if (error) {
return theme.colors.error;
}

return theme.colors.primary;
};
Comment thread
adrcotfas marked this conversation as resolved.

const getAndroidUncheckedColor = ({
theme,
customUncheckedColor,
error,
}: {
theme: InternalTheme;
customUncheckedColor?: ColorValue;
error?: boolean;
}) => {
if (customUncheckedColor) {
return customUncheckedColor;
}

if (error) {
return theme.colors.error;
}

return theme.colors.onSurfaceVariant;
};

Expand Down Expand Up @@ -62,17 +74,20 @@ export const getAndroidSelectionControlColor = ({
checked,
customColor,
customUncheckedColor,
error,
}: {
theme: InternalTheme;
checked: boolean;
disabled?: boolean;
customColor?: ColorValue;
customUncheckedColor?: ColorValue;
error?: boolean;
}) => {
const checkedColor = getAndroidCheckedColor({ theme, customColor });
const checkedColor = getAndroidCheckedColor({ theme, customColor, error });
const uncheckedColor = getAndroidUncheckedColor({
theme,
customUncheckedColor,
error,
});
const selectionControlOpacity = disabled
? stateOpacity.disabled
Expand All @@ -94,10 +109,12 @@ const getIOSCheckedColor = ({
theme,
disabled,
customColor,
error,
}: {
theme: InternalTheme;
customColor?: ColorValue;
disabled?: boolean;
error?: boolean;
}) => {
if (disabled) {
return theme.colors.primary;
Expand All @@ -107,19 +124,30 @@ const getIOSCheckedColor = ({
return customColor;
}

if (error) {
return theme.colors.error;
}

return theme.colors.primary;
};

export const getSelectionControlIOSColor = ({
theme,
disabled,
customColor,
error,
}: {
theme: InternalTheme;
disabled?: boolean;
customColor?: ColorValue;
error?: boolean;
}) => {
const checkedColor = getIOSCheckedColor({ theme, disabled, customColor });
const checkedColor = getIOSCheckedColor({
theme,
disabled,
customColor,
error,
});
const checkedColorOpacity = disabled
? stateOpacity.disabled
: stateOpacity.enabled;
Expand Down
123 changes: 123 additions & 0 deletions src/components/__tests__/Checkbox/utils.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,82 @@ describe('getAndroidSelectionControlColor - checkbox color', () => {
selectionControlColor: getTheme(false).colors.onSurfaceVariant,
});
});

it('should return error color, checked, when error is true', () => {
expect(
getAndroidSelectionControlColor({
theme: getTheme(),
checked: true,
error: true,
})
).toMatchObject({
selectionControlColor: getTheme().colors.error,
});
});

it('should return error color, unchecked, when error is true', () => {
expect(
getAndroidSelectionControlColor({
theme: getTheme(),
checked: false,
error: true,
})
).toMatchObject({
selectionControlColor: getTheme().colors.error,
});
});

it('should return error color, checked, dark mode, when error is true', () => {
expect(
getAndroidSelectionControlColor({
theme: getTheme(true),
checked: true,
error: true,
})
).toMatchObject({
selectionControlColor: getTheme(true).colors.error,
});
});

it('should return disabled color when both disabled and error are true (disabled wins)', () => {
expect(
getAndroidSelectionControlColor({
theme: getTheme(),
disabled: true,
checked: true,
error: true,
})
).toMatchObject({
selectionControlColor: getTheme().colors.onSurface,
selectionControlOpacity: stateOpacity.disabled,
});
});

it('should return custom color when both customColor and error are true, checked (customColor wins)', () => {
expect(
getAndroidSelectionControlColor({
theme: getTheme(),
checked: true,
customColor: 'purple',
error: true,
})
).toMatchObject({
selectionControlColor: 'purple',
});
});

it('should return custom unchecked color when both customUncheckedColor and error are true, unchecked (customUncheckedColor wins)', () => {
expect(
getAndroidSelectionControlColor({
theme: getTheme(),
checked: false,
customUncheckedColor: 'purple',
error: true,
})
).toMatchObject({
selectionControlColor: 'purple',
});
});
});

describe('getSelectionControlIOSColor - checked color', () => {
Expand Down Expand Up @@ -122,4 +198,51 @@ describe('getSelectionControlIOSColor - checked color', () => {
checkedColor: getTheme().colors.primary,
});
});

it('should return error color when error is true', () => {
expect(
getSelectionControlIOSColor({
theme: getTheme(),
error: true,
})
).toMatchObject({
checkedColor: getTheme().colors.error,
});
});

it('should return error color, dark mode, when error is true', () => {
expect(
getSelectionControlIOSColor({
theme: getTheme(true),
error: true,
})
).toMatchObject({
checkedColor: getTheme(true).colors.error,
});
});

it('should return disabled color when both disabled and error are true (disabled wins)', () => {
expect(
getSelectionControlIOSColor({
theme: getTheme(),
disabled: true,
error: true,
})
).toMatchObject({
checkedColor: getTheme().colors.primary,
checkedColorOpacity: stateOpacity.disabled,
});
});

it('should return custom color when both customColor and error are true (customColor wins)', () => {
expect(
getSelectionControlIOSColor({
theme: getTheme(),
customColor: 'purple',
error: true,
})
).toMatchObject({
checkedColor: 'purple',
});
});
});
Loading