mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at rn-stack-repro 564 lines 15 kB view raw
1import React from 'react' 2import { 3 Pressable, 4 Text, 5 PressableProps, 6 TextProps, 7 ViewStyle, 8 AccessibilityProps, 9 View, 10 TextStyle, 11 StyleSheet, 12 StyleProp, 13} from 'react-native' 14import LinearGradient from 'react-native-linear-gradient' 15 16import {useTheme, atoms as a, tokens, android, flatten} from '#/alf' 17import {Props as SVGIconProps} from '#/components/icons/common' 18 19export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'gradient' 20export type ButtonColor = 21 | 'primary' 22 | 'secondary' 23 | 'negative' 24 | 'gradient_sky' 25 | 'gradient_midnight' 26 | 'gradient_sunrise' 27 | 'gradient_sunset' 28 | 'gradient_nordic' 29 | 'gradient_bonfire' 30export type ButtonSize = 'tiny' | 'small' | 'large' 31export type ButtonShape = 'round' | 'square' | 'default' 32export type VariantProps = { 33 /** 34 * The style variation of the button 35 */ 36 variant?: ButtonVariant 37 /** 38 * The color of the button 39 */ 40 color?: ButtonColor 41 /** 42 * The size of the button 43 */ 44 size?: ButtonSize 45 /** 46 * The shape of the button 47 */ 48 shape?: ButtonShape 49} 50 51export type ButtonState = { 52 hovered: boolean 53 focused: boolean 54 pressed: boolean 55 disabled: boolean 56} 57 58export type ButtonContext = VariantProps & ButtonState 59 60export type ButtonProps = Pick< 61 PressableProps, 62 'disabled' | 'onPress' | 'testID' 63> & 64 AccessibilityProps & 65 VariantProps & { 66 testID?: string 67 label: string 68 style?: StyleProp<ViewStyle> 69 children: 70 | React.ReactNode 71 | string 72 | ((context: ButtonContext) => React.ReactNode | string) 73 } 74export type ButtonTextProps = TextProps & VariantProps & {disabled?: boolean} 75 76const Context = React.createContext<VariantProps & ButtonState>({ 77 hovered: false, 78 focused: false, 79 pressed: false, 80 disabled: false, 81}) 82 83export function useButtonContext() { 84 return React.useContext(Context) 85} 86 87export function Button({ 88 children, 89 variant, 90 color, 91 size, 92 shape = 'default', 93 label, 94 disabled = false, 95 style, 96 ...rest 97}: ButtonProps) { 98 const t = useTheme() 99 const [state, setState] = React.useState({ 100 pressed: false, 101 hovered: false, 102 focused: false, 103 }) 104 105 const onPressIn = React.useCallback(() => { 106 setState(s => ({ 107 ...s, 108 pressed: true, 109 })) 110 }, [setState]) 111 const onPressOut = React.useCallback(() => { 112 setState(s => ({ 113 ...s, 114 pressed: false, 115 })) 116 }, [setState]) 117 const onHoverIn = React.useCallback(() => { 118 setState(s => ({ 119 ...s, 120 hovered: true, 121 })) 122 }, [setState]) 123 const onHoverOut = React.useCallback(() => { 124 setState(s => ({ 125 ...s, 126 hovered: false, 127 })) 128 }, [setState]) 129 const onFocus = React.useCallback(() => { 130 setState(s => ({ 131 ...s, 132 focused: true, 133 })) 134 }, [setState]) 135 const onBlur = React.useCallback(() => { 136 setState(s => ({ 137 ...s, 138 focused: false, 139 })) 140 }, [setState]) 141 142 const {baseStyles, hoverStyles, focusStyles} = React.useMemo(() => { 143 const baseStyles: ViewStyle[] = [] 144 const hoverStyles: ViewStyle[] = [] 145 const light = t.name === 'light' 146 147 if (color === 'primary') { 148 if (variant === 'solid') { 149 if (!disabled) { 150 baseStyles.push({ 151 backgroundColor: t.palette.primary_500, 152 }) 153 hoverStyles.push({ 154 backgroundColor: t.palette.primary_600, 155 }) 156 } else { 157 baseStyles.push({ 158 backgroundColor: t.palette.primary_700, 159 }) 160 } 161 } else if (variant === 'outline') { 162 baseStyles.push(a.border, t.atoms.bg, { 163 borderWidth: 1, 164 }) 165 166 if (!disabled) { 167 baseStyles.push(a.border, { 168 borderColor: t.palette.primary_500, 169 }) 170 hoverStyles.push(a.border, { 171 backgroundColor: light 172 ? t.palette.primary_50 173 : t.palette.primary_950, 174 }) 175 } else { 176 baseStyles.push(a.border, { 177 borderColor: light ? t.palette.primary_200 : t.palette.primary_900, 178 }) 179 } 180 } else if (variant === 'ghost') { 181 if (!disabled) { 182 baseStyles.push(t.atoms.bg) 183 hoverStyles.push({ 184 backgroundColor: light 185 ? t.palette.primary_100 186 : t.palette.primary_900, 187 }) 188 } 189 } 190 } else if (color === 'secondary') { 191 if (variant === 'solid') { 192 if (!disabled) { 193 baseStyles.push({ 194 backgroundColor: t.palette.contrast_50, 195 }) 196 hoverStyles.push({ 197 backgroundColor: t.palette.contrast_100, 198 }) 199 } else { 200 baseStyles.push({ 201 backgroundColor: t.palette.contrast_200, 202 }) 203 } 204 } else if (variant === 'outline') { 205 baseStyles.push(a.border, t.atoms.bg, { 206 borderWidth: 1, 207 }) 208 209 if (!disabled) { 210 baseStyles.push(a.border, { 211 borderColor: t.palette.contrast_300, 212 }) 213 hoverStyles.push(t.atoms.bg_contrast_50) 214 } else { 215 baseStyles.push(a.border, { 216 borderColor: t.palette.contrast_200, 217 }) 218 } 219 } else if (variant === 'ghost') { 220 if (!disabled) { 221 baseStyles.push(t.atoms.bg) 222 hoverStyles.push({ 223 backgroundColor: t.palette.contrast_100, 224 }) 225 } 226 } 227 } else if (color === 'negative') { 228 if (variant === 'solid') { 229 if (!disabled) { 230 baseStyles.push({ 231 backgroundColor: t.palette.negative_500, 232 }) 233 hoverStyles.push({ 234 backgroundColor: t.palette.negative_600, 235 }) 236 } else { 237 baseStyles.push({ 238 backgroundColor: t.palette.negative_700, 239 }) 240 } 241 } else if (variant === 'outline') { 242 baseStyles.push(a.border, t.atoms.bg, { 243 borderWidth: 1, 244 }) 245 246 if (!disabled) { 247 baseStyles.push(a.border, { 248 borderColor: t.palette.negative_500, 249 }) 250 hoverStyles.push(a.border, { 251 backgroundColor: light 252 ? t.palette.negative_50 253 : t.palette.negative_975, 254 }) 255 } else { 256 baseStyles.push(a.border, { 257 borderColor: light 258 ? t.palette.negative_200 259 : t.palette.negative_900, 260 }) 261 } 262 } else if (variant === 'ghost') { 263 if (!disabled) { 264 baseStyles.push(t.atoms.bg) 265 hoverStyles.push({ 266 backgroundColor: light 267 ? t.palette.negative_100 268 : t.palette.negative_975, 269 }) 270 } 271 } 272 } 273 274 if (shape === 'default') { 275 if (size === 'large') { 276 baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) 277 } else if (size === 'small') { 278 baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) 279 } else if (size === 'tiny') { 280 baseStyles.push({paddingVertical: 4}, a.px_sm, a.rounded_xs, a.gap_xs) 281 } 282 } else if (shape === 'round' || shape === 'square') { 283 if (size === 'large') { 284 if (shape === 'round') { 285 baseStyles.push({height: 54, width: 54}) 286 } else { 287 baseStyles.push({height: 50, width: 50}) 288 } 289 } else if (size === 'small') { 290 baseStyles.push({height: 40, width: 40}) 291 } else if (size === 'tiny') { 292 baseStyles.push({height: 20, width: 20}) 293 } 294 295 if (shape === 'round') { 296 baseStyles.push(a.rounded_full) 297 } else if (shape === 'square') { 298 if (size === 'tiny') { 299 baseStyles.push(a.rounded_xs) 300 } else { 301 baseStyles.push(a.rounded_sm) 302 } 303 } 304 } 305 306 return { 307 baseStyles, 308 hoverStyles, 309 focusStyles: [ 310 ...hoverStyles, 311 { 312 outline: 0, 313 } as ViewStyle, 314 ], 315 } 316 }, [t, variant, color, size, shape, disabled]) 317 318 const {gradientColors, gradientHoverColors, gradientLocations} = 319 React.useMemo(() => { 320 const colors: string[] = [] 321 const hoverColors: string[] = [] 322 const locations: number[] = [] 323 const gradient = { 324 primary: tokens.gradients.sky, 325 secondary: tokens.gradients.sky, 326 negative: tokens.gradients.sky, 327 gradient_sky: tokens.gradients.sky, 328 gradient_midnight: tokens.gradients.midnight, 329 gradient_sunrise: tokens.gradients.sunrise, 330 gradient_sunset: tokens.gradients.sunset, 331 gradient_nordic: tokens.gradients.nordic, 332 gradient_bonfire: tokens.gradients.bonfire, 333 }[color || 'primary'] 334 335 if (variant === 'gradient') { 336 colors.push(...gradient.values.map(([_, color]) => color)) 337 hoverColors.push(...gradient.values.map(_ => gradient.hover_value)) 338 locations.push(...gradient.values.map(([location, _]) => location)) 339 } 340 341 return { 342 gradientColors: colors, 343 gradientHoverColors: hoverColors, 344 gradientLocations: locations, 345 } 346 }, [variant, color]) 347 348 const context = React.useMemo<ButtonContext>( 349 () => ({ 350 ...state, 351 variant, 352 color, 353 size, 354 disabled: disabled || false, 355 }), 356 [state, variant, color, size, disabled], 357 ) 358 359 const flattenedBaseStyles = flatten(baseStyles) 360 361 return ( 362 <Pressable 363 role="button" 364 accessibilityHint={undefined} // optional 365 {...rest} 366 aria-label={label} 367 aria-pressed={state.pressed} 368 accessibilityLabel={label} 369 disabled={disabled || false} 370 accessibilityState={{ 371 disabled: disabled || false, 372 }} 373 style={[ 374 a.flex_row, 375 a.align_center, 376 a.justify_center, 377 a.justify_center, 378 flattenedBaseStyles, 379 ...(state.hovered || state.pressed ? hoverStyles : []), 380 ...(state.focused ? focusStyles : []), 381 flatten(style), 382 ]} 383 onPressIn={onPressIn} 384 onPressOut={onPressOut} 385 onHoverIn={onHoverIn} 386 onHoverOut={onHoverOut} 387 onFocus={onFocus} 388 onBlur={onBlur}> 389 {variant === 'gradient' && ( 390 <View 391 style={[ 392 a.absolute, 393 a.inset_0, 394 a.overflow_hidden, 395 {borderRadius: flattenedBaseStyles.borderRadius}, 396 ]}> 397 <LinearGradient 398 colors={ 399 state.hovered || state.pressed || state.focused 400 ? gradientHoverColors 401 : gradientColors 402 } 403 locations={gradientLocations} 404 start={{x: 0, y: 0}} 405 end={{x: 1, y: 1}} 406 style={[a.absolute, a.inset_0]} 407 /> 408 </View> 409 )} 410 <Context.Provider value={context}> 411 {typeof children === 'string' ? ( 412 <ButtonText>{children}</ButtonText> 413 ) : typeof children === 'function' ? ( 414 children(context) 415 ) : ( 416 children 417 )} 418 </Context.Provider> 419 </Pressable> 420 ) 421} 422 423export function useSharedButtonTextStyles() { 424 const t = useTheme() 425 const {color, variant, disabled, size} = useButtonContext() 426 return React.useMemo(() => { 427 const baseStyles: TextStyle[] = [] 428 const light = t.name === 'light' 429 430 if (color === 'primary') { 431 if (variant === 'solid') { 432 if (!disabled) { 433 baseStyles.push({color: t.palette.white}) 434 } else { 435 baseStyles.push({color: t.palette.white, opacity: 0.5}) 436 } 437 } else if (variant === 'outline') { 438 if (!disabled) { 439 baseStyles.push({ 440 color: light ? t.palette.primary_600 : t.palette.primary_500, 441 }) 442 } else { 443 baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) 444 } 445 } else if (variant === 'ghost') { 446 if (!disabled) { 447 baseStyles.push({color: t.palette.primary_600}) 448 } else { 449 baseStyles.push({color: t.palette.primary_600, opacity: 0.5}) 450 } 451 } 452 } else if (color === 'secondary') { 453 if (variant === 'solid' || variant === 'gradient') { 454 if (!disabled) { 455 baseStyles.push({ 456 color: t.palette.contrast_700, 457 }) 458 } else { 459 baseStyles.push({ 460 color: t.palette.contrast_400, 461 }) 462 } 463 } else if (variant === 'outline') { 464 if (!disabled) { 465 baseStyles.push({ 466 color: t.palette.contrast_600, 467 }) 468 } else { 469 baseStyles.push({ 470 color: t.palette.contrast_300, 471 }) 472 } 473 } else if (variant === 'ghost') { 474 if (!disabled) { 475 baseStyles.push({ 476 color: t.palette.contrast_600, 477 }) 478 } else { 479 baseStyles.push({ 480 color: t.palette.contrast_300, 481 }) 482 } 483 } 484 } else if (color === 'negative') { 485 if (variant === 'solid' || variant === 'gradient') { 486 if (!disabled) { 487 baseStyles.push({color: t.palette.white}) 488 } else { 489 baseStyles.push({color: t.palette.white, opacity: 0.5}) 490 } 491 } else if (variant === 'outline') { 492 if (!disabled) { 493 baseStyles.push({color: t.palette.negative_400}) 494 } else { 495 baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 496 } 497 } else if (variant === 'ghost') { 498 if (!disabled) { 499 baseStyles.push({color: t.palette.negative_400}) 500 } else { 501 baseStyles.push({color: t.palette.negative_400, opacity: 0.5}) 502 } 503 } 504 } else { 505 if (!disabled) { 506 baseStyles.push({color: t.palette.white}) 507 } else { 508 baseStyles.push({color: t.palette.white, opacity: 0.5}) 509 } 510 } 511 512 if (size === 'large') { 513 baseStyles.push(a.text_md, android({paddingBottom: 1})) 514 } else if (size === 'tiny') { 515 baseStyles.push(a.text_xs, android({paddingBottom: 1})) 516 } else { 517 baseStyles.push(a.text_sm, android({paddingBottom: 1})) 518 } 519 520 return StyleSheet.flatten(baseStyles) 521 }, [t, variant, color, size, disabled]) 522} 523 524export function ButtonText({children, style, ...rest}: ButtonTextProps) { 525 const textStyles = useSharedButtonTextStyles() 526 527 return ( 528 <Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}> 529 {children} 530 </Text> 531 ) 532} 533 534export function ButtonIcon({ 535 icon: Comp, 536 position, 537 size: iconSize, 538}: { 539 icon: React.ComponentType<SVGIconProps> 540 position?: 'left' | 'right' 541 size?: SVGIconProps['size'] 542}) { 543 const {size, disabled} = useButtonContext() 544 const textStyles = useSharedButtonTextStyles() 545 546 return ( 547 <View 548 style={[ 549 a.z_20, 550 { 551 opacity: disabled ? 0.7 : 1, 552 marginLeft: position === 'left' ? -2 : 0, 553 marginRight: position === 'right' ? -2 : 0, 554 }, 555 ]}> 556 <Comp 557 size={ 558 iconSize ?? (size === 'large' ? 'md' : size === 'tiny' ? 'xs' : 'sm') 559 } 560 style={[{color: textStyles.color, pointerEvents: 'none'}]} 561 /> 562 </View> 563 ) 564}