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