1import { SymbolView } from "./AppSymbol";
2import { NativeThemeHost } from "./NativeThemeHost.android";
3import {
4 Box,
5 Button as ComposeButton,
6 LoadingIndicator,
7 FilledTonalButton,
8 Row,
9 RNHostView,
10 Text,
11 TextButton,
12} from "@expo/ui/jetpack-compose";
13import { defaultMinSize, fillMaxWidth, size } from "@expo/ui/jetpack-compose/modifiers";
14import { View } from "react-native";
15
16import { useControlTheme } from "./useControlTheme";
17import { useScaledTextRole } from "../../features/settings/appearance/useScaledTextRole";
18import type { ButtonProps } from "./Button";
19import { useAndroidControlSizing } from "./useAndroidControlSizing";
20
21export function Button(props: ButtonProps) {
22 const { colors } = useControlTheme();
23 const typography = useScaledTextRole("footnote");
24 const { scale, mediumIconSize } = useAndroidControlSizing();
25 const tone = props.tone ?? "secondary";
26 const Component =
27 tone === "text" ? TextButton : tone === "secondary" ? FilledTonalButton : ComposeButton;
28 const containerColor =
29 tone === "contrast"
30 ? colors["--color-foreground"]
31 : tone === "primary"
32 ? colors["--color-primary"]
33 : tone === "danger"
34 ? colors["--color-danger"]
35 : tone === "text"
36 ? "#00000000"
37 : colors["--color-secondary"];
38 const contentColor =
39 tone === "contrast"
40 ? colors["--color-screen"]
41 : tone === "primary"
42 ? colors["--color-primary-foreground"]
43 : tone === "danger"
44 ? colors["--color-danger-foreground"]
45 : tone === "text"
46 ? colors["--color-primary-text"]
47 : colors["--color-secondary-foreground"];
48 return (
49 <View
50 accessible
51 accessibilityRole="button"
52 accessibilityLabel={props.accessibilityLabel ?? props.label}
53 accessibilityHint={props.accessibilityHint}
54 accessibilityState={{
55 disabled: Boolean(props.disabled || props.loading),
56 busy: Boolean(props.loading),
57 }}
58 accessibilityActions={[{ name: "activate" }]}
59 onAccessibilityAction={() => {
60 if (!props.disabled && !props.loading) props.onPress();
61 }}
62 style={props.fullWidth ? { width: "100%" } : { alignSelf: "flex-start" }}
63 >
64 <View importantForAccessibility="no-hide-descendants">
65 <NativeThemeHost
66 matchContents={props.fullWidth ? { vertical: true } : true}
67 ignoreSafeAreaKeyboardInsets
68 style={props.fullWidth ? { width: "100%" } : { alignSelf: "flex-start" }}
69 >
70 <Component
71 enabled={!props.disabled && !props.loading}
72 onClick={props.onPress}
73 modifiers={[
74 defaultMinSize({ minHeight: 48 }),
75 ...(props.fullWidth ? [fillMaxWidth()] : []),
76 ]}
77 colors={{
78 containerColor,
79 contentColor,
80 disabledContainerColor: colors["--color-subtle-strong"],
81 disabledContentColor: colors["--color-foreground-muted"],
82 }}
83 >
84 <Row verticalAlignment="center">
85 {props.loading ? (
86 <>
87 <LoadingIndicator
88 modifiers={[size(mediumIconSize, mediumIconSize)]}
89 color={colors["--color-foreground-muted"]}
90 />
91 <Box modifiers={[size(8 * scale, 1)]} />
92 </>
93 ) : props.icon ? (
94 <>
95 <RNHostView matchContents modifiers={[size(mediumIconSize, mediumIconSize)]}>
96 <View style={{ width: mediumIconSize, height: mediumIconSize }}>
97 <SymbolView
98 name={props.icon}
99 size={mediumIconSize}
100 tintColor={
101 props.disabled ? colors["--color-foreground-muted"] : contentColor
102 }
103 />
104 </View>
105 </RNHostView>
106 <Box modifiers={[size(8 * scale, 1)]} />
107 </>
108 ) : null}
109 <Text style={{ ...typography, fontWeight: "500" }}>{props.label}</Text>
110 </Row>
111 </Component>
112 </NativeThemeHost>
113 </View>
114 </View>
115 );
116}