Skip to content
Source
Sign inSign up

ControlPill.tsx

143 lines4.2 KB

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