Context Menu Primitive
Shows a menu activated by either a right-click or a long-press.
Installation
Install the component via your command line.
npx expo install @rn-primitives/context-menu
Install @radix-ui/react-context-menu
npx expo install @radix-ui/react-context-menu
Copy/paste the following code for web to ~/components/primitives/context-menu/context-menu.web.tsx
import * as ContextMenu from '@radix-ui/react-context-menu';import { useAugmentedRef, useControllableState, useIsomorphicLayoutEffect,} from '~/components/primitives/hooks';import * as Slot from '~/components/primitives/slot';import { EmptyGestureResponderEvent } from '~/components/primitives/utils';import * as React from 'react';import { GestureResponderEvent, Pressable, Text, View } from 'react-native';import type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,} from './types';
const ContextMenuContext = React.createContext<{ open: boolean; onOpenChange: (open: boolean) => void;} | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, onOpenChange: onOpenChangeProp, ...viewProps }, ref) => { const [open, setOpen] = React.useState(false);
function onOpenChange(value: boolean) { setOpen(value); onOpenChangeProp?.(value); }
const Component = asChild ? Slot.View : View; return ( <ContextMenuContext.Provider value={{ open, onOpenChange }}> <ContextMenu.Root onOpenChange={onOpenChange}> <Component ref={ref} {...viewProps} /> </ContextMenu.Root> </ContextMenuContext.Provider> ); });
Root.displayName = 'RootWebContextMenu';
function useRootContext() { const context = React.useContext(ContextMenuContext); if (!context) { throw new Error( 'ContextMenu compound components cannot be rendered outside the ContextMenu component' ); } return context;}
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ({ asChild, disabled = false, ...props }, ref) => { const { open } = useRootContext(); const augmentedRef = useAugmentedRef({ ref, methods: { open() { console.warn('Warning: `open()` is only for Native platforms'); }, close() { console.warn('Warning: `close()` is only for Native platforms'); }, }, });
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLDivElement; augRef.dataset.state = open ? 'open' : 'closed'; } }, [open]);
useIsomorphicLayoutEffect(() => { if (augmentedRef.current) { const augRef = augmentedRef.current as unknown as HTMLDivElement; if (disabled) { augRef.dataset.disabled = 'true'; } else { augRef.dataset.disabled = undefined; } } }, [disabled]);
const Component = asChild ? Slot.Pressable : Pressable; return ( <ContextMenu.Trigger disabled={disabled ?? undefined} asChild> <Component ref={augmentedRef} disabled={disabled} {...props} /> </ContextMenu.Trigger> ); });
Trigger.displayName = 'TriggerWebContextMenu';
function Portal({ forceMount, container, children }: PortalProps) { return <ContextMenu.Portal forceMount={forceMount} container={container} children={children} />;}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} {...props} />;});
Overlay.displayName = 'OverlayWebContextMenu';
const ContextMenuContentContext = React.createContext<{ close: () => void;} | null>(null);
const Content = React.forwardRef<ContentRef, ContentProps>( ( { asChild = false, forceMount, align: _align, side: _side, sideOffset: _sideOffset, alignOffset = 0, avoidCollisions = true, insets, loop = true, onCloseAutoFocus, onEscapeKeyDown, onPointerDownOutside, onFocusOutside, onInteractOutside, collisionBoundary, sticky, hideWhenDetached, ...props }, ref ) => { const itemRef = React.useRef<HTMLDivElement>(null);
function close() { itemRef.current?.click(); }
const Component = asChild ? Slot.View : View; return ( <ContextMenuContentContext.Provider value={{ close }}> <ContextMenu.Content forceMount={forceMount} alignOffset={alignOffset} avoidCollisions={avoidCollisions} collisionPadding={insets} loop={loop} onCloseAutoFocus={onCloseAutoFocus} onEscapeKeyDown={onEscapeKeyDown} onPointerDownOutside={onPointerDownOutside} onFocusOutside={onFocusOutside} onInteractOutside={onInteractOutside} collisionBoundary={collisionBoundary} sticky={sticky} hideWhenDetached={hideWhenDetached} > <Component ref={ref} {...props} /> <ContextMenu.Item ref={itemRef} aria-hidden style={{ position: 'fixed', top: 0, left: 0, zIndex: -999999999 }} aria-disabled tabIndex={-1} hidden /> </ContextMenu.Content> </ContextMenuContentContext.Provider> ); });
Content.displayName = 'ContentWebContextMenu';
function useContextMenuContentContext() { const context = React.useContext(ContextMenuContentContext); if (!context) { throw new Error( 'ContextMenu compound components cannot be rendered outside the ContextMenu component' ); } return context;}
const Item = React.forwardRef<ItemRef, ItemProps>( ({ asChild, textValue, closeOnPress = true, onPress: onPressProp, ...props }, ref) => { const { close } = useContextMenuContentContext();
function onKeyDown(ev: React.KeyboardEvent) { if (ev.key === 'Enter' || ev.key === ' ') { onPressProp?.(EmptyGestureResponderEvent); if (closeOnPress) { close(); } } }
function onPress(ev: GestureResponderEvent) { onPressProp?.(ev); if (closeOnPress) { close(); } }
const Component = asChild ? Slot.Pressable : Pressable; return ( <ContextMenu.Item textValue={textValue} disabled={props.disabled ?? undefined} onSelect={closeOnPress ? undefined : onSelected} asChild > <Component ref={ref} role='button' onPress={onPress} onKeyDown={onKeyDown} {...props} /> </ContextMenu.Item> ); });
Item.displayName = 'ItemWebContextMenu';
const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <ContextMenu.Group asChild> <Component ref={ref} {...props} /> </ContextMenu.Group> );});
Group.displayName = 'GroupWebContextMenu';
const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return ( <ContextMenu.Label asChild> <Component ref={ref} {...props} /> </ContextMenu.Label> );});
Label.displayName = 'LabelWebContextMenu';
const CheckboxItem = React.forwardRef<CheckboxItemRef, CheckboxItemProps>( ( { asChild, checked, onCheckedChange, textValue, disabled = false, closeOnPress = true, onPress: onPressProp, onKeyDown: onKeyDownProp, ...props }, ref ) => { const { close } = useContextMenuContentContext();
function onKeyDown(ev: React.KeyboardEvent) { onKeyDownProp?.(ev); if (ev.key === 'Enter' || ev.key === ' ') { onPressProp?.(EmptyGestureResponderEvent); onCheckedChange?.(!checked); if (closeOnPress) { close(); } } }
function onPress(ev: GestureResponderEvent) { onPressProp?.(ev); onCheckedChange?.(!checked); if (closeOnPress) { close(); } }
const Component = asChild ? Slot.Pressable : Pressable; return ( <ContextMenu.CheckboxItem textValue={textValue} checked={checked} onCheckedChange={onCheckedChange} onSelect={closeOnPress ? undefined : onSelected} disabled={disabled ?? undefined} asChild > <Component ref={ref} disabled={disabled} // @ts-expect-error web only onKeyDown={onKeyDown} onPress={onPress} role='button' {...props} /> </ContextMenu.CheckboxItem> ); });
CheckboxItem.displayName = 'CheckboxItemWebContextMenu';
const ContextMenuRadioGroupContext = React.createContext<{ value?: string; onValueChange?: (value: string) => void;} | null>(null);
const RadioGroup = React.forwardRef<RadioGroupRef, RadioGroupProps>( ({ asChild, value, onValueChange, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <ContextMenuRadioGroupContext.Provider value={{ value, onValueChange }}> <ContextMenu.RadioGroup value={value} onValueChange={onValueChange} asChild> <Component ref={ref} {...props} /> </ContextMenu.RadioGroup> </ContextMenuRadioGroupContext.Provider> ); });
RadioGroup.displayName = 'RadioGroupWebContextMenu';
function useContextMenuRadioGroupContext() { const context = React.useContext(ContextMenuRadioGroupContext); if (!context) { throw new Error( 'ContextMenu compound components cannot be rendered outside the ContextMenu component' ); } return context;}
const RadioItem = React.forwardRef<RadioItemRef, RadioItemProps>( ( { asChild, value, textValue, closeOnPress = true, onPress: onPressProp, onKeyDown: onKeyDownProp, ...props }, ref ) => { const { onValueChange } = useContextMenuRadioGroupContext(); const { close } = useContextMenuContentContext();
function onKeyDown(ev: React.KeyboardEvent) { onKeyDownProp?.(ev); if (ev.key === 'Enter' || ev.key === ' ') { onValueChange?.(value); onPressProp?.(EmptyGestureResponderEvent); if (closeOnPress) { close(); } } }
function onPress(ev: GestureResponderEvent) { onValueChange?.(value); onPressProp?.(ev); if (closeOnPress) { close(); } } const Component = asChild ? Slot.Pressable : Pressable; return ( <ContextMenu.RadioItem value={value} textValue={textValue} disabled={props.disabled ?? undefined} onSelect={closeOnPress ? undefined : onSelected} asChild > <Component ref={ref} // @ts-expect-error web only onKeyDown={onKeyDown} onPress={onPress} {...props} /> </ContextMenu.RadioItem> ); });
RadioItem.displayName = 'RadioItemWebContextMenu';
const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <ContextMenu.ItemIndicator forceMount={forceMount} asChild> <Component ref={ref} {...props} /> </ContextMenu.ItemIndicator> ); });
ItemIndicator.displayName = 'ItemIndicatorWebContextMenu';
const Separator = React.forwardRef<SeparatorRef, SeparatorProps>( ({ asChild, decorative, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <ContextMenu.Separator asChild> <Component ref={ref} {...props} /> </ContextMenu.Separator> ); });
Separator.displayName = 'SeparatorWebContextMenu';
const ContextMenuSubContext = React.createContext<{ open: boolean; onOpenChange: (open: boolean) => void;} | null>(null);
const Sub = React.forwardRef<SubRef, SubProps>( ({ asChild, defaultOpen, open: openProp, onOpenChange: onOpenChangeProp, ...props }, ref) => { const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, });
const Component = asChild ? Slot.View : View; return ( <ContextMenuSubContext.Provider value={{ open, onOpenChange }}> <ContextMenu.Sub open={open} onOpenChange={onOpenChange}> <Component ref={ref} {...props} /> </ContextMenu.Sub> </ContextMenuSubContext.Provider> ); });
Sub.displayName = 'SubWebContextMenu';
function useSubContext() { const context = React.useContext(ContextMenuSubContext); if (!context) { throw new Error( 'ContextMenu compound components cannot be rendered outside the ContextMenu component' ); } return context;}
const SubTrigger = React.forwardRef<SubTriggerRef, SubTriggerProps>( ({ asChild, textValue, disabled = false, onPress: onPressProp, ...props }, ref) => { const { onOpenChange } = useSubContext();
function onPress(ev: GestureResponderEvent) { onOpenChange(true); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <ContextMenu.SubTrigger disabled={disabled ?? undefined} textValue={textValue} asChild> <Component ref={ref} onPress={onPress} {...props} /> </ContextMenu.SubTrigger> ); });
SubTrigger.displayName = 'SubTriggerWebContextMenu';
const SubContent = React.forwardRef<SubContentRef, SubContentProps>( ({ asChild = false, forceMount, ...props }, ref) => { const Component = asChild ? Slot.Pressable : Pressable; return ( <ContextMenu.Portal> <ContextMenu.SubContent forceMount={forceMount}> <Component ref={ref} {...props} /> </ContextMenu.SubContent> </ContextMenu.Portal> ); });
Content.displayName = 'ContentWebContextMenu';
export { CheckboxItem, Content, Group, Item, ItemIndicator, Label, Overlay, Portal, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, Trigger, useRootContext, useSubContext,};
function onSelected(ev: Event) { ev.preventDefault();}
Copy/paste the following code for native to ~/components/primitives/context-menu/context-menu.tsx
import { useAugmentedRef, useControllableState, useRelativePosition, type LayoutPosition,} from '~/components/primitives/hooks';import { Portal as RNPPortal } from '~/components/primitives/portal';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { BackHandler, Pressable, Text, View, type AccessibilityActionEvent, type GestureResponderEvent, type LayoutChangeEvent, type LayoutRectangle,} from 'react-native';import type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,} from './types';
interface IRootContext extends RootProps { open: boolean; onOpenChange: (open: boolean) => void; pressPosition: LayoutPosition | null; setPressPosition: (pressPosition: LayoutPosition | null) => void; contentLayout: LayoutRectangle | null; setContentLayout: (contentLayout: LayoutRectangle | null) => void; nativeID: string;}
const RootContext = React.createContext<IRootContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, relativeTo = 'longPress', onOpenChange: onOpenChangeProp, ...viewProps }, ref) => { const nativeID = React.useId(); const [pressPosition, setPressPosition] = React.useState<LayoutPosition | null>(null); const [contentLayout, setContentLayout] = React.useState<LayoutRectangle | null>(null); const [open, setOpen] = React.useState(false);
function onOpenChange(value: boolean) { setOpen(value); onOpenChangeProp?.(value); }
const Component = asChild ? Slot.View : View; return ( <RootContext.Provider value={{ open, onOpenChange, relativeTo, contentLayout, nativeID, pressPosition, setContentLayout, setPressPosition, }} > <Component ref={ref} {...viewProps} /> </RootContext.Provider> ); });
Root.displayName = 'RootNativeContextMenu';
function useRootContext() { const context = React.useContext(RootContext); if (!context) { throw new Error( 'ContextMenu compound components cannot be rendered outside the ContextMenu component' ); } return context;}
const accessibilityActions = [{ name: 'longpress' }];
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ( { asChild, onLongPress: onLongPressProp, disabled = false, onAccessibilityAction: onAccessibilityActionProp, ...props }, ref ) => { const { open, onOpenChange, relativeTo, setPressPosition } = useRootContext(); const augmentedRef = useAugmentedRef({ ref, methods: { open: () => { onOpenChange(true); augmentedRef.current?.measure((_x, _y, width, height, pageX, pageY) => { setPressPosition({ width, pageX, pageY: pageY, height }); }); }, close: () => { setPressPosition(null); onOpenChange(false); }, }, });
function onLongPress(ev: GestureResponderEvent) { if (disabled) return; if (relativeTo === 'longPress') { setPressPosition({ width: 0, pageX: ev.nativeEvent.pageX, pageY: ev.nativeEvent.pageY, height: 0, }); } if (relativeTo === 'trigger') { augmentedRef.current?.measure((_x, _y, width, height, pageX, pageY) => { setPressPosition({ width, pageX, pageY: pageY, height }); }); } onOpenChange(!open); onLongPressProp?.(ev); }
function onAccessibilityAction(event: AccessibilityActionEvent) { if (disabled) return; if (event.nativeEvent.actionName === 'longpress') { setPressPosition({ width: 0, pageX: 0, pageY: 0, height: 0, }); const newValue = !open; onOpenChange(newValue); } onAccessibilityActionProp?.(event); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={augmentedRef} aria-disabled={disabled ?? undefined} role='button' onLongPress={onLongPress} disabled={disabled ?? undefined} aria-expanded={open} accessibilityActions={accessibilityActions} onAccessibilityAction={onAccessibilityAction} {...props} /> ); });
Trigger.displayName = 'TriggerNativeContextMenu';
/** * @warning when using a custom `<PortalHost />`, you will have to adjust the Content's sideOffset to account for nav elements like headers. */function Portal({ forceMount, hostName, children }: PortalProps) { const value = useRootContext();
if (!value.pressPosition) { return null; }
if (!forceMount) { if (!value.open) { return null; } }
return ( <RNPPortal hostName={hostName} name={`${value.nativeID}_portal`}> <RootContext.Provider value={value}>{children}</RootContext.Provider> </RNPPortal> );}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, onPress: OnPressProp, closeOnPress = true, ...props }, ref) => { const { open, onOpenChange, setContentLayout, setPressPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setPressPosition(null); setContentLayout(null); onOpenChange(false); } OnPressProp?.(ev); }
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} onPress={onPress} {...props} />; });
Overlay.displayName = 'OverlayNativeContextMenu';
const Content = React.forwardRef<ContentRef, ContentProps>( ( { asChild = false, forceMount, align = 'start', side = 'bottom', sideOffset = 0, alignOffset = 0, avoidCollisions = true, onLayout: onLayoutProp, insets, style, disablePositioningStyle, ...props }, ref ) => { const { open, onOpenChange, contentLayout, nativeID, pressPosition, setContentLayout, setPressPosition, } = useRootContext();
React.useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { setPressPosition(null); setContentLayout(null); onOpenChange(false); return true; });
return () => { setContentLayout(null); backHandler.remove(); }; }, []);
const positionStyle = useRelativePosition({ align, avoidCollisions, triggerPosition: pressPosition, contentLayout, alignOffset, insets, sideOffset, side, disablePositioningStyle, });
function onLayout(event: LayoutChangeEvent) { setContentLayout(event.nativeEvent.layout); onLayoutProp?.(event); }
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} role='menu' nativeID={nativeID} aria-modal={true} style={[positionStyle, style]} onLayout={onLayout} onStartShouldSetResponder={onStartShouldSetResponder} {...props} /> ); });
Content.displayName = 'ContentNativeContextMenu';
const Item = React.forwardRef<ItemRef, ItemProps>( ( { asChild, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onOpenChange, setContentLayout, setPressPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setPressPosition(null); setContentLayout(null); onOpenChange(false); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} role='menuitem' onPress={onPress} disabled={disabled} aria-valuetext={textValue} aria-disabled={!!disabled} accessibilityState={{ disabled: !!disabled }} {...props} /> ); });
Item.displayName = 'ItemNativeContextMenu';
const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component ref={ref} role='group' {...props} />;});
Group.displayName = 'GroupNativeContextMenu';
const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return <Component ref={ref} {...props} />;});
Label.displayName = 'LabelNativeContextMenu';
type FormItemContext = | { checked: boolean } | { value: string | undefined; onValueChange: (value: string) => void; };
const FormItemContext = React.createContext<FormItemContext | null>(null);
const CheckboxItem = React.forwardRef<CheckboxItemRef, CheckboxItemProps>( ( { asChild, checked, onCheckedChange, textValue, onPress: onPressProp, closeOnPress = true, disabled = false, ...props }, ref ) => { const { onOpenChange, setContentLayout, setPressPosition, nativeID } = useRootContext();
function onPress(ev: GestureResponderEvent) { onCheckedChange(!checked); if (closeOnPress) { setPressPosition(null); setContentLayout(null); onOpenChange(false); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <FormItemContext.Provider value={{ checked }}> <Component ref={ref} role='checkbox' aria-checked={checked} onPress={onPress} disabled={disabled} aria-disabled={!!disabled} aria-valuetext={textValue} accessibilityState={{ disabled: !!disabled }} {...props} /> </FormItemContext.Provider> ); });
CheckboxItem.displayName = 'CheckboxItemNativeContextMenu';
function useFormItemContext() { const context = React.useContext(FormItemContext); if (!context) { throw new Error( 'CheckboxItem or RadioItem compound components cannot be rendered outside of a CheckboxItem or RadioItem component' ); } return context;}
const RadioGroup = React.forwardRef<RadioGroupRef, RadioGroupProps>( ({ asChild, value, onValueChange, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <FormItemContext.Provider value={{ value, onValueChange }}> <Component ref={ref} role='radiogroup' {...props} /> </FormItemContext.Provider> ); });
RadioGroup.displayName = 'RadioGroupNativeContextMenu';
type BothFormItemContext = Exclude<FormItemContext, { checked: boolean }> & { checked: boolean;};
const RadioItemContext = React.createContext({} as { itemValue: string });
const RadioItem = React.forwardRef<RadioItemRef, RadioItemProps>( ( { asChild, value: itemValue, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onOpenChange, setContentLayout, setPressPosition } = useRootContext();
const { value, onValueChange } = useFormItemContext() as BothFormItemContext; function onPress(ev: GestureResponderEvent) { onValueChange(itemValue); if (closeOnPress) { setPressPosition(null); setContentLayout(null); onOpenChange(false); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <RadioItemContext.Provider value={{ itemValue }}> <Component ref={ref} onPress={onPress} role='radio' aria-checked={value === itemValue} disabled={disabled ?? false} accessibilityState={{ disabled: disabled ?? false, checked: value === itemValue, }} aria-valuetext={textValue} {...props} /> </RadioItemContext.Provider> ); });
RadioItem.displayName = 'RadioItemNativeContextMenu';
function useItemIndicatorContext() { return React.useContext(RadioItemContext);}
const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const { itemValue } = useItemIndicatorContext(); const { checked, value } = useFormItemContext() as BothFormItemContext;
if (!forceMount) { if (itemValue == null && !checked) { return null; } if (value !== itemValue) { return null; } } const Component = asChild ? Slot.View : View; return <Component ref={ref} role='presentation' {...props} />; });
ItemIndicator.displayName = 'ItemIndicatorNativeContextMenu';
const Separator = React.forwardRef<SeparatorRef, SeparatorProps>( ({ asChild, decorative, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component role={decorative ? 'presentation' : 'separator'} ref={ref} {...props} />; });
Separator.displayName = 'SeparatorNativeContextMenu';
const SubContext = React.createContext<{ nativeID: string; open: boolean; onOpenChange: (value: boolean) => void;} | null>(null);
const Sub = React.forwardRef<SubRef, SubProps>( ({ asChild, defaultOpen, open: openProp, onOpenChange: onOpenChangeProp, ...props }, ref) => { const nativeID = React.useId(); const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, });
const Component = asChild ? Slot.View : View; return ( <SubContext.Provider value={{ nativeID, open, onOpenChange, }} > <Component ref={ref} {...props} /> </SubContext.Provider> ); });
Sub.displayName = 'SubNativeContextMenu';
function useSubContext() { const context = React.useContext(SubContext); if (!context) { throw new Error('Sub compound components cannot be rendered outside of a Sub component'); } return context;}
const SubTrigger = React.forwardRef<SubTriggerRef, SubTriggerProps>( ({ asChild, textValue, onPress: onPressProp, disabled = false, ...props }, ref) => { const { nativeID, open, onOpenChange } = useSubContext();
function onPress(ev: GestureResponderEvent) { onOpenChange(!open); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-valuetext={textValue} role='menuitem' aria-expanded={open} accessibilityState={{ expanded: open, disabled: !!disabled }} nativeID={nativeID} onPress={onPress} disabled={disabled} aria-disabled={!!disabled} {...props} /> ); });
SubTrigger.displayName = 'SubTriggerNativeContextMenu';
const SubContent = React.forwardRef<SubContentRef, SubContentProps>( ({ asChild = false, forceMount, ...props }, ref) => { const { open, nativeID } = useSubContext();
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} role='group' aria-labelledby={nativeID} {...props} />; });
Content.displayName = 'ContentNativeContextMenu';
export { CheckboxItem, Content, Group, Item, ItemIndicator, Label, Overlay, Portal, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, Trigger, useRootContext, useSubContext,};
function onStartShouldSetResponder() { return true;}
Copy/paste the following code for types to ~/components/primitives/context-menu/types.ts
import { ForceMountable, PositionedContentProps, PressableRef, SlottablePressableProps, SlottableTextProps, SlottableViewProps, TextRef, ViewRef,} from '~/components/primitives/types';
type RootProps = SlottableViewProps & { onOpenChange?: (open: boolean) => void; /** * Platform: NATIVE ONLY */ relativeTo?: 'longPress' | 'trigger';};
interface PortalProps extends ForceMountable { children: React.ReactNode; /** * Platform: NATIVE ONLY */ hostName?: string; /** * Platform: WEB ONLY */ container?: HTMLElement | null | undefined;}
type OverlayProps = ForceMountable & SlottablePressableProps & { /** * Platform: NATIVE ONLY */ closeOnPress?: boolean; };
type ItemProps = SlottablePressableProps & { textValue?: string; closeOnPress?: boolean;};
type CheckboxItemProps = SlottablePressableProps & { checked: boolean; onCheckedChange: (checked: boolean) => void; closeOnPress?: boolean; textValue?: string;};
type RadioGroupProps = SlottableViewProps & { value: string | undefined; onValueChange: (value: string) => void;};
type RadioItemProps = SlottablePressableProps & { value: string; textValue?: string; closeOnPress?: boolean;};
type SeparatorProps = SlottableViewProps & { decorative?: boolean;};
type SubProps = SlottableViewProps & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (value: boolean) => void;};
type SubTriggerProps = SlottablePressableProps & { textValue?: string;};
type TriggerProps = SlottablePressableProps;type ContentProps = SlottableViewProps & PositionedContentProps;type SubContentProps = SlottablePressableProps & ForceMountable;type ItemIndicatorProps = SlottableViewProps & ForceMountable;type GroupProps = SlottableViewProps;type LabelProps = SlottableTextProps;
type CheckboxItemRef = PressableRef;type ContentRef = ViewRef;type GroupRef = ViewRef;type ItemIndicatorRef = ViewRef;type ItemRef = PressableRef;type LabelRef = TextRef;type OverlayRef = PressableRef;type RadioGroupRef = ViewRef;type RadioItemRef = PressableRef;type RootRef = ViewRef;type SeparatorRef = ViewRef;type SubContentRef = PressableRef;type SubRef = ViewRef;type SubTriggerRef = PressableRef;type TriggerRef = PressableRef & { /** * Platform: NATIVE ONLY */ open: () => void; /** * Platform: NATIVE ONLY */ close: () => void;};
export type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,};
Copy/paste the following code for exporting to ~/components/primitives/context-menu/index.ts
export * from './context-menu';export * from './types';
Copy/paste the following code for native to ~/components/primitives/context-menu/index.tsx
import { useAugmentedRef, useControllableState, useRelativePosition, type LayoutPosition,} from '~/components/primitives/hooks';import { Portal as RNPPortal } from '~/components/primitives/portal';import * as Slot from '~/components/primitives/slot';import * as React from 'react';import { BackHandler, Pressable, Text, View, type AccessibilityActionEvent, type GestureResponderEvent, type LayoutChangeEvent, type LayoutRectangle,} from 'react-native';import type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,} from './types';
interface IRootContext extends RootProps { open: boolean; onOpenChange: (open: boolean) => void; pressPosition: LayoutPosition | null; setPressPosition: (pressPosition: LayoutPosition | null) => void; contentLayout: LayoutRectangle | null; setContentLayout: (contentLayout: LayoutRectangle | null) => void; nativeID: string;}
const RootContext = React.createContext<IRootContext | null>(null);
const Root = React.forwardRef<RootRef, RootProps>( ({ asChild, relativeTo = 'longPress', onOpenChange: onOpenChangeProp, ...viewProps }, ref) => { const nativeID = React.useId(); const [pressPosition, setPressPosition] = React.useState<LayoutPosition | null>(null); const [contentLayout, setContentLayout] = React.useState<LayoutRectangle | null>(null); const [open, setOpen] = React.useState(false);
function onOpenChange(value: boolean) { setOpen(value); onOpenChangeProp?.(value); }
const Component = asChild ? Slot.View : View; return ( <RootContext.Provider value={{ open, onOpenChange, relativeTo, contentLayout, nativeID, pressPosition, setContentLayout, setPressPosition, }} > <Component ref={ref} {...viewProps} /> </RootContext.Provider> ); });
Root.displayName = 'RootNativeContextMenu';
function useRootContext() { const context = React.useContext(RootContext); if (!context) { throw new Error( 'ContextMenu compound components cannot be rendered outside the ContextMenu component' ); } return context;}
const accessibilityActions = [{ name: 'longpress' }];
const Trigger = React.forwardRef<TriggerRef, TriggerProps>( ( { asChild, onLongPress: onLongPressProp, disabled = false, onAccessibilityAction: onAccessibilityActionProp, ...props }, ref ) => { const { open, onOpenChange, relativeTo, setPressPosition } = useRootContext(); const augmentedRef = useAugmentedRef({ ref, methods: { open: () => { onOpenChange(true); augmentedRef.current?.measure((_x, _y, width, height, pageX, pageY) => { setPressPosition({ width, pageX, pageY: pageY, height }); }); }, close: () => { setPressPosition(null); onOpenChange(false); }, }, });
function onLongPress(ev: GestureResponderEvent) { if (disabled) return; if (relativeTo === 'longPress') { setPressPosition({ width: 0, pageX: ev.nativeEvent.pageX, pageY: ev.nativeEvent.pageY, height: 0, }); } if (relativeTo === 'trigger') { augmentedRef.current?.measure((_x, _y, width, height, pageX, pageY) => { setPressPosition({ width, pageX, pageY: pageY, height }); }); } onOpenChange(!open); onLongPressProp?.(ev); }
function onAccessibilityAction(event: AccessibilityActionEvent) { if (disabled) return; if (event.nativeEvent.actionName === 'longpress') { setPressPosition({ width: 0, pageX: 0, pageY: 0, height: 0, }); const newValue = !open; onOpenChange(newValue); } onAccessibilityActionProp?.(event); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={augmentedRef} aria-disabled={disabled ?? undefined} role='button' onLongPress={onLongPress} disabled={disabled ?? undefined} aria-expanded={open} accessibilityActions={accessibilityActions} onAccessibilityAction={onAccessibilityAction} {...props} /> ); });
Trigger.displayName = 'TriggerNativeContextMenu';
/** * @warning when using a custom `<PortalHost />`, you will have to adjust the Content's sideOffset to account for nav elements like headers. */function Portal({ forceMount, hostName, children }: PortalProps) { const value = useRootContext();
if (!value.pressPosition) { return null; }
if (!forceMount) { if (!value.open) { return null; } }
return ( <RNPPortal hostName={hostName} name={`${value.nativeID}_portal`}> <RootContext.Provider value={value}>{children}</RootContext.Provider> </RNPPortal> );}
const Overlay = React.forwardRef<OverlayRef, OverlayProps>( ({ asChild, forceMount, onPress: OnPressProp, closeOnPress = true, ...props }, ref) => { const { open, onOpenChange, setContentLayout, setPressPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setPressPosition(null); setContentLayout(null); onOpenChange(false); } OnPressProp?.(ev); }
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} onPress={onPress} {...props} />; });
Overlay.displayName = 'OverlayNativeContextMenu';
const Content = React.forwardRef<ContentRef, ContentProps>( ( { asChild = false, forceMount, align = 'start', side = 'bottom', sideOffset = 0, alignOffset = 0, avoidCollisions = true, onLayout: onLayoutProp, insets, style, disablePositioningStyle, ...props }, ref ) => { const { open, onOpenChange, contentLayout, nativeID, pressPosition, setContentLayout, setPressPosition, } = useRootContext();
React.useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', () => { setPressPosition(null); setContentLayout(null); onOpenChange(false); return true; });
return () => { setContentLayout(null); backHandler.remove(); }; }, []);
const positionStyle = useRelativePosition({ align, avoidCollisions, triggerPosition: pressPosition, contentLayout, alignOffset, insets, sideOffset, side, disablePositioningStyle, });
function onLayout(event: LayoutChangeEvent) { setContentLayout(event.nativeEvent.layout); onLayoutProp?.(event); }
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.View : View; return ( <Component ref={ref} role='menu' nativeID={nativeID} aria-modal={true} style={[positionStyle, style]} onLayout={onLayout} onStartShouldSetResponder={onStartShouldSetResponder} {...props} /> ); });
Content.displayName = 'ContentNativeContextMenu';
const Item = React.forwardRef<ItemRef, ItemProps>( ( { asChild, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onOpenChange, setContentLayout, setPressPosition } = useRootContext();
function onPress(ev: GestureResponderEvent) { if (closeOnPress) { setPressPosition(null); setContentLayout(null); onOpenChange(false); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} role='menuitem' onPress={onPress} disabled={disabled} aria-valuetext={textValue} aria-disabled={!!disabled} accessibilityState={{ disabled: !!disabled }} {...props} /> ); });
Item.displayName = 'ItemNativeContextMenu';
const Group = React.forwardRef<GroupRef, GroupProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component ref={ref} role='group' {...props} />;});
Group.displayName = 'GroupNativeContextMenu';
const Label = React.forwardRef<LabelRef, LabelProps>(({ asChild, ...props }, ref) => { const Component = asChild ? Slot.Text : Text; return <Component ref={ref} {...props} />;});
Label.displayName = 'LabelNativeContextMenu';
type FormItemContext = | { checked: boolean } | { value: string | undefined; onValueChange: (value: string) => void; };
const FormItemContext = React.createContext<FormItemContext | null>(null);
const CheckboxItem = React.forwardRef<CheckboxItemRef, CheckboxItemProps>( ( { asChild, checked, onCheckedChange, textValue, onPress: onPressProp, closeOnPress = true, disabled = false, ...props }, ref ) => { const { onOpenChange, setContentLayout, setPressPosition, nativeID } = useRootContext();
function onPress(ev: GestureResponderEvent) { onCheckedChange(!checked); if (closeOnPress) { setPressPosition(null); setContentLayout(null); onOpenChange(false); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <FormItemContext.Provider value={{ checked }}> <Component ref={ref} role='checkbox' aria-checked={checked} onPress={onPress} disabled={disabled} aria-disabled={!!disabled} aria-valuetext={textValue} accessibilityState={{ disabled: !!disabled }} {...props} /> </FormItemContext.Provider> ); });
CheckboxItem.displayName = 'CheckboxItemNativeContextMenu';
function useFormItemContext() { const context = React.useContext(FormItemContext); if (!context) { throw new Error( 'CheckboxItem or RadioItem compound components cannot be rendered outside of a CheckboxItem or RadioItem component' ); } return context;}
const RadioGroup = React.forwardRef<RadioGroupRef, RadioGroupProps>( ({ asChild, value, onValueChange, ...props }, ref) => { const Component = asChild ? Slot.View : View; return ( <FormItemContext.Provider value={{ value, onValueChange }}> <Component ref={ref} role='radiogroup' {...props} /> </FormItemContext.Provider> ); });
RadioGroup.displayName = 'RadioGroupNativeContextMenu';
type BothFormItemContext = Exclude<FormItemContext, { checked: boolean }> & { checked: boolean;};
const RadioItemContext = React.createContext({} as { itemValue: string });
const RadioItem = React.forwardRef<RadioItemRef, RadioItemProps>( ( { asChild, value: itemValue, textValue, onPress: onPressProp, disabled = false, closeOnPress = true, ...props }, ref ) => { const { onOpenChange, setContentLayout, setPressPosition } = useRootContext();
const { value, onValueChange } = useFormItemContext() as BothFormItemContext; function onPress(ev: GestureResponderEvent) { onValueChange(itemValue); if (closeOnPress) { setPressPosition(null); setContentLayout(null); onOpenChange(false); } onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <RadioItemContext.Provider value={{ itemValue }}> <Component ref={ref} onPress={onPress} role='radio' aria-checked={value === itemValue} disabled={disabled ?? false} accessibilityState={{ disabled: disabled ?? false, checked: value === itemValue, }} aria-valuetext={textValue} {...props} /> </RadioItemContext.Provider> ); });
RadioItem.displayName = 'RadioItemNativeContextMenu';
function useItemIndicatorContext() { return React.useContext(RadioItemContext);}
const ItemIndicator = React.forwardRef<ItemIndicatorRef, ItemIndicatorProps>( ({ asChild, forceMount, ...props }, ref) => { const { itemValue } = useItemIndicatorContext(); const { checked, value } = useFormItemContext() as BothFormItemContext;
if (!forceMount) { if (itemValue == null && !checked) { return null; } if (value !== itemValue) { return null; } } const Component = asChild ? Slot.View : View; return <Component ref={ref} role='presentation' {...props} />; });
ItemIndicator.displayName = 'ItemIndicatorNativeContextMenu';
const Separator = React.forwardRef<SeparatorRef, SeparatorProps>( ({ asChild, decorative, ...props }, ref) => { const Component = asChild ? Slot.View : View; return <Component role={decorative ? 'presentation' : 'separator'} ref={ref} {...props} />; });
Separator.displayName = 'SeparatorNativeContextMenu';
const SubContext = React.createContext<{ nativeID: string; open: boolean; onOpenChange: (value: boolean) => void;} | null>(null);
const Sub = React.forwardRef<SubRef, SubProps>( ({ asChild, defaultOpen, open: openProp, onOpenChange: onOpenChangeProp, ...props }, ref) => { const nativeID = React.useId(); const [open = false, onOpenChange] = useControllableState({ prop: openProp, defaultProp: defaultOpen, onChange: onOpenChangeProp, });
const Component = asChild ? Slot.View : View; return ( <SubContext.Provider value={{ nativeID, open, onOpenChange, }} > <Component ref={ref} {...props} /> </SubContext.Provider> ); });
Sub.displayName = 'SubNativeContextMenu';
function useSubContext() { const context = React.useContext(SubContext); if (!context) { throw new Error('Sub compound components cannot be rendered outside of a Sub component'); } return context;}
const SubTrigger = React.forwardRef<SubTriggerRef, SubTriggerProps>( ({ asChild, textValue, onPress: onPressProp, disabled = false, ...props }, ref) => { const { nativeID, open, onOpenChange } = useSubContext();
function onPress(ev: GestureResponderEvent) { onOpenChange(!open); onPressProp?.(ev); }
const Component = asChild ? Slot.Pressable : Pressable; return ( <Component ref={ref} aria-valuetext={textValue} role='menuitem' aria-expanded={open} accessibilityState={{ expanded: open, disabled: !!disabled }} nativeID={nativeID} onPress={onPress} disabled={disabled} aria-disabled={!!disabled} {...props} /> ); });
SubTrigger.displayName = 'SubTriggerNativeContextMenu';
const SubContent = React.forwardRef<SubContentRef, SubContentProps>( ({ asChild = false, forceMount, ...props }, ref) => { const { open, nativeID } = useSubContext();
if (!forceMount) { if (!open) { return null; } }
const Component = asChild ? Slot.Pressable : Pressable; return <Component ref={ref} role='group' aria-labelledby={nativeID} {...props} />; });
Content.displayName = 'ContentNativeContextMenu';
export { CheckboxItem, Content, Group, Item, ItemIndicator, Label, Overlay, Portal, RadioGroup, RadioItem, Root, Separator, Sub, SubContent, SubTrigger, Trigger, useRootContext, useSubContext,};
function onStartShouldSetResponder() { return true;}
Copy/paste the following code for types to ~/components/primitives/context-menu/types.ts
import { ForceMountable, PositionedContentProps, PressableRef, SlottablePressableProps, SlottableTextProps, SlottableViewProps, TextRef, ViewRef,} from '~/components/primitives/types';
type RootProps = SlottableViewProps & { onOpenChange?: (open: boolean) => void; /** * Platform: NATIVE ONLY */ relativeTo?: 'longPress' | 'trigger';};
interface PortalProps extends ForceMountable { children: React.ReactNode; /** * Platform: NATIVE ONLY */ hostName?: string; /** * Platform: WEB ONLY */ container?: HTMLElement | null | undefined;}
type OverlayProps = ForceMountable & SlottablePressableProps & { /** * Platform: NATIVE ONLY */ closeOnPress?: boolean; };
type ItemProps = SlottablePressableProps & { textValue?: string; closeOnPress?: boolean;};
type CheckboxItemProps = SlottablePressableProps & { checked: boolean; onCheckedChange: (checked: boolean) => void; closeOnPress?: boolean; textValue?: string;};
type RadioGroupProps = SlottableViewProps & { value: string | undefined; onValueChange: (value: string) => void;};
type RadioItemProps = SlottablePressableProps & { value: string; textValue?: string; closeOnPress?: boolean;};
type SeparatorProps = SlottableViewProps & { decorative?: boolean;};
type SubProps = SlottableViewProps & { defaultOpen?: boolean; open?: boolean; onOpenChange?: (value: boolean) => void;};
type SubTriggerProps = SlottablePressableProps & { textValue?: string;};
type TriggerProps = SlottablePressableProps;type ContentProps = SlottableViewProps & PositionedContentProps;type SubContentProps = SlottablePressableProps & ForceMountable;type ItemIndicatorProps = SlottableViewProps & ForceMountable;type GroupProps = SlottableViewProps;type LabelProps = SlottableTextProps;
type CheckboxItemRef = PressableRef;type ContentRef = ViewRef;type GroupRef = ViewRef;type ItemIndicatorRef = ViewRef;type ItemRef = PressableRef;type LabelRef = TextRef;type OverlayRef = PressableRef;type RadioGroupRef = ViewRef;type RadioItemRef = PressableRef;type RootRef = ViewRef;type SeparatorRef = ViewRef;type SubContentRef = PressableRef;type SubRef = ViewRef;type SubTriggerRef = PressableRef;type TriggerRef = PressableRef & { /** * Platform: NATIVE ONLY */ open: () => void; /** * Platform: NATIVE ONLY */ close: () => void;};
export type { CheckboxItemProps, CheckboxItemRef, ContentProps, ContentRef, GroupProps, GroupRef, ItemIndicatorProps, ItemIndicatorRef, ItemProps, ItemRef, LabelProps, LabelRef, OverlayProps, OverlayRef, PortalProps, RadioGroupProps, RadioGroupRef, RadioItemProps, RadioItemRef, RootProps, RootRef, SeparatorProps, SeparatorRef, SubContentProps, SubContentRef, SubProps, SubRef, SubTriggerProps, SubTriggerRef, TriggerProps, TriggerRef,};
Usage
import * as React from 'react';import * as ContextMenuPrimitive from '@rn-primitives/context-menu';import { Text } from 'react-native';
function Example() { const [checkboxValue, setCheckboxValue] = React.useState(false); const [subCheckboxValue, setSubCheckboxValue] = React.useState(false); const [radioValue, setRadioValue] = React.useState('pedro');
return ( <ContextMenu.Root> <ContextMenu.Trigger> <Text> {Platform.OS === 'web' ? 'Right click here' : 'Long press here'} </Text> </ContextMenu.Trigger>
<ContextMenuPrimitive.Portal> <ContextMenuPrimitive.Overlay> <ContextMenu.Content> <ContextMenu.Item > <Text>Back</Text> </ContextMenu.Item> <ContextMenu.Item > <Text>Forward</Text> </ContextMenu.Item> <ContextMenu.Item > <Text>Reload</Text> </ContextMenu.Item>
<ContextMenu.Sub> <ContextMenu.SubTrigger > <Text>More Tools</Text> </ContextMenu.SubTrigger> <ContextMenu.SubContent> <ContextMenu.Item> <Text>Save Page As...</Text> </ContextMenu.Item> <ContextMenu.Item> <Text>Create Shortcut...</Text> </ContextMenu.Item>
<ContextMenu.Separator /> <ContextMenu.Item> <Text>Developer Tools</Text> </ContextMenu.Item> </ContextMenuSubContent> </ContextMenuSub>
<ContextMenu.Separator /> <ContextMenu.CheckboxItem checked={checkboxValue} onCheckedChange={setCheckboxValue} closeOnPress={false} > <Text>Show Bookmarks Bar</Text> </ContextMenu.CheckboxItem> <ContextMenu.CheckboxItem checked={subCheckboxValue} onCheckedChange={setSubCheckboxValue} closeOnPress={false} > <Text>Show Full URLs</Text> </ContextMenu.CheckboxItem> <ContextMenu.Separator /> <ContextMenu.RadioGroup value={radioValue} onValueChange={setRadioValue}> <ContextMenu.Label >People</ContextMenu.Label> <ContextMenu.Separator /> <ContextMenu.RadioItem value='pedro' closeOnPress={false}> <Text>Elmer Fudd</Text> </ContextMenu.RadioItem> <ContextMenu.RadioItem value='colm' closeOnPress={false}> <Text>Foghorn Leghorn</Text> </ContextMenu.RadioItem> </ContextMenu.RadioGroup> </ContextMenu.Content> </ContextMenuPrimitive.Overlay> </ContextMenuPrimitive.Portal> </ContextMenu.Root> );}
Props
Root
Extends View
props
Prop | Type | Note |
---|---|---|
onOpenChange | (open: boolean) => void | |
asChild | boolean | (optional) |
relativeTo | ’longPress’ | ‘trigger’ | Native Only_(optional)_ |
Trigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
TYPE: ContextMenuTriggerRef
Methods | args | Note |
---|---|---|
open | Native ONLY: opens the context menu | |
close | Native ONLY: closes the context menu |
Portal
Prop | Type | Note |
---|---|---|
children* | React.ReactNode | |
forceMount | true | undefined | (optional) |
hostName | string | Web Only (optional) |
container | HTMLElement | null | undefined | Web Only (optional) |
Overlay
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true | undefined; | (optional) |
Content
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true | undefined | (optional) |
alignOffset | number | (optional) |
insets | Insets | (optional) |
avoidCollisions | boolean | (optional) |
align | ’start’ | ‘center’ | ‘end’ | (optional) |
side | ’top’ | ‘bottom’ | (optional) |
sideOffset | number | (optional) |
disablePositioningStyle | boolean | Native Only (optional) |
loop | boolean | Web Only (optional) |
onCloseAutoFocus | (event: Event) => void | Web Only (optional) |
onEscapeKeyDown | (event: KeyboardEvent) => void | Web Only (optional) |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void | Web Only (optional) |
onFocusOutside | (event: FocusOutsideEvent) => void | Web Only (optional) |
onInteractOutside | PointerDownOutsideEvent | FocusOutsideEvent | Web Only (optional) |
collisionBoundary | Element | null | Array<Element | null> | Web Only (optional) |
sticky | ’partial’ | ‘always’ | Web Only (optional) |
hideWhenDetached | boolean | Web Only (optional) |
Group
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Label
Extends Text
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
Item
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
textValue | boolean | (optional) |
closeOnPress | boolean | (optional) |
CheckboxItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
checked* | boolean | |
onCheckedChange* | (value: boolean) => void | |
textValue* | string | |
asChild | boolean | (optional) |
closeOnPress | boolean | Native Only_(optional)_ |
RadioGroup
Extends View
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onValueChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
RadioItem
Extends Pressable
props
Prop | Type | Note |
---|---|---|
value* | boolean | |
onCheckedChange* | (value: boolean) => void | |
asChild | boolean | (optional) |
closeOnPress | boolean | Native Only_(optional)_ |
ItemIndicator
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true / | undefined |
Separator
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
decorative | boolean | (optional) |
Sub
Extends View
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
defaultOpen | boolean | (optional) |
open | boolean | (optional) |
onOpenChange | (value: boolean) => void | (optional) |
SubTrigger
Extends Pressable
props
Prop | Type | Note |
---|---|---|
textValue | string | (optional) |
asChild | boolean | (optional) |
SubContent
Extends Pressable
props
Prop | Type | Note |
---|---|---|
asChild | boolean | (optional) |
forceMount | true / | undefined |
useRootContext
Must be used within a Root
component. It provides the following values from the context menu: open
, and onOpenChange
.
useSubContext
Must be used within a Sub
component. It provides the following values from the context menu: open
, and onOpenChange
.