Skip to content

Commit aa913e1

Browse files
committed
Add CheckboxGroup component
- Add CheckboxGroup component. You can access it using Checkbox.Group. - Move checkbox icon calculation to a hook. - Add a context to handle checkboxes group state. - Control checbox with state context when it's inside a group.
1 parent 722675a commit aa913e1

9 files changed

+220
-92
lines changed
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { cloneElement } from 'react';
2+
import Text from '../Text';
3+
import Pressable from '../Pressable';
4+
import { HStack } from '../Stack';
5+
import { useCheckboxPropsResolver } from './hooks';
6+
import type { ICheckboxProps } from './types';
7+
import Box from '../Box';
8+
9+
const Checkbox = ( {
10+
label,
11+
selected = false,
12+
indeterminated = false,
13+
testID,
14+
...props
15+
}: ICheckboxProps ) => {
16+
const {
17+
icon,
18+
iconProps,
19+
iconContainerProps,
20+
labelProps,
21+
containerProps
22+
} = useCheckboxPropsResolver( {
23+
indeterminated, selected, ...props
24+
} );
25+
26+
return (
27+
<Pressable
28+
accessible
29+
accessibilityRole='checkbox'
30+
accessibilityLabel={label}
31+
accessibilityState={{
32+
checked: indeterminated ? 'mixed' : selected,
33+
disabled: props.disabled || false
34+
}}
35+
testID={testID}
36+
{...containerProps}
37+
>
38+
<HStack space="0.5" alignItems="center" alignContent="flex-start">
39+
<Box {...iconContainerProps}>
40+
{cloneElement( icon, {
41+
...iconProps,
42+
testID: testID && `${testID}-icon`
43+
} )}
44+
</Box>
45+
{!!label && (
46+
<Text
47+
{...labelProps}
48+
selectable={false}
49+
testID={testID && `${testID}-label`}
50+
>
51+
{label}
52+
</Text>
53+
)}
54+
</HStack>
55+
</Pressable>
56+
);
57+
};
58+
59+
export default Checkbox;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React, {
2+
memo, forwardRef, useState, useCallback
3+
} from 'react';
4+
import type { ICheckboxGroupProps } from './types';
5+
import Box from '../Box';
6+
import { CheckboxGroupContext } from './context';
7+
8+
const CheckboxGroup = ( {
9+
children,
10+
value: initialSelectedValues = [],
11+
onChange,
12+
...props
13+
}: ICheckboxGroupProps, ref?: any ) => {
14+
const [ selectedValues, setSelectedValues ] = useState( new Set( initialSelectedValues ) );
15+
const onCheckboxPress = useCallback( ( checkboxValue: string ) => {
16+
if ( selectedValues.has( checkboxValue ) ) {
17+
selectedValues.delete( checkboxValue );
18+
} else {
19+
selectedValues.add( checkboxValue );
20+
}
21+
22+
setSelectedValues( selectedValues );
23+
onChange?.( Array.from( selectedValues ) );
24+
}, [ selectedValues, setSelectedValues, onChange ] );
25+
26+
return (
27+
<CheckboxGroupContext.Provider
28+
value={( { selectedValues, onCheckboxPress } )}
29+
>
30+
<Box ref={ref} {...props}>
31+
{children}
32+
</Box>
33+
</CheckboxGroupContext.Provider>
34+
);
35+
};
36+
37+
export default memo( forwardRef( CheckboxGroup ) );
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createContext, useContext } from 'react';
2+
import type { ICheckboxContext } from './types';
3+
4+
export const CheckboxGroupContext = createContext<ICheckboxContext | null>(
5+
null
6+
);
7+
8+
export const useCheckboxGroupContext = () => useContext( CheckboxGroupContext );
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as useCheckboxIcon } from './useCheckboxIcon';
2+
export { default as useCheckboxStateFromGroup } from './useCheckboxStateFromGroup';
3+
export { default as useCheckboxPropsResolver } from './useCheckboxPropsResolver';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
3+
import UIKitIcon from '../../../../icons/UIKitIcon';
4+
import Icon from '../../Icon';
5+
6+
const defaultCheckedIcon = <Icon as={UIKitIcon} name="box-checked" />;
7+
const defaultUncheckedIcon = <Icon as={UIKitIcon} name="box-unchecked" />;
8+
const defaultIndeterminatedIcon = <Icon as={UIKitIcon} name="box-indeterminated" />;
9+
10+
interface IUseCheckboxIconProps {
11+
selected: boolean,
12+
indeterminated: boolean,
13+
checkedIcon?: JSX.Element,
14+
uncheckedIcon?: JSX.Element,
15+
indeterminatedIcon?: JSX.Element
16+
}
17+
18+
const useCheckboxIcon = ( {
19+
selected,
20+
indeterminated,
21+
checkedIcon = defaultCheckedIcon,
22+
uncheckedIcon = defaultUncheckedIcon,
23+
indeterminatedIcon = defaultIndeterminatedIcon
24+
}: IUseCheckboxIconProps ) => {
25+
if ( indeterminated ) return indeterminatedIcon;
26+
if ( selected ) return checkedIcon;
27+
return uncheckedIcon;
28+
};
29+
30+
export default useCheckboxIcon;
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
import { useMemo } from 'react';
2-
import useIsFocused from '../../hooks/useIsFocused';
3-
import useIsHovered from '../../hooks/useIsHovered';
4-
import useIsPressed from '../../hooks/useIsPressed';
5-
import { useComponentPropsResolver } from '../../../hooks';
6-
import type { ICheckboxProps } from './types';
7-
import type { IIconProps } from '../Icon/types';
8-
import type { IBoxProps } from '../Box/types';
9-
import type { ITextProps } from '../Text/types';
2+
import { useIsFocused, useIsHovered, useIsPressed } from '../../../hooks';
3+
import { useComponentPropsResolver } from '../../../../hooks';
4+
import type { ICheckboxProps } from '../types';
5+
import type { IIconProps } from '../../Icon/types';
6+
import type { IBoxProps } from '../../Box/types';
7+
import type { ITextProps } from '../../Text/types';
8+
import useCheckboxStateFromGroup from './useCheckboxStateFromGroup';
9+
import useCheckboxIcon from './useCheckboxIcon';
1010

