diff --git a/src/alf/themes.ts b/src/alf/themes.ts index dc45944563..08aee75512 100644 --- a/src/alf/themes.ts +++ b/src/alf/themes.ts @@ -152,7 +152,7 @@ export const light = { backgroundColor: lightPalette.contrast_300, }, border: { - borderColor: lightPalette.contrast_100, + borderColor: lightPalette.contrast_200, }, border_contrast: { borderColor: lightPalette.contrast_400, diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index 875ccbbed5..0b07c88987 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -5,12 +5,10 @@ import {useTheme, atoms as a, web} from '#/alf' import {Text} from '#/components/Typography' import {useInteractionState} from '#/components/hooks/useInteractionState' import {StyleProp} from 'react-native' -import {AccessibilityRole} from 'react-native' type ItemState = { - id: string name: string - value: boolean + selected: boolean disabled: boolean hasError: boolean hovered: boolean @@ -19,9 +17,8 @@ type ItemState = { } const ItemContext = React.createContext({ - id: '', name: '', - value: false, + selected: false, disabled: false, hasError: false, hovered: false, @@ -32,15 +29,32 @@ const ItemContext = React.createContext({ const GroupContext = React.createContext<{ values: string[] disabled: boolean - role?: 'radio' | 'checkbox' + type: 'radio' | 'checkbox' + setFieldValue: (props: {name: string; value: boolean}) => void }>({ + type: 'checkbox', values: [], disabled: false, - role: 'checkbox', + setFieldValue: () => {}, }) -type ItemProps = Omit & { +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 @@ -48,17 +62,132 @@ type ItemProps = Omit & { 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, + disabled: itemDisabled = false, onChange, hasError, style, - role, + type = 'checkbox', + label, + ...rest }: ItemProps) { - const labelId = React.useId() + // 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, @@ -71,39 +200,46 @@ function Item({ } = 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 - onChange?.({name, value: next}) - }, [name, value, onChange]) + setFieldValue({name, value: next}) + onChange?.({name, value: next}) // TODO don't use confusing method + }, [name, value, onChange, setFieldValue]) const state = React.useMemo( () => ({ - id: labelId, name, - value, + selected, disabled: disabled ?? false, hasError: hasError ?? false, hovered, pressed, focused, }), - [labelId, name, value, disabled, hovered, pressed, focused, hasError], + [name, selected, disabled, hovered, pressed, focused, hasError], ) return ( void - disabled?: boolean - role?: 'radio' | 'checkbox' - maxSelections?: number - style?: StyleProp -}>) { - const _disabled = disabled ?? false - const [values, setValues] = React.useState( - role === 'radio' ? initialValues.slice(0, 1) : initialValues, - ) - const [maxReached, setMaxReached] = React.useState(false) - - const itemOnChange = React.useCallback< - Exclude - >( - ({name, value}) => { - if (role === 'checkbox') { - setValues(s => { - const state = s.filter(v => v !== name) - return value ? state.concat(name) : state - }) - } else { - setValues([name]) - } - }, - [role, setValues], - ) - - React.useEffect(() => { - onChange(values) - }, [values, onChange]) - - React.useEffect(() => { - if (role === 'checkbox') { - if ( - maxSelections && - values.length >= maxSelections && - maxReached === false - ) { - setMaxReached(true) - } else if ( - maxSelections && - values.length < maxSelections && - maxReached === true - ) { - setMaxReached(false) - } - } - }, [role, values.length, maxSelections, maxReached, setMaxReached]) - - 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, - role: role === 'radio' ? 'radio' : 'checkbox', - value: isSelected, - onChange: itemOnChange, - })} - - ) : null - })} - - - ) -} - function Label({children}: React.PropsWithChildren<{}>) { const t = useTheme() - const {id, disabled} = React.useContext(ItemContext) + const {disabled} = React.useContext(ItemContext) return ( - value: boolean + selected: boolean hovered: boolean focused: boolean disabled: boolean @@ -251,7 +296,7 @@ function createSharedToggleStyles({ const baseHover: ViewStyle[] = [] const indicator: ViewStyle[] = [] - if (value) { + if (selected) { base.push({ backgroundColor: t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900, @@ -308,14 +353,14 @@ function createSharedToggleStyles({ function Checkbox() { const t = useTheme() - const {value, hovered, focused, disabled, hasError} = + const {selected, hovered, focused, disabled, hasError} = React.useContext(ItemContext) const {baseStyles, baseHoverStyles, indicatorStyles} = createSharedToggleStyles({ theme: t, hovered, focused, - value, + selected, disabled, hasError, }) @@ -330,19 +375,19 @@ function Checkbox() { { height: 20, width: 20, - backgroundColor: value ? t.palette.primary_500 : undefined, - borderColor: value ? t.palette.primary_500 : undefined, + backgroundColor: selected ? t.palette.primary_500 : undefined, + borderColor: selected ? t.palette.primary_500 : undefined, }, baseStyles, hovered || focused ? baseHoverStyles : {}, ]}> - {value ? ( + {selected ? ( - {value ? ( + {selected ? ( Toggles console.log(e)} style={[a.gap_sm]}> - + Click me - + Click me - + Click me - + Click me - + Click me console.log(e)} style={[a.gap_sm]}> - + Click me - + Click me - + Click me - + Click me - + Click me console.log(e)} style={[a.gap_sm]}> - + Click me - + Click me - + Click me - + Click me - + Click me + + +

ToggleButton

+ + console.log(e)}> + + Hide + + + Warn + + + Show + + +
) }