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})