mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
770 lines
19 KiB
Plaintext
770 lines
19 KiB
Plaintext
---
|
|
title: Context Menu
|
|
description: A menu component triggered by right-click on web and long press on touch devices
|
|
name: context-menu
|
|
component: ContextMenu
|
|
package: context-menu
|
|
demoName: ContextMenu
|
|
---
|
|
|
|
<HeroContainer>
|
|
<ContextMenuDemo />
|
|
</HeroContainer>
|
|
|
|
```tsx hero template=ContextMenu
|
|
|
|
```
|
|
|
|
<Highlights
|
|
features={[
|
|
'Full keyboard navigation.',
|
|
'Supports items, icons, images, checkboxes, groups, and more.',
|
|
'Supports submenus.',
|
|
'Supports modal and non-modal modes.',
|
|
'Supports native menus on native platforms.',
|
|
'Customizable alignment, offsets, and positioning.',
|
|
'Support for native iOS and Android icons.',
|
|
'Supports previews on iOS.',
|
|
]}
|
|
/>
|
|
|
|
ContextMenu displays a menu triggered by right-click on web or long-press on mobile. It supports submenus, native platform menus with iOS previews, and [automatically stacks](/ui/z-index) above other content.
|
|
|
|
## Installation
|
|
|
|
ContextMenu is already installed in `hanzogui`, or you can install it independently:
|
|
|
|
```bash
|
|
yarn add @hanzogui/context-menu
|
|
```
|
|
|
|
If you want to use native menus, add these dependencies:
|
|
|
|
```sh
|
|
yarn add @react-native-menu/menu
|
|
yarn add react-native-ios-context-menu
|
|
yarn add react-native-ios-utilities
|
|
yarn add zeego
|
|
yarn add sf-symbols-typescript
|
|
```
|
|
|
|
Then add the setup import at your app entry point:
|
|
|
|
```tsx
|
|
import '@hanzogui/native/setup-zeego'
|
|
```
|
|
|
|
<Notice theme="blue">
|
|
On native, `ContextMenu.Trigger` coordinates with parent Gui pressables automatically.
|
|
You should not need extra wrapper views or empty `onPress` handlers just to prevent a
|
|
parent `pressStyle` from activating.
|
|
</Notice>
|
|
|
|
<Notice theme="yellow">
|
|
**Expo Router users:** This import must run _before_ `expo-router/entry`. Create an
|
|
`index.js` at your project root that imports the setup first, then expo-router, and
|
|
update your `package.json` main field to `"index.js"`. See the [upgrade
|
|
guide](/docs/guides/how-to-upgrade#expo-router-entry-point) for details.
|
|
</Notice>
|
|
|
|
## Anatomy
|
|
|
|
Import all parts and piece them together.
|
|
|
|
```jsx
|
|
import { ContextMenu } from '@hanzo/gui' // or '@hanzogui/context-menu'
|
|
|
|
export default () => (
|
|
<ContextMenu>
|
|
<ContextMenu.Trigger asChild>
|
|
<YStack>
|
|
<Text>Right Click or longPress</Text>
|
|
</YStack>
|
|
</ContextMenu.Trigger>
|
|
|
|
<ContextMenu.Portal zIndex={100}>
|
|
<ContextMenu.Content>
|
|
<ContextMenu.Item>
|
|
<ContextMenu.ItemTitle>About Notes</ContextMenu.ItemTitle>
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Item>
|
|
<ContextMenu.ItemTitle>Settings</ContextMenu.ItemTitle>
|
|
</ContextMenu.Item>
|
|
{/* when title is nested inside a React element then you need to use `textValue` */}
|
|
<ContextMenu.Item textValue="Calendar">
|
|
<ContextMenu.ItemTitle>
|
|
<Text>Calendar</Text>
|
|
</ContextMenu.ItemTitle>
|
|
<ContextMenu.ItemIcon>
|
|
<Calendar color="gray" size="$1" />
|
|
</ContextMenu.ItemIcon>
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Separator />
|
|
<ContextMenu.Sub>
|
|
<ContextMenu.SubTrigger>
|
|
<ContextMenu.ItemTitle>Actions</ContextMenu.ItemTitle>
|
|
</ContextMenu.SubTrigger>
|
|
<ContextMenu.Portal zIndex={200}>
|
|
<ContextMenu.SubContent>
|
|
<ContextMenu.Label fontSize={'$1'}>Note settings</ContextMenu.Label>
|
|
<ContextMenu.Item onSelect={onSelect} key="create-note">
|
|
<ContextMenu.ItemTitle>Create note</ContextMenu.ItemTitle>
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Item onSelect={onSelect} key="delete-all">
|
|
<ContextMenu.ItemTitle>Delete all notes</ContextMenu.ItemTitle>
|
|
</ContextMenu.Item>
|
|
<ContextMenu.Item onSelect={onSelect} key="sync-all">
|
|
<ContextMenu.ItemTitle>Sync notes</ContextMenu.ItemTitle>
|
|
</ContextMenu.Item>
|
|
</ContextMenu.SubContent>
|
|
</ContextMenu.Portal>
|
|
</ContextMenu.Sub>
|
|
</ContextMenu.Content>
|
|
</ContextMenu.Portal>
|
|
</ContextMenu>
|
|
)
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### ContextMenu
|
|
|
|
Contains every component for the ContextMenu.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'React.ReactNode',
|
|
required: true,
|
|
description: 'Menu parts: Trigger, Portal, Content, Items, etc.',
|
|
},
|
|
{
|
|
name: 'placement',
|
|
type: 'Placement',
|
|
required: false,
|
|
description: `Where the menu appears relative to the trigger. Options: 'top' | 'right' | 'bottom' | 'left' with optional '-start' | '-end' alignment.`,
|
|
},
|
|
{
|
|
name: 'open',
|
|
type: 'boolean',
|
|
required: false,
|
|
description: 'Controlled open state for the menu.',
|
|
},
|
|
{
|
|
name: 'defaultOpen',
|
|
type: 'boolean',
|
|
required: false,
|
|
description: 'Initial open state when uncontrolled.',
|
|
},
|
|
{
|
|
name: 'onOpenChange',
|
|
type: '(open: boolean, event?: { preventDefault: () => void }) => void',
|
|
required: false,
|
|
description:
|
|
'Called when the menu opens or closes. Call event.preventDefault() to cancel and allow the native context menu to show instead.',
|
|
},
|
|
{
|
|
name: 'onOpenWillChange',
|
|
type: '(open: boolean) => void',
|
|
required: false,
|
|
platform: 'ios',
|
|
description: 'Called before the open/close animation begins.',
|
|
},
|
|
{
|
|
name: 'modal',
|
|
type: 'boolean',
|
|
default: 'true',
|
|
required: false,
|
|
description:
|
|
'When true, traps focus inside the menu and blocks outside scroll/interactions.',
|
|
},
|
|
{
|
|
name: 'stayInFrame',
|
|
type: 'ShiftProps | boolean',
|
|
required: false,
|
|
default: '{ padding: 10 }',
|
|
description:
|
|
'Shifts the menu horizontally to stay within viewport bounds. Pass an object to customize shift behavior (mainAxis, crossAxis, padding).',
|
|
},
|
|
{
|
|
name: 'allowFlip',
|
|
type: 'FlipProps | boolean',
|
|
required: false,
|
|
description: 'Flips the menu to the opposite side if there is insufficient space.',
|
|
},
|
|
{
|
|
name: 'offset',
|
|
type: 'OffsetOptions',
|
|
required: false,
|
|
description: 'Distance between the menu and its trigger.',
|
|
},
|
|
{
|
|
name: 'unstyled',
|
|
required: false,
|
|
type: 'boolean',
|
|
description: 'Removes all default Gui styles.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
#### Allowing Native Context Menu
|
|
|
|
You can call `event.preventDefault()` in `onOpenChange` to prevent the Gui menu from opening and allow the native browser context menu to appear instead:
|
|
|
|
```tsx
|
|
<ContextMenu
|
|
onOpenChange={(open, event) => {
|
|
if (someCondition) {
|
|
// prevent Gui menu, let native context menu show
|
|
event?.preventDefault()
|
|
}
|
|
}}
|
|
>
|
|
{/* ... */}
|
|
</ContextMenu>
|
|
```
|
|
|
|
### ContextMenu.Portal
|
|
|
|
This is necessary for the ContextMenu.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'zIndex',
|
|
required: false,
|
|
type: 'number',
|
|
description: 'Stacking order of the portal layer.',
|
|
},
|
|
{
|
|
name: 'children',
|
|
type: 'React.ReactNode',
|
|
required: true,
|
|
description: 'Content to render inside the portal.',
|
|
},
|
|
{
|
|
name: 'forceMount',
|
|
type: 'true',
|
|
required: false,
|
|
description:
|
|
'Forces the portal to stay mounted, useful for controlling animations.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.Trigger
|
|
|
|
The ContextMenu will only be triggered when the user right-clicks or
|
|
long-presses within the Trigger area.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'action',
|
|
required: false,
|
|
type: 'press|longPress',
|
|
default: 'longPress',
|
|
platform: 'android/ios',
|
|
description:
|
|
"Works with the native prop and accepts 'press' or 'longPress'. The default is 'longPress' for ContextMenu.",
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.Content
|
|
|
|
Contains the content of the ContextMenu.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'React.ReactNode',
|
|
required: true,
|
|
description: 'Menu items, groups, labels, separators, and submenus.',
|
|
},
|
|
{
|
|
name: 'loop',
|
|
type: 'boolean',
|
|
required: false,
|
|
default: 'false',
|
|
description: 'Whether keyboard navigation wraps from last to first item.',
|
|
},
|
|
{
|
|
name: 'forceMount',
|
|
type: 'true',
|
|
required: false,
|
|
description:
|
|
'Forces the content to stay mounted, useful for controlling animations.',
|
|
},
|
|
{
|
|
name: 'onCloseAutoFocus',
|
|
type: '(event: Event) => void',
|
|
required: false,
|
|
description: 'Called when focus returns to the trigger after closing.',
|
|
},
|
|
{
|
|
name: 'onEscapeKeyDown',
|
|
type: '(event: KeyboardEvent) => void',
|
|
required: false,
|
|
description: 'Called when the escape key is pressed. Can be prevented.',
|
|
},
|
|
{
|
|
name: 'onPointerDownOutside',
|
|
type: '(event: PointerEvent) => void',
|
|
required: false,
|
|
description: 'Called when a pointer event occurs outside the content.',
|
|
},
|
|
{
|
|
name: 'onInteractOutside',
|
|
type: '(event: Event) => void',
|
|
required: false,
|
|
description: 'Called when any interaction occurs outside the content.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.Item
|
|
|
|
A selectable menu item that triggers an action when selected.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'key',
|
|
type: 'string',
|
|
required: true,
|
|
description: 'Unique identifier for the item.',
|
|
},
|
|
{
|
|
name: 'disabled',
|
|
type: 'boolean',
|
|
required: false,
|
|
default: 'false',
|
|
description: 'Prevents interaction and dims the item.',
|
|
},
|
|
{
|
|
name: 'destructive',
|
|
type: 'boolean',
|
|
required: false,
|
|
platform: 'ios/android',
|
|
description:
|
|
'Renders the item in red on iOS to indicate a dangerous action (e.g. delete). No effect on web.',
|
|
},
|
|
{
|
|
name: 'hidden',
|
|
type: 'boolean',
|
|
required: false,
|
|
description: 'Hides the item from the menu.',
|
|
},
|
|
{
|
|
name: 'onSelect',
|
|
type: '(event?: Event) => void',
|
|
required: false,
|
|
description: 'Called when the item is selected via click or keyboard.',
|
|
},
|
|
{
|
|
name: 'onFocus',
|
|
type: '() => void',
|
|
required: false,
|
|
description: 'Called when the item receives focus.',
|
|
},
|
|
{
|
|
name: 'onBlur',
|
|
type: '() => void',
|
|
required: false,
|
|
description: 'Called when the item loses focus.',
|
|
},
|
|
{
|
|
name: 'textValue',
|
|
type: 'string',
|
|
required: false,
|
|
platform: 'ios/android',
|
|
description:
|
|
'Text used for typeahead and native menus. Required when ItemTitle contains a React node instead of a string.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.ItemTitle
|
|
|
|
Renders the title of the menu item.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'string | React.ReactNode',
|
|
required: true,
|
|
description: 'The title text or element to display.',
|
|
},
|
|
]}
|
|
/>
|
|
<Notice>
|
|
You can directly pass a text node to the ItemTitle. However, if you use a nested
|
|
React node like `<Text>`, you need to pass `textValue` to the `<Item>` so that it
|
|
works with native menus.
|
|
</Notice>
|
|
|
|
### ContextMenu.ItemIcon
|
|
|
|
A component to render an icon. For non-native menus, you can pass an icon
|
|
component. For native menus, you can pass platform-specific icons to the
|
|
`android`/`ios` props.
|
|
|
|
On iOS, it renders the native
|
|
[SF Symbols](https://github.com/andrewtavis/sf-symbols-online) icons.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'React.ReactNode',
|
|
required: false,
|
|
description: 'Fallback icon for web when native icons are not available.',
|
|
},
|
|
{
|
|
name: 'ios',
|
|
type: 'object',
|
|
required: false,
|
|
platform: 'ios',
|
|
description:
|
|
'SF Symbol configuration: name, weight, scale, hierarchicalColor, paletteColors.',
|
|
},
|
|
{
|
|
name: 'android',
|
|
type: 'object',
|
|
required: false,
|
|
platform: 'android',
|
|
description: 'Android resource drawable name.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
```jsx line=1
|
|
<ContextMenu.ItemIcon
|
|
ios={{
|
|
name: '0.circle.fill', // required
|
|
pointSize: 5,
|
|
weight: 'semibold',
|
|
scale: 'medium',
|
|
// can also be a color string. Requires iOS 15+
|
|
hierarchicalColor: {
|
|
dark: 'blue',
|
|
light: 'green',
|
|
},
|
|
// alternative to hierarchical color. Requires iOS 15+
|
|
paletteColors: [
|
|
{
|
|
dark: 'blue',
|
|
light: 'green',
|
|
},
|
|
],
|
|
}}
|
|
>
|
|
<CircleIcon />
|
|
</ContextMenu.ItemIcon>
|
|
```
|
|
|
|
### ContextMenu.ItemImage
|
|
|
|
A component to render an item image. For native menus, it only works on iOS. It
|
|
takes the same properties as `@hanzogui/image`.
|
|
|
|
### ContextMenu.ItemSubtitle
|
|
|
|
A component to render a subtitle for the menu item. For native menus, it only
|
|
works on iOS.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'string',
|
|
required: true,
|
|
description: 'The subtitle text to display below the title.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.Group
|
|
|
|
A component that groups multiple menu items together.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'React.ReactNode',
|
|
required: true,
|
|
description: 'Menu items to group together.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.CheckboxItem
|
|
|
|
A menu item with a checkbox that can be toggled on/off.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'key',
|
|
type: 'string',
|
|
required: true,
|
|
description: 'Unique identifier for the checkbox item.',
|
|
},
|
|
{
|
|
name: 'disabled',
|
|
type: 'boolean',
|
|
required: false,
|
|
default: 'false',
|
|
description: 'Prevents interaction and dims the item.',
|
|
},
|
|
{
|
|
name: 'destructive',
|
|
type: 'boolean',
|
|
required: false,
|
|
platform: 'ios/android',
|
|
description:
|
|
'Renders the item in red on iOS to indicate a dangerous action. No effect on web.',
|
|
},
|
|
{
|
|
name: 'hidden',
|
|
type: 'boolean',
|
|
required: false,
|
|
description: 'Hides the item from the menu.',
|
|
},
|
|
{
|
|
name: 'onFocus',
|
|
type: '() => void',
|
|
required: false,
|
|
description: 'Called when the item receives focus.',
|
|
},
|
|
{
|
|
name: 'onBlur',
|
|
type: '() => void',
|
|
required: false,
|
|
description: 'Called when the item loses focus.',
|
|
},
|
|
{
|
|
name: 'textValue',
|
|
type: 'string',
|
|
required: false,
|
|
platform: 'ios/android',
|
|
description:
|
|
'Text for native menus. Required when ItemTitle contains a React node.',
|
|
},
|
|
{
|
|
name: 'value',
|
|
type: "'on' | 'off' | 'mixed'",
|
|
required: false,
|
|
platform: 'ios/android',
|
|
description: 'Controlled checked state for native menus.',
|
|
},
|
|
{
|
|
name: 'onValueChange',
|
|
type: '(state, prevState) => void',
|
|
required: false,
|
|
platform: 'ios/android',
|
|
description: 'Called when checked state changes on native menus.',
|
|
},
|
|
{
|
|
name: 'checked',
|
|
type: 'boolean',
|
|
required: false,
|
|
description: 'Controlled checked state for web menus.',
|
|
},
|
|
{
|
|
name: 'onCheckedChange',
|
|
type: '(checked: boolean) => void',
|
|
required: false,
|
|
description: 'Called when checked state changes on web.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.ItemIndicator
|
|
|
|
Use inside `CheckboxItem` or `RadioItem` to indicate when an item is checked.
|
|
This allows you to conditionally render a checkmark.
|
|
|
|
```jsx line=1
|
|
<ContextMenu.ItemIndicator>
|
|
<CheckmarkIcon /> {/* This does not work with the native prop. */}
|
|
</ContextMenu.ItemIndicator>
|
|
```
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'React.ReactNode',
|
|
required: false,
|
|
description: 'Custom checkmark icon. Only works on web.',
|
|
},
|
|
{
|
|
name: 'forceMount',
|
|
type: 'true',
|
|
required: false,
|
|
description: 'Forces the indicator to stay mounted for animation control.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.Label
|
|
|
|
Renders a non-focusable label for a group of items. On native menus, only one
|
|
label is supported per menu and submenu.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'string',
|
|
required: true,
|
|
description: 'The label text.',
|
|
},
|
|
{
|
|
name: 'textValue',
|
|
type: 'string',
|
|
required: false,
|
|
platform: 'ios/android',
|
|
description: 'Text for native menus when children is a React node.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.Arrow
|
|
|
|
Renders an arrow pointing to the trigger.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'size',
|
|
type: 'number | SizeToken',
|
|
required: false,
|
|
description: 'Width and height of the arrow.',
|
|
},
|
|
{
|
|
name: 'unstyled',
|
|
type: 'boolean',
|
|
required: false,
|
|
description: 'Removes default arrow styles.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.Separator
|
|
|
|
Renders a visual divider between menu items. Web only.
|
|
|
|
### ContextMenu.Sub
|
|
|
|
A container for nested sub-menu components.
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'React.ReactNode',
|
|
required: true,
|
|
description: 'SubTrigger, Portal, and SubContent components.',
|
|
},
|
|
{
|
|
name: 'open',
|
|
type: 'boolean',
|
|
required: false,
|
|
description: 'Controlled open state for the sub-menu.',
|
|
},
|
|
{
|
|
name: 'onOpenChange',
|
|
type: '(open: boolean) => void',
|
|
required: false,
|
|
description: 'Called when the sub-menu opens or closes.',
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### ContextMenu.SubContent
|
|
|
|
Renders the content of a sub-menu. Same props as `Content`, excluding `side` and
|
|
`align`.
|
|
|
|
### ContextMenu.SubTrigger
|
|
|
|
A menu item that opens a sub-menu on hover/focus. Accepts the same props as
|
|
`Item`.
|
|
|
|
### ContextMenu.Preview
|
|
|
|
When the ContextMenu is visible, this renders a custom preview component. Only
|
|
works with native iOS menus.
|
|
|
|
<Notice>Should be rendered as a child of `ContextMenu.Content`.</Notice>
|
|
|
|
<PropsTable
|
|
data={[
|
|
{
|
|
name: 'children',
|
|
type: 'React.ReactNode | (() => React.ReactNode)',
|
|
required: true,
|
|
platform: 'ios',
|
|
description: 'The preview content to render. Can be a function for lazy rendering.',
|
|
},
|
|
{
|
|
name: 'size',
|
|
type: '{ width?: number, height?: number }',
|
|
required: false,
|
|
platform: 'ios',
|
|
description: 'Dimensions of the preview.',
|
|
},
|
|
{
|
|
name: 'onPress',
|
|
type: '() => void',
|
|
required: false,
|
|
platform: 'ios',
|
|
description: 'Called when the preview is pressed.',
|
|
},
|
|
{
|
|
name: 'backgroundColor',
|
|
type: 'string | { dark: string, light: string }',
|
|
required: false,
|
|
platform: 'ios',
|
|
description: 'Background color of the preview.',
|
|
},
|
|
{
|
|
name: 'borderRadius',
|
|
type: 'number',
|
|
required: false,
|
|
platform: 'ios',
|
|
description: 'Border radius of the preview.',
|
|
},
|
|
{
|
|
name: 'preferredCommitStyle',
|
|
type: "'pop' | 'dismiss'",
|
|
required: false,
|
|
default: "'dismiss'",
|
|
platform: 'ios',
|
|
description:
|
|
"Exit transition when preview is tapped. Use 'pop' when navigating to another screen.",
|
|
},
|
|
]}
|
|
/>
|
|
|
|
```jsx line=1
|
|
<ContextMenu.Preview
|
|
// optional props:
|
|
preferredCommitStyle="pop" // or "dismiss"
|
|
backgroundColor={{
|
|
// or a color string directly
|
|
dark: 'black',
|
|
light: 'white',
|
|
}}
|
|
>
|
|
{() => <Preview />}
|
|
</ContextMenu.Preview>
|
|
```
|