mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at samuel/fancy-queue 224 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 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})