mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {
3 ActivityIndicator,
4 GestureResponderEvent,
5 NativeSyntheticEvent,
6 NativeTouchEvent,
7 Pressable,
8 PressableStateCallbackType,
9 StyleProp,
10 StyleSheet,
11 TextStyle,
12 View,
13 ViewStyle,
14} from 'react-native'
15
16import {choose} from '#/lib/functions'
17import {useTheme} from '#/lib/ThemeContext'
18import {Text} from '../text/Text'
19
20export type ButtonType =
21 | 'primary'
22 | 'secondary'
23 | 'default'
24 | 'inverted'
25 | 'primary-outline'
26 | 'secondary-outline'
27 | 'primary-light'
28 | 'secondary-light'
29 | 'default-light'
30
31// Augment type for react-native-web (see https://github.com/necolas/react-native-web/issues/1684#issuecomment-766451866)
32declare module 'react-native' {
33 interface PressableStateCallbackType {
34 // @ts-ignore web only
35 hovered?: boolean
36 focused?: boolean
37 }
38}
39
40// TODO: Enforce that button always has a label
41export function Button({
42 type = 'primary',
43 label,
44 style,
45 labelContainerStyle,
46 labelStyle,
47 onPress,
48 children,
49 testID,
50 accessibilityLabel,
51 accessibilityHint,
52 accessibilityLabelledBy,
53 onAccessibilityEscape,
54 withLoading = false,
55 disabled = false,
56}: React.PropsWithChildren<{
57 type?: ButtonType
58 label?: string
59 style?: StyleProp<ViewStyle>
60 labelContainerStyle?: StyleProp<ViewStyle>
61 labelStyle?: StyleProp<TextStyle>
62 onPress?: (e: NativeSyntheticEvent<NativeTouchEvent>) => void | Promise<void>
63 testID?: string
64 accessibilityLabel?: string
65 accessibilityHint?: string
66 accessibilityLabelledBy?: string
67 onAccessibilityEscape?: () => void
68 withLoading?: boolean
69 disabled?: boolean
70}>) {
71 const theme = useTheme()
72 const typeOuterStyle = choose<ViewStyle, Record<ButtonType, ViewStyle>>(
73 type,
74 {
75 primary: {
76 backgroundColor: theme.palette.primary.background,
77 },
78 secondary: {
79 backgroundColor: theme.palette.secondary.background,
80 },
81 default: {
82 backgroundColor: theme.palette.default.backgroundLight,
83 },
84 inverted: {
85 backgroundColor: theme.palette.inverted.background,
86 },
87 'primary-outline': {
88 backgroundColor: theme.palette.default.background,
89 borderWidth: 1,
90 borderColor: theme.palette.primary.border,
91 },
92 'secondary-outline': {
93 backgroundColor: theme.palette.default.background,
94 borderWidth: 1,
95 borderColor: theme.palette.secondary.border,
96 },
97 'primary-light': {
98 backgroundColor: theme.palette.default.background,
99 },
100 'secondary-light': {
101 backgroundColor: theme.palette.default.background,
102 },
103 'default-light': {
104 backgroundColor: theme.palette.default.background,
105 },
106 },
107 )
108 const typeLabelStyle = choose<TextStyle, Record<ButtonType, TextStyle>>(
109 type,
110 {
111 primary: {
112 color: theme.palette.primary.text,
113 fontWeight: '600',
114 },
115 secondary: {
116 color: theme.palette.secondary.text,
117 fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
118 },
119 default: {
120 color: theme.palette.default.text,
121 },
122 inverted: {
123 color: theme.palette.inverted.text,
124 fontWeight: '600',
125 },
126 'primary-outline': {
127 color: theme.palette.primary.textInverted,
128 fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
129 },
130 'secondary-outline': {
131 color: theme.palette.secondary.textInverted,
132 fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
133 },
134 'primary-light': {
135 color: theme.palette.primary.textInverted,
136 fontWeight: theme.palette.primary.isLowContrast ? '600' : undefined,
137 },
138 'secondary-light': {
139 color: theme.palette.secondary.textInverted,
140 fontWeight: theme.palette.secondary.isLowContrast ? '600' : undefined,
141 },
142 'default-light': {
143 color: theme.palette.default.text,
144 fontWeight: theme.palette.default.isLowContrast ? '600' : undefined,
145 },
146 },
147 )
148
149 const [isLoading, setIsLoading] = React.useState(false)
150 const onPressWrapped = React.useCallback(
151 async (event: GestureResponderEvent) => {
152 event.stopPropagation()
153 event.preventDefault()
154 withLoading && setIsLoading(true)
155 await onPress?.(event)
156 withLoading && setIsLoading(false)
157 },
158 [onPress, withLoading],
159 )
160
161 const getStyle = React.useCallback(
162 (state: PressableStateCallbackType) => {
163 const arr = [typeOuterStyle, styles.outer, style]
164 if (state.pressed) {
165 arr.push({opacity: 0.6})
166 } else if (state.hovered) {
167 arr.push({opacity: 0.8})
168 }
169 return arr
170 },
171 [typeOuterStyle, style],
172 )
173
174 const renderChildern = React.useCallback(() => {
175 if (!label) {
176 return children
177 }
178
179 return (
180 <View style={[styles.labelContainer, labelContainerStyle]}>
181 {label && withLoading && isLoading ? (
182 <ActivityIndicator size={12} color={typeLabelStyle.color} />
183 ) : null}
184 <Text type="button" style={[typeLabelStyle, labelStyle]}>
185 {label}
186 </Text>
187 </View>
188 )
189 }, [
190 children,
191 label,
192 withLoading,
193 isLoading,
194 labelContainerStyle,
195 typeLabelStyle,
196 labelStyle,
197 ])
198
199 return (
200 <Pressable
201 style={getStyle}
202 onPress={onPressWrapped}
203 disabled={disabled || isLoading}
204 testID={testID}
205 accessibilityRole="button"
206 accessibilityLabel={accessibilityLabel}
207 accessibilityHint={accessibilityHint}
208 accessibilityLabelledBy={accessibilityLabelledBy}
209 onAccessibilityEscape={onAccessibilityEscape}>
210 {renderChildern}
211 </Pressable>
212 )
213}
214
215const styles = StyleSheet.create({
216 outer: {
217 paddingHorizontal: 14,
218 paddingVertical: 8,
219 borderRadius: 24,
220 },
221 labelContainer: {
222 flexDirection: 'row',
223 gap: 8,
224 },
225})