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