import React from 'react' import {Pressable, PressableProps, View, ViewStyle} from 'react-native' import {useTheme, atoms as a, web} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' import {StyleProp} from 'react-native' type ItemState = { name: string selected: boolean disabled: boolean hasError: boolean hovered: boolean pressed: boolean focused: boolean } const ItemContext = React.createContext({ name: '', selected: false, disabled: false, hasError: false, hovered: false, pressed: false, focused: false, }) const GroupContext = React.createContext<{ values: string[] disabled: boolean type: 'radio' | 'checkbox' setFieldValue: (props: {name: string; value: boolean}) => void }>({ type: 'checkbox', values: [], disabled: false, setFieldValue: () => {}, }) export type GroupProps = React.PropsWithChildren<{ type?: 'radio' | 'checkbox' values: string[] maxSelections?: number disabled?: boolean onChange: (value: string[]) => void label: string style?: StyleProp }> export type ItemProps = Omit< PressableProps, 'children' | 'style' | 'onPress' | 'role' > & { type?: 'radio' | 'checkbox' name: string label: string value?: boolean onChange?: ({name, value}: {name: string; value: boolean}) => void hasError?: boolean style?: (state: ItemState) => ViewStyle children: ((props: ItemState) => React.ReactNode) | React.ReactNode } function Group({ children, values: initialValues, onChange, disabled = false, type = 'checkbox', maxSelections, style, label, }: GroupProps) { if (!initialValues) { throw new Error(`Don't forget to pass in 'values' to your Toggle.Group`) } const groupRole = type === 'radio' ? 'radiogroup' : undefined const [values, setValues] = React.useState( type === 'radio' ? initialValues.slice(0, 1) : initialValues, ) const [maxReached, setMaxReached] = React.useState(false) const setFieldValue = React.useCallback< Exclude >( ({name, value}) => { if (type === 'checkbox') { setValues(s => { const state = s.filter(v => v !== name) return value ? state.concat(name) : state }) } else { setValues([name]) } }, [type, setValues], ) React.useEffect(() => { onChange(values) }, [values, onChange]) React.useEffect(() => { if (type === 'checkbox') { if ( maxSelections && values.length >= maxSelections && maxReached === false ) { setMaxReached(true) } else if ( maxSelections && values.length < maxSelections && maxReached === true ) { setMaxReached(false) } } }, [type, values.length, maxSelections, maxReached, setMaxReached]) const context = React.useMemo( () => ({ values, type, disabled, setFieldValue, }), [values, disabled, type, setFieldValue], ) return ( {React.Children.map(children, child => { if (!React.isValidElement(child)) return null const isSelected = values.includes(child.props.name) let isDisabled = disabled || child.props.disabled if (maxReached && !isSelected) { isDisabled = true } return React.isValidElement(child) ? ( {React.cloneElement(child, { // @ts-ignore TODO figure out children types disabled: isDisabled, type: type === 'radio' ? 'radio' : 'checkbox', value: isSelected, onChange: setFieldValue, })} ) : null })} ) } function Item({ children, name, value = false, disabled: itemDisabled = false, onChange, hasError, style, type = 'checkbox', label, ...rest }: ItemProps) { // context can be empty if used outside a Group const { values: selectedValues, type: groupType, disabled: groupDisabled, setFieldValue, } = React.useContext(GroupContext) const { state: hovered, onIn: onHoverIn, onOut: onHoverOut, } = useInteractionState() const { state: pressed, onIn: onPressIn, onOut: onPressOut, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const role = groupType === 'radio' ? 'radio' : type const selected = selectedValues.includes(name) || !!value const disabled = groupDisabled || itemDisabled const onPress = React.useCallback(() => { const next = !value setFieldValue({name, value: next}) onChange?.({name, value: next}) // TODO don't use confusing method }, [name, value, onChange, setFieldValue]) const state = React.useMemo( () => ({ name, selected, disabled: disabled ?? false, hasError: hasError ?? false, hovered, pressed, focused, }), [name, selected, disabled, hovered, pressed, focused, hasError], ) return ( {typeof children === 'function' ? children(state) : children} ) } function Label({children}: React.PropsWithChildren<{}>) { const t = useTheme() const {disabled} = React.useContext(ItemContext) return ( {children} ) } function createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, hasError, }: { theme: ReturnType selected: boolean hovered: boolean focused: boolean disabled: boolean hasError: boolean }) { const base: ViewStyle[] = [] const baseHover: ViewStyle[] = [] const indicator: ViewStyle[] = [] if (selected) { base.push({ backgroundColor: t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900, borderColor: t.palette.primary_500, }) if (hovered || focused) { baseHover.push({ backgroundColor: t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800, borderColor: t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400, }) } } else { if (hovered || focused) { baseHover.push({ backgroundColor: t.palette.contrast_50, borderColor: t.palette.contrast_500, }) } } if (hasError) { base.push({ backgroundColor: t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, borderColor: t.name === 'light' ? t.palette.negative_300 : t.palette.negative_800, }) if (hovered || focused) { baseHover.push({ backgroundColor: t.name === 'light' ? t.palette.negative_25 : t.palette.negative_900, borderColor: t.palette.negative_500, }) } } if (disabled) { base.push({ backgroundColor: t.palette.contrast_200, borderColor: t.palette.contrast_300, }) } return { baseStyles: base, baseHoverStyles: baseHover, indicatorStyles: indicator, } } function Checkbox() { const t = useTheme() const {selected, hovered, focused, disabled, hasError} = React.useContext(ItemContext) const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, hasError, }) return ( {selected ? ( ) : null} ) } function Switch() { const t = useTheme() const {selected, hovered, focused, disabled, hasError} = React.useContext(ItemContext) const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, hasError, }) return ( ) } function Radio() { const t = useTheme() const {selected, hovered, focused, disabled, hasError} = React.useContext(ItemContext) const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, hasError, }) return ( {selected ? ( ) : null} ) } export default { Item, Checkbox, Label, Switch, Radio, Group, }