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