1import { type ComponentProps, type ReactNode, useRef } from "react";
2import { Platform, Pressable, View } from "react-native";
3import { cn } from "../lib/cn";
4import { SymbolView } from "./AppSymbol";
5import { AppText as Text } from "./AppText";
6import { MaterialIconButton } from "./MaterialIconButton";
7import { MaterialButton } from "./MaterialButton";
8import { useAndroidControlSizing } from "./useAndroidControlSizing";
9
10export { ControlPillMenu } from "./ControlPillMenu";
11
12export function ControlPill(props: {
13 readonly icon?: ComponentProps<typeof SymbolView>["name"];
14 readonly iconNode?: ReactNode;
15 readonly label?: string;
16 readonly accessibilityLabel?: string;
17 readonly onPress?: () => void;
18 readonly activateOnPressIn?: boolean;
19 readonly variant?: "circle" | "pill" | "primary" | "danger";
20 readonly disabled?: boolean;
21 readonly className?: string;
22}) {
23 const variant = props.variant ?? "circle";
24 const { smallIconSize } = useAndroidControlSizing();
25 const activatedOnPressInRef = useRef(false);
26
27 const handlePressIn = () => {
28 activatedOnPressInRef.current = true;
29 props.onPress?.();
30 };
31 const handlePressOut = () => {
32 // Pressability invokes onPressOut immediately before onPress on release.
33 // Defer the reset so onPress can identify the same physical gesture.
34 setTimeout(() => {
35 activatedOnPressInRef.current = false;
36 }, 0);
37 };
38 const handlePress = () => {
39 if (activatedOnPressInRef.current) {
40 return;
41 }
42 props.onPress?.();
43 };
44
45 const iconTintClassName =
46 variant === "primary"
47 ? props.disabled
48 ? "accent-icon-subtle"
49 : "accent-primary-foreground"
50 : variant === "danger"
51 ? "accent-danger-foreground"
52 : "accent-icon";
53
54 const isCircle =
55 variant === "circle" || variant === "danger" || (variant === "primary" && !props.label);
56 const containerClassName = cn(
57 isCircle
58 ? "h-11 w-11 items-center justify-center rounded-full"
59 : variant === "primary"
60 ? "h-11 flex-row items-center justify-center gap-2 rounded-full px-5"
61 : "h-11 flex-row items-center justify-center gap-2 rounded-full px-3.5",
62 variant === "primary"
63 ? props.disabled
64 ? "bg-subtle-strong"
65 : "bg-primary"
66 : variant === "danger"
67 ? "bg-danger"
68 : "bg-subtle",
69 props.className,
70 );
71 const labelClassName = cn(
72 "text-center text-xs font-t3-bold",
73 variant === "primary"
74 ? props.disabled
75 ? "text-foreground-muted"
76 : "text-primary-foreground"
77 : variant === "danger"
78 ? "text-danger-foreground"
79 : "text-foreground",
80 );
81
82 if (
83 Platform.OS === "android" &&
84 (variant === "pill" || variant === "primary") &&
85 props.label &&
86 props.onPress &&
87 !props.icon &&
88 !props.iconNode &&
89 !props.className &&
90 !props.activateOnPressIn &&
91 (!props.accessibilityLabel || props.accessibilityLabel === props.label)
92 ) {
93 return (
94 <MaterialButton
95 label={props.label}
96 onPress={props.onPress}
97 disabled={props.disabled}
98 tone={variant === "primary" ? "primary" : "secondary"}
99 />
100 );
101 }
102
103 if (
104 Platform.OS === "android" &&
105 props.accessibilityLabel &&
106 props.icon &&
107 !props.iconNode &&
108 !props.label &&
109 !props.className &&
110 !props.activateOnPressIn
111 ) {
112 return (
113 <MaterialIconButton
114 accessibilityLabel={props.accessibilityLabel}
115 icon={props.icon}
116 onPress={props.onPress}
117 disabled={props.disabled}
118 variant={variant === "primary" ? "primary" : variant === "danger" ? "danger" : "tonal"}
119 />
120 );
121 }
122
123 return (
124 <Pressable
125 accessibilityLabel={props.accessibilityLabel ?? props.label}
126 accessibilityRole="button"
127 onPress={props.activateOnPressIn ? handlePress : props.onPress}
128 onPressIn={props.activateOnPressIn ? handlePressIn : undefined}
129 onPressOut={props.activateOnPressIn ? handlePressOut : undefined}
130 disabled={props.disabled}
131 className={containerClassName}
132 >
133 {props.iconNode ? (
134 <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View>
135 ) : props.icon ? (
136 <SymbolView
137 name={props.icon}
138 size={smallIconSize}
139 tintColorClassName={iconTintClassName}
140 type="monochrome"
141 />
142 ) : null}
143 {props.label ? <Text className={labelClassName}>{props.label}</Text> : null}
144 </Pressable>
145 );
146}