import React from 'react' import {Pressable, View, ViewStyle} from 'react-native' import {HITSLOP_10} from 'lib/constants' import {useTheme, atoms as a, web, native, flatten, ViewStyleProp} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' export type ItemState = { name: string selected: boolean disabled: boolean isInvalid: boolean hovered: boolean pressed: boolean focused: boolean } const ItemContext = React.createContext({ name: '', selected: false, disabled: false, isInvalid: false, hovered: false, pressed: false, focused: false, }) const GroupContext = React.createContext<{ values: string[] disabled: boolean type: 'radio' | 'checkbox' maxSelectionsReached: boolean setFieldValue: (props: {name: string; value: boolean}) => void }>({ type: 'checkbox', values: [], disabled: false, maxSelectionsReached: false, setFieldValue: () => {}, }) export type GroupProps = React.PropsWithChildren<{ type?: 'radio' | 'checkbox' values: string[] maxSelections?: number disabled?: boolean onChange: (value: string[]) => void label: string }> export type ItemProps = ViewStyleProp & { type?: 'radio' | 'checkbox' name: string label: string value?: boolean disabled?: boolean onChange?: (selected: boolean) => void isInvalid?: boolean children: ((props: ItemState) => React.ReactNode) | React.ReactNode } export function useItemContext() { return React.useContext(ItemContext) } export function Group({ children, values: providedValues, onChange, disabled = false, type = 'checkbox', maxSelections, label, }: GroupProps) { const groupRole = type === 'radio' ? 'radiogroup' : undefined const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues const [maxReached, setMaxReached] = React.useState(false) const setFieldValue = React.useCallback< (props: {name: string; value: boolean}) => void >( ({name, value}) => { if (type === 'checkbox') { const pruned = values.filter(v => v !== name) const next = value ? pruned.concat(name) : pruned onChange(next) } else { onChange([name]) } }, [type, onChange, values], ) 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, maxSelectionsReached: maxReached, setFieldValue, }), [values, disabled, type, maxReached, setFieldValue], ) return ( {children} ) } export function Item({ children, name, value = false, disabled: itemDisabled = false, onChange, isInvalid, style, type = 'checkbox', label, ...rest }: ItemProps) { const { values: selectedValues, type: groupType, disabled: groupDisabled, setFieldValue, maxSelectionsReached, } = 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 || (!selected && maxSelectionsReached) const onPress = React.useCallback(() => { const next = !selected setFieldValue({name, value: next}) onChange?.(next) }, [name, selected, onChange, setFieldValue]) const state = React.useMemo( () => ({ name, selected, disabled: disabled ?? false, isInvalid: isInvalid ?? false, hovered, pressed, focused, }), [name, selected, disabled, hovered, pressed, focused, isInvalid], ) return ( {typeof children === 'function' ? children(state) : children} ) } export function Label({children}: React.PropsWithChildren<{}>) { const t = useTheme() const {disabled} = useItemContext() return ( {children} ) } // TODO(eric) refactor to memoize styles without knowledge of state export function createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, isInvalid, }: { theme: ReturnType selected: boolean hovered: boolean focused: boolean disabled: boolean isInvalid: 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.name === 'light' ? t.palette.contrast_50 : t.palette.contrast_100, borderColor: t.palette.contrast_500, }) } } if (isInvalid) { 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_100, borderColor: t.palette.contrast_400, }) } return { baseStyles: base, baseHoverStyles: disabled ? [] : baseHover, indicatorStyles: indicator, } } export function Checkbox() { const t = useTheme() const {selected, hovered, focused, disabled, isInvalid} = useItemContext() const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, isInvalid, }) return ( {selected ? ( ) : null} ) } export function Switch() { const t = useTheme() const {selected, hovered, focused, disabled, isInvalid} = useItemContext() const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, isInvalid, }) return ( ) } export function Radio() { const t = useTheme() const {selected, hovered, focused, disabled, isInvalid} = React.useContext(ItemContext) const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, hovered, focused, selected, disabled, isInvalid, }) return ( {selected ? ( ) : null} ) }