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