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