mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {Pressable, View, ViewStyle} from 'react-native'
3import Animated, {LinearTransition} from 'react-native-reanimated'
4
5import {isNative} from '#/platform/detection'
6import {HITSLOP_10} from 'lib/constants'
7import {
8 atoms as a,
9 flatten,
10 native,
11 TextStyleProp,
12 useTheme,
13 ViewStyleProp,
14} from '#/alf'
15import {useInteractionState} from '#/components/hooks/useInteractionState'
16import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
17import {Text} from '#/components/Typography'
18
19export type ItemState = {
20 name: string
21 selected: boolean
22 disabled: boolean
23 isInvalid: boolean
24 hovered: boolean
25 pressed: boolean
26 focused: boolean
27}
28
29const ItemContext = React.createContext<ItemState>({
30 name: '',
31 selected: false,
32 disabled: false,
33 isInvalid: false,
34 hovered: false,
35 pressed: false,
36 focused: false,
37})
38
39const GroupContext = React.createContext<{
40 values: string[]
41 disabled: boolean
42 type: 'radio' | 'checkbox'
43 maxSelectionsReached: boolean
44 setFieldValue: (props: {name: string; value: boolean}) => void
45}>({
46 type: 'checkbox',
47 values: [],
48 disabled: false,
49 maxSelectionsReached: false,
50 setFieldValue: () => {},
51})
52
53export type GroupProps = React.PropsWithChildren<{
54 type?: 'radio' | 'checkbox'
55 values: string[]
56 maxSelections?: number
57 disabled?: boolean
58 onChange: (value: string[]) => void
59 label: string
60}>
61
62export type ItemProps = ViewStyleProp & {
63 type?: 'radio' | 'checkbox'
64 name: string
65 label: string
66 value?: boolean
67 disabled?: boolean
68 onChange?: (selected: boolean) => void
69 isInvalid?: boolean
70 children: ((props: ItemState) => React.ReactNode) | React.ReactNode
71}
72
73export function useItemContext() {
74 return React.useContext(ItemContext)
75}
76
77export function Group({
78 children,
79 values: providedValues,
80 onChange,
81 disabled = false,
82 type = 'checkbox',
83 maxSelections,
84 label,
85}: GroupProps) {
86 const groupRole = type === 'radio' ? 'radiogroup' : undefined
87 const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues
88 const [maxReached, setMaxReached] = React.useState(false)
89
90 const setFieldValue = React.useCallback<
91 (props: {name: string; value: boolean}) => void
92 >(
93 ({name, value}) => {
94 if (type === 'checkbox') {
95 const pruned = values.filter(v => v !== name)
96 const next = value ? pruned.concat(name) : pruned
97 onChange(next)
98 } else {
99 onChange([name])
100 }
101 },
102 [type, onChange, values],
103 )
104
105 React.useEffect(() => {
106 if (type === 'checkbox') {
107 if (
108 maxSelections &&
109 values.length >= maxSelections &&
110 maxReached === false
111 ) {
112 setMaxReached(true)
113 } else if (
114 maxSelections &&
115 values.length < maxSelections &&
116 maxReached === true
117 ) {
118 setMaxReached(false)
119 }
120 }
121 }, [type, values.length, maxSelections, maxReached, setMaxReached])
122
123 const context = React.useMemo(
124 () => ({
125 values,
126 type,
127 disabled,
128 maxSelectionsReached: maxReached,
129 setFieldValue,
130 }),
131 [values, disabled, type, maxReached, setFieldValue],
132 )
133
134 return (
135 <GroupContext.Provider value={context}>
136 <View
137 style={[a.w_full]}
138 role={groupRole}
139 {...(groupRole === 'radiogroup'
140 ? {
141 'aria-label': label,
142 accessibilityLabel: label,
143 accessibilityRole: groupRole,
144 }
145 : {})}>
146 {children}
147 </View>
148 </GroupContext.Provider>
149 )
150}
151
152export function Item({
153 children,
154 name,
155 value = false,
156 disabled: itemDisabled = false,
157 onChange,
158 isInvalid,
159 style,
160 type = 'checkbox',
161 label,
162 ...rest
163}: ItemProps) {
164 const {
165 values: selectedValues,
166 type: groupType,
167 disabled: groupDisabled,
168 setFieldValue,
169 maxSelectionsReached,
170 } = React.useContext(GroupContext)
171 const {
172 state: hovered,
173 onIn: onHoverIn,
174 onOut: onHoverOut,
175 } = useInteractionState()
176 const {
177 state: pressed,
178 onIn: onPressIn,
179 onOut: onPressOut,
180 } = useInteractionState()
181 const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
182
183 const role = groupType === 'radio' ? 'radio' : type
184 const selected = selectedValues.includes(name) || !!value
185 const disabled =
186 groupDisabled || itemDisabled || (!selected && maxSelectionsReached)
187
188 const onPress = React.useCallback(() => {
189 const next = !selected
190 setFieldValue({name, value: next})
191 onChange?.(next)
192 }, [name, selected, onChange, setFieldValue])
193
194 const state = React.useMemo(
195 () => ({
196 name,
197 selected,
198 disabled: disabled ?? false,
199 isInvalid: isInvalid ?? false,
200 hovered,
201 pressed,
202 focused,
203 }),
204 [name, selected, disabled, hovered, pressed, focused, isInvalid],
205 )
206
207 return (
208 <ItemContext.Provider value={state}>
209 <Pressable
210 accessibilityHint={undefined} // optional
211 hitSlop={HITSLOP_10}
212 {...rest}
213 disabled={disabled}
214 aria-disabled={disabled ?? false}
215 aria-checked={selected}
216 aria-invalid={isInvalid}
217 aria-label={label}
218 role={role}
219 accessibilityRole={role}
220 accessibilityState={{
221 disabled: disabled ?? false,
222 selected: selected,
223 }}
224 accessibilityLabel={label}
225 onPress={onPress}
226 onHoverIn={onHoverIn}
227 onHoverOut={onHoverOut}
228 onPressIn={onPressIn}
229 onPressOut={onPressOut}
230 onFocus={onFocus}
231 onBlur={onBlur}
232 style={[a.flex_row, a.align_center, a.gap_sm, flatten(style)]}>
233 {typeof children === 'function' ? children(state) : children}
234 </Pressable>
235 </ItemContext.Provider>
236 )
237}
238
239export function LabelText({
240 children,
241 style,
242}: React.PropsWithChildren<TextStyleProp>) {
243 const t = useTheme()
244 const {disabled} = useItemContext()
245 return (
246 <Text
247 style={[
248 a.font_bold,
249 a.leading_tight,
250 {
251 userSelect: 'none',
252 color: disabled
253 ? t.atoms.text_contrast_low.color
254 : t.atoms.text_contrast_high.color,
255 },
256 native({
257 paddingTop: 2,
258 }),
259 flatten(style),
260 ]}>
261 {children}
262 </Text>
263 )
264}
265
266// TODO(eric) refactor to memoize styles without knowledge of state
267export function createSharedToggleStyles({
268 theme: t,
269 hovered,
270 selected,
271 disabled,
272 isInvalid,
273}: {
274 theme: ReturnType<typeof useTheme>
275 selected: boolean
276 hovered: boolean
277 focused: boolean
278 disabled: boolean
279 isInvalid: boolean
280}) {
281 const base: ViewStyle[] = []
282 const baseHover: ViewStyle[] = []
283 const indicator: ViewStyle[] = []
284
285 if (selected) {
286 base.push({
287 backgroundColor: t.palette.primary_25,
288 borderColor: t.palette.primary_500,
289 })
290
291 if (hovered) {
292 baseHover.push({
293 backgroundColor: t.palette.primary_100,
294 borderColor: t.palette.primary_600,
295 })
296 }
297 } else {
298 if (hovered) {
299 baseHover.push({
300 backgroundColor: t.palette.contrast_50,
301 borderColor: t.palette.contrast_500,
302 })
303 }
304 }
305
306 if (isInvalid) {
307 base.push({
308 backgroundColor: t.palette.negative_25,
309 borderColor: t.palette.negative_300,
310 })
311
312 if (hovered) {
313 baseHover.push({
314 backgroundColor: t.palette.negative_25,
315 borderColor: t.palette.negative_600,
316 })
317 }
318 }
319
320 if (disabled) {
321 base.push({
322 backgroundColor: t.palette.contrast_100,
323 borderColor: t.palette.contrast_400,
324 })
325 }
326
327 return {
328 baseStyles: base,
329 baseHoverStyles: disabled ? [] : baseHover,
330 indicatorStyles: indicator,
331 }
332}
333
334export function Checkbox() {
335 const t = useTheme()
336 const {selected, hovered, focused, disabled, isInvalid} = useItemContext()
337 const {baseStyles, baseHoverStyles} = createSharedToggleStyles({
338 theme: t,
339 hovered,
340 focused,
341 selected,
342 disabled,
343 isInvalid,
344 })
345 return (
346 <View
347 style={[
348 a.justify_center,
349 a.align_center,
350 a.rounded_xs,
351 t.atoms.border_contrast_high,
352 {
353 borderWidth: 1,
354 height: 20,
355 width: 20,
356 },
357 baseStyles,
358 hovered ? baseHoverStyles : {},
359 ]}>
360 {selected ? <Checkmark size="xs" fill={t.palette.primary_500} /> : null}
361 </View>
362 )
363}
364
365export function Switch() {
366 const t = useTheme()
367 const {selected, hovered, focused, disabled, isInvalid} = useItemContext()
368 const {baseStyles, baseHoverStyles, indicatorStyles} =
369 createSharedToggleStyles({
370 theme: t,
371 hovered,
372 focused,
373 selected,
374 disabled,
375 isInvalid,
376 })
377 return (
378 <View
379 style={[
380 a.relative,
381 a.rounded_full,
382 t.atoms.bg,
383 t.atoms.border_contrast_high,
384 {
385 borderWidth: 1,
386 height: 20,
387 width: 32,
388 padding: 2,
389 },
390 baseStyles,
391 hovered ? baseHoverStyles : {},
392 ]}>
393 <Animated.View
394 layout={LinearTransition.duration(100)}
395 style={[
396 a.rounded_full,
397 {
398 height: 14,
399 width: 14,
400 },
401 selected
402 ? {
403 backgroundColor: t.palette.primary_500,
404 alignSelf: 'flex-end',
405 }
406 : {
407 backgroundColor: t.palette.contrast_400,
408 alignSelf: 'flex-start',
409 },
410 indicatorStyles,
411 ]}
412 />
413 </View>
414 )
415}
416
417export function Radio() {
418 const t = useTheme()
419 const {selected, hovered, focused, disabled, isInvalid} =
420 React.useContext(ItemContext)
421 const {baseStyles, baseHoverStyles, indicatorStyles} =
422 createSharedToggleStyles({
423 theme: t,
424 hovered,
425 focused,
426 selected,
427 disabled,
428 isInvalid,
429 })
430 return (
431 <View
432 style={[
433 a.justify_center,
434 a.align_center,
435 a.rounded_full,
436 t.atoms.border_contrast_high,
437 {
438 borderWidth: 1,
439 height: 20,
440 width: 20,
441 },
442 baseStyles,
443 hovered ? baseHoverStyles : {},
444 ]}>
445 {selected ? (
446 <View
447 style={[
448 a.absolute,
449 a.rounded_full,
450 {height: 12, width: 12},
451 selected
452 ? {
453 backgroundColor: t.palette.primary_500,
454 }
455 : {},
456 indicatorStyles,
457 ]}
458 />
459 ) : null}
460 </View>
461 )
462}
463
464export const Platform = isNative ? Switch : Checkbox