forked from pingdotgg/t3code
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathControlPill.tsx
More file actions
157 lines (149 loc) · 5.35 KB
/
Copy pathControlPill.tsx
File metadata and controls
157 lines (149 loc) · 5.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import { MenuView } from "@react-native-menu/menu";
import * as Haptics from "expo-haptics";
import {
cloneElement,
isValidElement,
type ComponentProps,
type ReactElement,
type ReactNode,
} from "react";
import { Platform, Pressable, useColorScheme, View } from "react-native";
import { useThemeColor } from "../lib/useThemeColor";
import { cn } from "../lib/cn";
import { AndroidAnchoredMenu } from "./AndroidAnchoredMenu";
import { SymbolView } from "./AppSymbol";
import { AppText as Text } from "./AppText";
export function ControlPill(props: {
readonly icon?: ComponentProps<typeof SymbolView>["name"];
readonly iconNode?: ReactNode;
readonly label?: string;
readonly accessibilityLabel?: string;
readonly onPress?: () => void;
readonly variant?: "circle" | "pill" | "primary" | "danger";
readonly disabled?: boolean;
}) {
const variant = props.variant ?? "circle";
const iconColor = useThemeColor("--color-icon");
const iconSubtle = useThemeColor("--color-icon-subtle");
const primaryFg = useThemeColor("--color-primary-foreground");
const dangerFg = useThemeColor("--color-danger-foreground");
const iconTintColor =
variant === "primary"
? props.disabled
? iconSubtle
: primaryFg
: variant === "danger"
? dangerFg
: iconColor;
const isCircle =
variant === "circle" || variant === "danger" || (variant === "primary" && !props.label);
const containerClassName = cn(
isCircle
? "h-11 w-11 items-center justify-center rounded-full"
: variant === "primary"
? "h-11 flex-row items-center justify-center gap-2 rounded-full px-5"
: "h-11 flex-row items-center justify-center gap-2 rounded-full px-3.5",
variant === "primary"
? props.disabled
? "bg-subtle-strong"
: "bg-primary"
: variant === "danger"
? "bg-danger"
: "bg-subtle",
);
const labelClassName = cn(
"text-center text-xs font-t3-bold",
variant === "primary"
? props.disabled
? "text-foreground-muted"
: "text-primary-foreground"
: "",
);
return (
<Pressable
accessibilityLabel={props.accessibilityLabel ?? props.label}
accessibilityRole="button"
onPress={props.onPress}
disabled={props.disabled}
className={containerClassName}
>
{props.iconNode ? (
<View className="h-4 w-4 items-center justify-center">{props.iconNode}</View>
) : props.icon ? (
<SymbolView name={props.icon} size={16} tintColor={iconTintColor} type="monochrome" />
) : null}
{props.label ? <Text className={labelClassName}>{props.label}</Text> : null}
</Pressable>
);
}
// iOS renders the native UIMenu (standard checkmark for `state: "on"`);
// Android renders the token-styled AndroidAnchoredMenu, since the native
// AppCompat popup can't be themed past its stock animation, metrics, and
// submenu chrome.
export function ControlPillMenu(
props: Omit<ComponentProps<typeof MenuView>, "children" | "themeVariant"> & {
readonly children: ReactNode;
readonly className?: string;
},
) {
const isDarkMode = useColorScheme() === "dark";
if (Platform.OS === "android") {
// Long-press menus keep their child interactive: the child element gets
// an injected onLongPress (mirroring the iOS context-menu interaction)
// so its own tap handling still works.
if (props.shouldOpenOnLongPress && isValidElement(props.children)) {
const child = props.children as ReactElement<{ onLongPress?: () => void }>;
return (
<AndroidAnchoredMenu
actions={props.actions}
className={props.className}
title={props.title}
style={props.style}
onPressAction={props.onPressAction}
>
{(open) =>
cloneElement(child, {
onLongPress: () => {
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
open();
},
})
}
</AndroidAnchoredMenu>
);
}
return (
<AndroidAnchoredMenu
actions={props.actions}
className={props.className}
title={props.title}
style={props.style}
onPressAction={props.onPressAction}
>
{props.children}
</AndroidAnchoredMenu>
);
}
const { className: _className, ...menuProps } = props;
let children = menuProps.children;
// In long-press mode the wrapped pressable still receives the touch (the
// patched MenuView button is touch-transparent) and RN's Fabric touch
// handler is never cancelled by the in-tree UIContextMenuInteraction, so a
// bare onPress would fire on finger-up even after the menu opened — and
// also on a long press released just under the menu threshold. A dispatched
// onLongPress makes Pressability swallow the release, so holds past 350ms
// (below the ~500ms context-menu threshold) can only open the menu, never
// tap through.
if (props.shouldOpenOnLongPress && isValidElement(children)) {
const child = children as ReactElement<{ onLongPress?: () => void; delayLongPress?: number }>;
children = cloneElement(child, {
onLongPress: child.props.onLongPress ?? (() => undefined),
delayLongPress: child.props.delayLongPress ?? 350,
});
}
return (
<MenuView {...menuProps} themeVariant={isDarkMode ? "dark" : "light"}>
{children}
</MenuView>
);
}