Open History to see the last commit
1import {
2 Box,
3 DropdownMenu,
4 Icon,
5 LoadingIndicator,
6 RNHostView,
7 Row,
8 SplitButtonLayout,
9 SplitButtonLeadingButton,
10 SplitButtonTrailingButton,
11 Text,
12} from "@expo/ui/jetpack-compose";
13import {
14 defaultMinSize,
15 fillMaxWidth,
16 rotate,
17 semantics,
18 size,
19} from "@expo/ui/jetpack-compose/modifiers";
20import { useState } from "react";
21import { Pressable } from "react-native";
22
23import { useScaledTextRole } from "../../features/settings/appearance/useScaledTextRole";
24import { SymbolView } from "./AppSymbol";
25import { Button } from "./Button";
26import { MenuPopupItems } from "./MenuPopup.android";
27import { NativeThemeHost } from "./NativeThemeHost.android";
28import type { SplitButtonProps } from "./SplitButton.types";
29import { useAndroidControlSizing } from "./useAndroidControlSizing";
30import { useControlTheme } from "./useControlTheme";
31
32export function SplitButton(props: SplitButtonProps) {
33 const { colors } = useControlTheme();
34 const typography = useScaledTextRole("footnote");
35 const { scale, iconSize, mediumIconSize, menuWidth } = useAndroidControlSizing();
36 const [expanded, setExpanded] = useState(false);
37 if (props.actions.length === 0) return <Button {...props} tone="primary" fullWidth />;
38 const enabled = !props.disabled && !props.loading;
39 const menuEnabled = props.actions.some((action) => !action.attributes?.disabled);
40 const buttonColors = {
41 containerColor: colors["--color-primary"],
42 contentColor: colors["--color-primary-foreground"],
43 disabledContainerColor: colors["--color-subtle-strong"],
44 disabledContentColor: colors["--color-foreground-muted"],
45 };
46 // A usable menu beside a disabled action still takes the disabled palette, so the pair reads as one control.
47 const trailingColors = enabled
48 ? buttonColors
49 : {
50 ...buttonColors,
51 containerColor: buttonColors.disabledContainerColor,
52 contentColor: buttonColors.disabledContentColor,
53 };
54 const label = (
55 <Row verticalAlignment="center">
56 {props.loading ? (
57 <>
58 <LoadingIndicator
59 modifiers={[size(mediumIconSize, mediumIconSize)]}
60 color={colors["--color-foreground-muted"]}
61 />
62 <Box modifiers={[size(8 * scale, 1)]} />
63 </>
64 ) : props.icon ? (
65 <>
66 {/* The glyph host consumes touches, so it runs the action itself. */}
67 <RNHostView matchContents modifiers={[size(mediumIconSize, mediumIconSize)]}>
68 <Pressable
69 accessible={false}
70 disabled={!enabled}
71 onPress={props.onPress}
72 importantForAccessibility="no-hide-descendants"
73 style={{ width: mediumIconSize, height: mediumIconSize }}
74 >
75 <SymbolView
76 name={props.icon}
77 size={mediumIconSize}
78 tintColor={enabled ? buttonColors.contentColor : buttonColors.disabledContentColor}
79 />
80 </Pressable>
81 </RNHostView>
82 <Box modifiers={[size(8 * scale, 1)]} />
83 </>
84 ) : null}
85 <Text style={{ ...typography, fontWeight: "500" }}>{props.label}</Text>
86 </Row>
87 );
88 return (
89 <NativeThemeHost
90 matchContents={{ vertical: true }}
91 ignoreSafeAreaKeyboardInsets
92 style={{ width: "100%" }}
93 >
94 <SplitButtonLayout modifiers={[fillMaxWidth()]}>
95 <SplitButtonLayout.LeadingButton>
96 <SplitButtonLeadingButton
97 enabled={enabled}
98 onClick={props.onPress}
99 colors={buttonColors}
100 modifiers={[
101 fillMaxWidth(),
102 defaultMinSize({ minHeight: 48 }),
103 semantics({ contentDescription: props.accessibilityLabel ?? props.label }),
104 ]}
105 >
106 {label}
107 </SplitButtonLeadingButton>
108 </SplitButtonLayout.LeadingButton>
109 <SplitButtonLayout.TrailingButton>
110 <SplitButtonTrailingButton
111 checked={expanded}
112 onCheckedChange={setExpanded}
113 enabled={menuEnabled}
114 colors={trailingColors}
115 modifiers={[
116 defaultMinSize({ minWidth: 48, minHeight: 48 }),
117 semantics({ contentDescription: props.menuTitle }),
118 ]}
119 >
120 <DropdownMenu
121 expanded={expanded && menuEnabled}
122 onDismissRequest={() => setExpanded(false)}
123 color={colors["--color-card-alt"]}
124 >
125 <DropdownMenu.Trigger>
126 <Icon
127 source={require("../../../assets/icons/chevron-down.xml")}
128 size={iconSize}
129 modifiers={[rotate(expanded ? 180 : 0)]}
130 />
131 </DropdownMenu.Trigger>
132 <DropdownMenu.Items>
133 <MenuPopupItems
134 menuWidth={menuWidth}
135 actions={props.actions}
136 title={props.menuTitle}
137 parent={null}
138 onPress={(action) => {
139 setExpanded(false);
140 if (action.id !== undefined) props.onPressAction(action.id);
141 }}
142 onBack={() => {}}
143 />
144 </DropdownMenu.Items>
145 </DropdownMenu>
146 </SplitButtonTrailingButton>
147 </SplitButtonLayout.TrailingButton>
148 </SplitButtonLayout>
149 </NativeThemeHost>
150 );
151}