From 541e62514d5dbd6070b81b48a81461daaf2c974f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 13 Jan 2024 17:43:20 -0600 Subject: [PATCH] Toggles --- src/alf/atoms.ts | 6 + src/alf/tokens.ts | 4 +- src/components/forms/Toggle.tsx | 399 +++++++++++++++++++++++++++ src/view/screens/Storybook/Forms.tsx | 282 ++++++++++++------- 4 files changed, 592 insertions(+), 99 deletions(-) create mode 100644 src/components/forms/Toggle.tsx diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index b3b06efccf..d2dafac7eb 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -48,6 +48,12 @@ export const atoms = { /* * Border radius */ + rounded_2xs: { + borderRadius: tokens.borderRadius._2xs, + }, + rounded_xs: { + borderRadius: tokens.borderRadius.xs, + }, rounded_sm: { borderRadius: tokens.borderRadius.sm, }, diff --git a/src/alf/tokens.ts b/src/alf/tokens.ts index db402840f8..ff8a8ebb8b 100644 --- a/src/alf/tokens.ts +++ b/src/alf/tokens.ts @@ -16,7 +16,7 @@ export const color = { blue_25: `#F5FAFF`, blue_50: `#EFF8FF`, - blue_100: `#EFF8FF`, + blue_100: `#D1E9FF`, blue_200: `#B2DDFF`, blue_300: `#84CAFF`, blue_400: `#53B1FD`, @@ -84,6 +84,8 @@ export const lineHeight = { } as const export const borderRadius = { + _2xs: 2, + xs: 4, sm: 8, md: 12, full: 999, diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx new file mode 100644 index 0000000000..e3cfa1a4cc --- /dev/null +++ b/src/components/forms/Toggle.tsx @@ -0,0 +1,399 @@ +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 + value: boolean + disabled: boolean + hovered: boolean + pressed: boolean + focused: boolean +} + +const ItemContext = React.createContext({ + name: '', + value: false, + disabled: false, + hovered: false, + pressed: false, + focused: false, +}) + +const GroupContext = React.createContext<{ + values: string[] + disabled: boolean + role?: 'radio' | 'checkbox' +}>({ + values: [], + disabled: false, + role: 'checkbox', +}) + +type ItemProps = Omit & { + name: string + value?: boolean + onChange?: ({name, value}: {name: string; value: boolean}) => void + style?: (state: ItemState) => ViewStyle + children: ((props: ItemState) => React.ReactNode) | React.ReactNode +} + +function Item({ + children, + name, + value = false, + disabled, + onChange, + style, +}: ItemProps) { + 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 onPress = React.useCallback(() => { + const next = !value + onChange?.({name, value: next}) + }, [name, value, onChange]) + + const state = React.useMemo( + () => ({ + name, + value, + disabled: disabled ?? false, + hovered, + pressed, + focused, + }), + [name, value, disabled, hovered, pressed, focused], + ) + + return ( + + + {typeof children === 'function' ? children(state) : children} + + + ) +} + +function Group({ + children, + values: initialValues, + onChange, + disabled, + role = 'checkbox', + maxSelections, + style, +}: React.PropsWithChildren<{ + values: string[] + onChange: (value: string[]) => 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, + value: isSelected, + onChange: itemOnChange, + })} + + ) : null + })} + + + ) +} + +function Label({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + const {disabled} = React.useContext(ItemContext) + return ( + + {children} + + ) +} + +function createSharedToggleStyles({ + theme: t, + hovered, + focused, + value, + disabled, +}: { + theme: ReturnType + value: boolean + hovered: boolean + focused: boolean + disabled: boolean +}) { + return [ + hovered || focused + ? { + backgroundColor: t.palette.contrast_50, + borderColor: t.palette.contrast_500, + } + : {}, + value + ? { + backgroundColor: + t.name === 'light' ? t.palette.primary_25 : t.palette.primary_900, + borderColor: t.palette.primary_500, + } + : {}, + value && (hovered || focused) + ? { + backgroundColor: + t.name === 'light' ? t.palette.primary_100 : t.palette.primary_800, + borderColor: + t.name === 'light' ? t.palette.primary_600 : t.palette.primary_400, + } + : {}, + disabled + ? { + backgroundColor: t.palette.contrast_200, + borderColor: t.palette.contrast_300, + } + : {}, + ] +} + +function Checkbox() { + const t = useTheme() + const {value, hovered, focused, disabled} = React.useContext(ItemContext) + return ( + + {value ? ( + + ) : null} + + ) +} + +function Switch() { + const t = useTheme() + const {value, hovered, focused, disabled} = React.useContext(ItemContext) + return ( + + + + ) +} + +function Radio() { + const t = useTheme() + const {value, hovered, focused, disabled} = React.useContext(ItemContext) + return ( + + {value ? ( + + ) : null} + + ) +} + +export default { + Item, + Checkbox, + Label, + Switch, + Radio, + Group, +} diff --git a/src/view/screens/Storybook/Forms.tsx b/src/view/screens/Storybook/Forms.tsx index fa687d786d..a6513641be 100644 --- a/src/view/screens/Storybook/Forms.tsx +++ b/src/view/screens/Storybook/Forms.tsx @@ -2,113 +2,199 @@ import React from 'react' import {View} from 'react-native' import {atoms as a} from '#/alf' -import {Text} from '#/components/Typography' +import {Text, H1, H3} from '#/components/Typography' import {InputText} from '#/components/forms/InputText' import {InputDate, utils} from '#/components/forms/InputDate' import {InputGroup} from '#/components/forms/InputGroup' import {Logo} from '#/view/icons/Logo' +import Toggle from '#/components/forms/Toggle' export function Forms() { return ( - - console.log(text)} - /> - console.log(text)} - /> - console.log(text)} - icon={Logo} - /> - console.log(text)} - icon={Logo} - /> - console.log(text)} - icon={Logo} - suffix={() => .bksy.social} - /> - console.log(text)} - /> + +

Forms

- - console.log(text)} - /> - console.log(text)} - /> - console.log(text)} - /> - + +

InputText

- console.log(date)} - accessibilityLabel="Date" - accessibilityHint="Enter a date" - /> - console.log(date)} - accessibilityLabel="Date" - accessibilityHint="Enter a date" - /> + console.log(text)} + /> + console.log(text)} + /> + console.log(text)} + icon={Logo} + /> + console.log(text)} + icon={Logo} + /> + console.log(text)} + icon={Logo} + suffix={() => .bksy.social} + /> + console.log(text)} + /> + +

InputDate

+ console.log(date)} + accessibilityLabel="Date" + accessibilityHint="Enter a date" + /> + console.log(date)} + accessibilityLabel="Date" + accessibilityHint="Enter a date" + /> +
+ + +

InputGroup (WIP)

+ + console.log(text)} + /> + console.log(text)} + /> + console.log(text)} + /> + +
+ + +

Toggles

+ + console.log(e)} + style={[a.gap_sm]}> + + + Click me + + + + Click me + + + + Click me + + + + Click me + + + + console.log(e)} + style={[a.gap_sm]}> + + + Click me + + + + Click me + + + + Click me + + + + Click me + + + + console.log(e)} + style={[a.gap_sm]}> + + + Click me + + + + Click me + + + + Click me + + + + Click me + + +
) }