Context Menu
Displays a menu triggered by a right-click or long-press.
Installation
bash npx @moe/cli add context-menu Install the following dependency:
npx expo install @rn-primitives/context-menuCopy/paste the following code to @/components/ui/context-menu.tsx
import * as ContextMenuPrimitive from "@rn-primitives/context-menu";
import * as React from "react";
import { Platform, StyleSheet, View } from "react-native";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { Check } from "@/lib/icons/Check";
import { ChevronRight } from "@/lib/icons/ChevronRight";
import { cn } from "@/lib/utils";
import { TextClassContext } from "@/components/ui/text";
const ContextMenu = ContextMenuPrimitive.Root;
const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
const ContextMenuGroup = ContextMenuPrimitive.Group;
const ContextMenuSub = ContextMenuPrimitive.Sub;
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}
>(({ className, inset, children, ...props }, ref) => {
const { open } = ContextMenuPrimitive.useSubContext();
return (
<TextClassContext.Provider
value={cn("select-none text-sm native:text-lg text-primary")}
>
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex flex-row web:cursor-default web:select-none items-center gap-2 px-2 py-1.5 native:py-2 web:outline-none web:focus:bg-accent active:bg-accent web:hover:bg-accent rounded-sm",
open && "bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
<>{children}</>
<ChevronRight size={18} className="ml-auto text-foreground" />
</ContextMenuPrimitive.SubTrigger>
</TextClassContext.Provider>
);
});
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => {
return (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border mt-1 bg-popover p-1 shadow-md shadow-foreground/5",
className,
)}
{...props}
/>
);
});
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Overlay style={StyleSheet.absoluteFill}>
<Animated.View
entering={FadeIn.duration(200)}
exiting={FadeOut.duration(200)}
>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 shadow-md shadow-foreground/5",
className,
)}
{...props}
/>
</Animated.View>
</ContextMenuPrimitive.Overlay>
</ContextMenuPrimitive.Portal>
);
});
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<TextClassContext.Provider value="select-none text-sm native:text-lg text-popover-foreground web:group-focus:text-accent-foreground">
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex flex-row web:cursor-default items-center gap-2 rounded-sm px-2 py-1.5 native:py-2 web:outline-none web:focus:bg-accent active:bg-accent web:hover:bg-accent group",
inset && "pl-8",
props.disabled && "opacity-50 web:pointer-events-none",
className,
)}
{...props}
/>
</TextClassContext.Provider>
));
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex flex-row web:cursor-default items-center rounded-sm py-1.5 native:py-2 pl-8 pr-2 web:outline-none web:focus:bg-accent active:bg-accent group",
props.disabled && "web:pointer-events-none opacity-50",
className,
)}
checked={checked}
{...props}
>
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check size={14} strokeWidth={3} className="text-foreground" />
</ContextMenuPrimitive.ItemIndicator>
</View>
<>{children}</>
</ContextMenuPrimitive.CheckboxItem>
));
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName;
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex flex-row web:cursor-default items-center rounded-sm py-1.5 native:py-2 pl-8 pr-2 web:outline-none web:focus:bg-accent active:bg-accent group",
props.disabled && "web:pointer-events-none opacity-50",
className,
)}
{...props}
>
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<View className="h-2 w-2 rounded-full bg-foreground" />
</ContextMenuPrimitive.ItemIndicator>
</View>
<>{children}</>
</ContextMenuPrimitive.RadioItem>
));
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm native:text-base font-semibold text-foreground web:cursor-default",
inset && "pl-8",
className,
)}
{...props}
/>
));
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
));
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
const ContextMenuShortcut = ({
className,
...props
}: React.ComponentPropsWithoutRef<typeof View>) => {
return (
<View
className={cn("ml-auto flex flex-row items-center gap-1", className)}
{...props}
/>
);
};
ContextMenuShortcut.displayName = "ContextMenuShortcut";
export {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuGroup,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
};Usage
import { Text, View } from "react-native";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@moe/registry/ui/context-menu";
export function ContextMenuDemo() {
return (
<ContextMenu>
<ContextMenuTrigger>
<View className="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed">
<Text>Long press me</Text>
</View>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Settings</ContextMenuItem>
<ContextMenuItem>Logout</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}