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