mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at ruby-v 225 lines 5.9 kB view raw
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})