1import { lightTheme, darkTheme, borderRadius, shadows, palette } from '@expo/styleguide-native'; 2import * as React from 'react'; 3import { 4 Text as RNText, 5 Animated, 6 useColorScheme, 7 Pressable as RNPressable, 8 Easing, 9} from 'react-native'; 10 11import { create } from './create-primitive'; 12import { rounded, margin, padding, text } from './theme'; 13 14const AnimatedPressable = Animated.createAnimatedComponent(RNPressable); 15 16const Text = create(RNText, { 17 base: { 18 fontFamily: 'Inter-Regular', 19 color: lightTheme.text.default, 20 fontSize: 16, 21 lineHeight: 22, 22 }, 23 24 props: { 25 accessibilityRole: 'text', 26 }, 27 28 variants: { 29 ...text, 30 31 color: { 32 primary: { color: lightTheme.button.primary.foreground }, 33 secondary: { color: lightTheme.button.secondary.foreground }, 34 tertiary: { color: lightTheme.button.tertiary.foreground }, 35 ghost: { color: lightTheme.button.ghost.foreground }, 36 transparent: { color: lightTheme.button.transparent.foreground }, 37 default: { color: lightTheme.text.default }, 38 }, 39 }, 40 41 selectors: { 42 dark: { 43 base: { 44 color: darkTheme.text.default, 45 }, 46 47 color: { 48 primary: { color: darkTheme.button.primary.foreground }, 49 secondary: { color: darkTheme.button.secondary.foreground }, 50 tertiary: { color: darkTheme.button.tertiary.foreground }, 51 ghost: { color: darkTheme.button.ghost.foreground }, 52 transparent: { color: darkTheme.button.transparent.foreground }, 53 default: { color: darkTheme.text.default }, 54 }, 55 }, 56 }, 57}); 58 59const Container = create(AnimatedPressable, { 60 base: { 61 overflow: 'hidden', 62 borderRadius: borderRadius.medium, 63 }, 64 65 props: { 66 accessibilityRole: 'button', 67 android_disableSound: true, 68 }, 69 70 variants: { 71 bg: { 72 default: { backgroundColor: lightTheme.background.default }, 73 primary: { backgroundColor: lightTheme.button.primary.background }, 74 secondary: { backgroundColor: lightTheme.button.secondary.background }, 75 tertiary: { backgroundColor: lightTheme.button.tertiary.background }, 76 ghost: { backgroundColor: lightTheme.button.ghost.background }, 77 transparent: { backgroundColor: lightTheme.button.transparent.background }, 78 disabled: { backgroundColor: lightTheme.status.default }, 79 }, 80 81 border: { 82 ghost: { borderColor: lightTheme.button.ghost.border, borderWidth: 1 }, 83 }, 84 85 shadow: { 86 button: shadows.button, 87 }, 88 89 ...rounded, 90 ...padding, 91 ...margin, 92 }, 93 94 selectors: { 95 dark: { 96 bg: { 97 default: { backgroundColor: darkTheme.background.default }, 98 primary: { backgroundColor: darkTheme.button.primary.background }, 99 secondary: { backgroundColor: darkTheme.button.secondary.background }, 100 tertiary: { backgroundColor: darkTheme.button.tertiary.background }, 101 ghost: { backgroundColor: darkTheme.button.ghost.background }, 102 transparent: { backgroundColor: darkTheme.button.transparent.background }, 103 disabled: { backgroundColor: darkTheme.status.default }, 104 }, 105 }, 106 }, 107}); 108 109export const Button = { 110 Container, 111 FadeOnPressContainer, 112 HighlightOnPressContainer, 113 Text, 114}; 115 116type FadingPressableProps = { 117 activeOpacity?: number; 118}; 119 120type NoOptionals<T> = { 121 [P in keyof T]-?: T[P]; 122}; 123 124type ContainerProps = React.ComponentProps<typeof Container>; 125type ContainerBackgroundColors = NoOptionals<ContainerProps>['bg']; 126 127const lightHighlightColorMap: Record<ContainerBackgroundColors, string> = { 128 disabled: 'transparent', 129 default: lightTheme.background.secondary, 130 primary: lightTheme.background.tertiary, 131 secondary: lightTheme.background.quaternary, 132 tertiary: palette.light.gray[600], 133 ghost: lightTheme.background.tertiary, 134 transparent: lightTheme.background.secondary, 135}; 136 137const darkHighlightColorMap: Record<ContainerBackgroundColors, string> = { 138 disabled: 'transparent', 139 default: darkTheme.background.secondary, 140 primary: darkTheme.background.tertiary, 141 secondary: darkTheme.background.quaternary, 142 tertiary: palette.dark.gray[600], 143 ghost: darkTheme.background.tertiary, 144 transparent: darkTheme.background.secondary, 145}; 146 147const highlightColorMap = { 148 dark: darkHighlightColorMap, 149 light: lightHighlightColorMap, 150}; 151 152function FadeOnPressContainer({ 153 activeOpacity = 0.2, 154 ...props 155}: React.ComponentProps<typeof Container> & FadingPressableProps) { 156 const animatedValue = React.useRef(new Animated.Value(1)); 157 158 const onPressIn = React.useCallback(() => { 159 Animated.timing(animatedValue.current, { 160 toValue: activeOpacity, 161 duration: 150, 162 easing: Easing.inOut(Easing.quad), 163 useNativeDriver: true, 164 }).start(); 165 }, []); 166 167 const onPressOut = React.useCallback(() => { 168 Animated.timing(animatedValue.current, { 169 toValue: 1, 170 duration: 150, 171 easing: Easing.inOut(Easing.quad), 172 useNativeDriver: true, 173 }).start(); 174 }, []); 175 176 return ( 177 <Container 178 onPressIn={onPressIn} 179 onPressOut={onPressOut} 180 {...props} 181 style={{ 182 opacity: animatedValue.current, 183 }}> 184 {props.children} 185 </Container> 186 ); 187} 188 189function HighlightOnPressContainer(props: React.ComponentProps<typeof Container>) { 190 const theme = useColorScheme(); 191 const [isPressing, setIsPressing] = React.useState(false); 192 193 const onPressIn = React.useCallback(() => { 194 setIsPressing(true); 195 }, []); 196 197 const onPressOut = React.useCallback(() => { 198 setIsPressing(false); 199 }, []); 200 201 let backgroundColor = 'transparent'; 202 203 if (props.bg && isPressing && theme != null) { 204 backgroundColor = highlightColorMap[theme][props.bg]; 205 } 206 207 const underlayStyle = { 208 backgroundColor, 209 }; 210 211 return ( 212 <Container onPressIn={onPressIn} onPressOut={onPressOut} {...props}> 213 <Animated.View style={underlayStyle}>{props.children}</Animated.View> 214 </Container> 215 ); 216} 217