1111
interface IUseCheckboxPropsResolverReturnType {
12+
icon: JSX.Element,
1213
iconProps?: Omit<IIconProps, 'name'>,
1314
iconContainerProps?: Omit<IBoxProps, 'children'>,
1415
labelProps?: Omit<ITextProps, 'children'>,
1516
containerProps: Omit<ICheckboxProps, '__icon'>
1617
}
1718

18-
export const useCheckboxPropsResolver = ( {
19-
selected = false,
19+
const useCheckboxPropsResolver = ( {
20+
value,
21+
checkedIcon,
22+
uncheckedIcon,
23+
indeterminatedIcon,
24+
selected: selectedProp = false,
2025
indeterminated = false,
26+
onPress: onPressProp,
2127
...props
2228
} : ICheckboxProps
2329
): IUseCheckboxPropsResolverReturnType => {
@@ -26,6 +32,10 @@ export const useCheckboxPropsResolver = ( {
2632
const { isHovered, onHoverIn, onHoverOut } = useIsHovered( props );
2733
const { isFocused, onFocus, onBlur } = useIsFocused( props );
2834

35+
const groupState = useCheckboxStateFromGroup( value );
36+
const selected = groupState?.selected || selectedProp;
37+
const onPress = groupState?.onPress || onPressProp;
38+
2939
const state = useMemo( () => ( {
3040
isSelected: selected,
3141
isIndeterminated: indeterminated,
@@ -42,7 +52,15 @@ export const useCheckboxPropsResolver = ( {
4252
...containerProps
4353
} = useComponentPropsResolver( 'Checkbox', props, state ) as ICheckboxProps;
4454

45-
containerProps.onPress = props.onPress;
55+
const icon = useCheckboxIcon( {
56+
selected,
57+
indeterminated,
58+
checkedIcon,
59+
uncheckedIcon,
60+
indeterminatedIcon
61+
} );
62+
63+
containerProps.onPress = onPress;
4664
containerProps.onPressIn = onPressIn;
4765
containerProps.onPressOut = onPressOut;
4866
containerProps.onHoverIn = onHoverIn;
@@ -51,6 +69,12 @@ export const useCheckboxPropsResolver = ( {
5169
containerProps.onBlur = onBlur;
5270

5371
return {
54-
iconProps, iconContainerProps, labelProps, containerProps
72+
icon,
73+
iconProps,
74+
iconContainerProps,
75+
labelProps,
76+
containerProps
5577
};
5678
};
79+
80+
export default useCheckboxPropsResolver;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useCheckboxGroupContext } from '../context';
2+
3+
const useCheckboxStateFromGroup = ( value?: string ) => {
4+
const context = useCheckboxGroupContext();
5+
if ( !context ) { return null; }
6+
7+
if ( !value ) {
8+
throw new Error( 'A Checkbox inside a CheckboxGroup must have a value assgined.' );
9+
}
10+
11+
const { selectedValues, onCheckboxPress } = context;
12+
if ( !selectedValues && !onCheckboxPress ) { return null; }
13+
14+
return ( {
15+
selected: selectedValues?.has( value ),
16+
onPress: () => onCheckboxPress( value )
17+
} );
18+
};
19+
20+
export default useCheckboxStateFromGroup;
+5-80
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,8 @@
1-
import React, { cloneElement } from 'react';
2-
import UIKitIcon from '../../../icons/UIKitIcon';
3-
import Icon from '../Icon';
4-
import Text from '../Text';
5-
import Pressable from '../Pressable';
6-
import { HStack } from '../Stack';
7-
import { useCheckboxPropsResolver } from './hooks';
8-
import type { ICheckboxProps } from './types';
9-
import Box from '../Box';
1+
import CheckboxMain from './Checkbox';
2+
import CheckboxGroup from './CheckboxGroup';
3+
import type { ICheckboxComponentType, ICheckboxGroupComponentType } from './types';
104

11-
const defaultCheckedIcon = <Icon as={UIKitIcon} name="box-checked" />;
12-
13-
const defaultUncheckedIcon = <Icon as={UIKitIcon} name="box-unchecked" />;
14-
15-
const defaultIndeterminatedIcon = <Icon as={UIKitIcon} name="box-indeterminated" />;
16-
17-
const selectIcon = ( selected: boolean, indeterminated: boolean, checkedIcon: JSX.Element,
18-
uncheckedIcon: JSX.Element, indeterminatedIcon: JSX.Element ) => {
19-
if ( indeterminated ) return indeterminatedIcon;
20-
if ( selected ) return checkedIcon;
21-
return uncheckedIcon;
22-
};
23-
24-
const Checkbox = ( {
25-
label,
26-
selected = false,
27-
indeterminated = false,
28-
checkedIcon = defaultCheckedIcon,
29-
uncheckedIcon = defaultUncheckedIcon,
30-
indeterminatedIcon = defaultIndeterminatedIcon,
31-
testID,
32-
...props
33-
}: ICheckboxProps ) => {
34-
const {
35-
iconProps,
36-
iconContainerProps,
37-
labelProps,
38-
containerProps
39-
} = useCheckboxPropsResolver( {
40-
indeterminated, selected, ...props
41-
} );
42-
43-
const icon = selectIcon(
44-
selected,
45-
indeterminated,
46-
checkedIcon,
47-
uncheckedIcon,
48-
indeterminatedIcon );
49-
50-
return (
51-
<Pressable
52-
accessible
53-
accessibilityRole='checkbox'
54-
accessibilityLabel={label}
55-
accessibilityState={{
56-
checked: indeterminated ? 'mixed' : selected,
57-
disabled: props.disabled || false
58-
}}
59-
testID={testID}
60-
{...containerProps}
61-
>
62-
<HStack space="0.5" alignItems="center" alignContent="flex-start">
63-
<Box {...iconContainerProps}>
64-
{cloneElement( icon, {
65-
...iconProps,
66-
testID: testID && `${testID}-icon`
67-
} )}
68-
</Box>
69-
{!!label && (
70-
<Text
71-
{...labelProps}
72-
selectable={false}
73-
testID={testID && `${testID}-label`}
74-
>
75-
{label}
76-
</Text>
77-
)}
78-
</HStack>
79-
</Pressable>
80-
);
81-
};
5+
const Checkbox = CheckboxMain as ICheckboxComponentType;
6+
Checkbox.Group = CheckboxGroup as ICheckboxGroupComponentType;
827

838
export default Checkbox;

src/components/main/Checkbox/types.ts

+22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface ICheckboxProps extends Omit<PressableProps, 'children'>,
55
ComponentStyledProps<'Checkbox'>
66
{
77
label?: string,
8+
value?: string,
89

910
selected?: boolean,
1011
indeterminated?: boolean,
@@ -24,3 +25,24 @@ export interface ICheckboxProps extends Omit<PressableProps, 'children'>,
2425
onHoverIn?: ( ( event: GestureResponderEvent ) => void ),
2526
onHoverOut?: ( ( event: GestureResponderEvent ) => void ),
2627
}
28+
29+
export interface ICheckboxGroupProps {
30+
children?: JSX.Element | JSX.Element[] | string | any;
31+
value?: string[],
32+
onChange?: ( selectedValues: string[] ) => void
33+
}
34+
35+
export interface ICheckboxContext {
36+
selectedValues: Set<string>,
37+
onCheckboxPress: ( value: string ) => void
38+
}
39+
40+
export type ICheckboxGroupComponentType = (
41+
props: ICheckboxGroupProps
42+
) => JSX.Element;
43+
44+
export type ICheckboxComponentType = ( (
45+
props: ICheckboxProps
46+
) => JSX.Element ) & {
47+
Group: ICheckboxGroupComponentType
48+
};

0 commit comments

Comments
 (0)