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