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