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