1import {
2 Box,
3 ExtendedFloatingActionButton,
4 FloatingActionButton,
5 Host,
6 LargeFloatingActionButton,
7 Text,
8} from "@expo/ui/jetpack-compose";
9import { defaultMinSize, height, size, width } from "@expo/ui/jetpack-compose/modifiers";
10import { View, type StyleProp, type ViewStyle } from "react-native";
11import { useAndroidControlSizing } from "./useAndroidControlSizing";
12import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
13import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
14import { SymbolView, type AppSymbolName } from "./AppSymbol";
15
16export function MaterialFloatingActionButton(props: {
17 readonly onPress: () => void;
18 readonly label: string;
19 readonly icon: AppSymbolName;
20 readonly variant?: "extended" | "large";
21 readonly expanded?: boolean;
22 readonly tone?: "primary" | "secondary";
23 readonly className?: string;
24 readonly style?: StyleProp<ViewStyle>;
25}) {
26 const { themeAppearance, themeVariables: colors } = useAppearancePreferences();
27 const typography = useScaledTextRole("footnote");
28 const { scale, iconSize: standardIconSize, fabSize, largeFabSize } = useAndroidControlSizing();
29 const buttonSize = props.variant === "large" ? largeFabSize : fabSize;
30 const primary = props.tone === "primary";
31 const containerColor = colors[primary ? "--color-primary" : "--color-secondary"];
32 const contentColor =
33 colors[primary ? "--color-primary-foreground" : "--color-secondary-foreground"];
34 const Component =
35 props.variant === "extended"
36 ? ExtendedFloatingActionButton
37 : props.variant === "large"
38 ? LargeFloatingActionButton
39 : FloatingActionButton;
40 const iconSize = props.variant === "large" ? Math.round(36 * scale) : standardIconSize;
41 return (
42 <View
43 accessible
44 accessibilityRole="button"
45 accessibilityLabel={props.label}
46 accessibilityActions={[{ name: "activate" }]}
47 onAccessibilityAction={props.onPress}
48 className={props.className}
49 style={props.style}
50 >
51 <View importantForAccessibility="no-hide-descendants">
52 <Host matchContents colorScheme={themeAppearance} ignoreSafeAreaKeyboardInsets>
53 <Component
54 modifiers={[
55 defaultMinSize({ minWidth: buttonSize }),
56 height(buttonSize),
57 ...(props.variant === "extended" ? [] : [width(buttonSize)]),
58 ]}
59 containerColor={containerColor}
60 onClick={props.onPress}
61 expanded={props.expanded}
62 >
63 <Component.Icon>
64 <Box modifiers={[size(iconSize, iconSize)]} />
65 </Component.Icon>
66 {props.variant === "extended" ? (
67 <ExtendedFloatingActionButton.Text>
68 <Text color={contentColor} style={{ ...typography, fontWeight: "500" }}>
69 {props.label}
70 </Text>
71 </ExtendedFloatingActionButton.Text>
72 ) : null}
73 </Component>
74 </Host>
75 </View>
76 {/* The RN icon stays outside Compose so it cannot intercept native button taps. */}
77 <View
78 pointerEvents="none"
79 className="absolute inset-y-0 justify-center"
80 style={
81 props.variant === "extended" ? { left: 16 } : { left: 0, right: 0, alignItems: "center" }
82 }
83 >
84 <SymbolView
85 name={props.icon}
86 size={iconSize}
87 tintColorClassName={primary ? "accent-primary-foreground" : "accent-secondary-foreground"}
88 />
89 </View>
90 </View>
91 );
92}