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