141 lines · 4.5 KB · TypeScript
RawDownload
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;  return (    <MenuView {...menuProps} themeVariant={isDarkMode ? "dark" : "light"}>      {menuProps.children}    </MenuView>  );}