Displays a menu located at the pointer, triggered by a right-click or a long-press.
/* eslint-disable no-console */
import React from 'react';
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContentColors,
} from '@minddrop/ui';
export const ContextMenuDemo = () => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<ContextMenu>
<ContextMenuTrigger>
<div
style={{
border: '2px dashed white',
borderRadius: 4,
userSelect: 'none',
padding: '45px 0',
width: 300,
textAlign: 'center',
color: 'white',
}}
>
Right click here.
</div>
</ContextMenuTrigger>
<ContextMenuContent
style={{ width: 240 }}
content={[
{
type: 'menu-item',
label: 'Add title',
icon: 'title',
onSelect: () => console.log('Add title'),
keyboardShortcut: ['Ctrl', 'T'],
},
{
type: 'menu-item',
label: 'Add note',
icon: 'note',
onSelect: () => console.log('Add note'),
keyboardShortcut: ['Ctrl', 'Shift', 'N'],
},
{
type: 'menu-item',
label: 'Color',
icon: 'color-palette',
submenuContentClass: 'color-selection-submenu',
submenu: ContentColors.map((color) => ({
type: 'menu-color-selection-item',
color: color.value,
onSelect: () => console.log(color.value),
})),
},
{
type: 'menu-item',
label: 'Turn into',
icon: 'turn-into',
submenu: [
{
type: 'menu-item',
label: 'Text',
onSelect: () => console.log('Turn into text'),
},
{
type: 'menu-item',
label: 'Image',
onSelect: () => console.log('Turn into image'),
},
{
type: 'menu-item',
label: 'Equation',
onSelect: () => console.log('Turn into equation'),
},
],
},
{
type: 'menu-separator',
},
{
type: 'menu-label',
label: 'Actions',
},
{
type: 'menu-item',
label: 'Copy link',
icon: 'link',
onSelect: () => console.log('Copy link'),
keyboardShortcut: ['Ctrl', 'Shift', 'C'],
tooltipTitle: 'Copy drop link',
tooltipDescription:
'Paste the link into other drops to create a network or related information.',
},
{
type: 'menu-item',
label: 'Move to',
icon: 'arrow-up-right',
onSelect: () => console.log('Move to'),
submenuContentClass: 'topic-selection-submenu',
submenu: [
{
type: 'menu-topic-selection-item',
label: 'Sailing',
onSelect: () => console.log("Move to 'Sailing'"),
subtopics: [
{
type: 'menu-topic-selection-item',
label: 'Navigation',
onSelect: () => console.log("Move to 'Navigation'"),
subtopics: [
{
type: 'menu-topic-selection-item',
label: 'Coastal navigation',
onSelect: () =>
console.log("Move to 'Coastal navigation'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Offshore navigation',
onSelect: () =>
console.log("Move to 'Offshore navigation'"),
subtopics: [],
},
],
},
{
type: 'menu-topic-selection-item',
id: 'anchoring',
label: 'Anchoring',
onSelect: () => console.log("Move to 'Anchoring'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Sailboats',
onSelect: () => console.log("Move to 'Sailboats'"),
subtopics: [],
},
],
},
{
type: 'menu-topic-selection-item',
label: 'Home',
onSelect: () => console.log("Move to 'Home'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Tea',
onSelect: () => console.log("Move to 'Tea'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Work',
onSelect: () => console.log("Move to 'work'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Japanese',
onSelect: () => console.log("Move to 'Japanese'"),
subtopics: [],
},
],
},
{
type: 'menu-item',
label: 'Delete',
icon: 'trash',
onSelect: () => console.log('Delete'),
keyboardShortcut: ['Del'],
secondaryLabel: 'Delete everywhere',
secondaryOnSelect: () => console.log('Delete everywhere'),
tooltipTitle: 'Delete drop',
tooltipDescription: 'Shift + Click to delete from all topics',
},
]}
/>
</ContextMenu>
</div>
);
export default ContextMenuDemo;
You can compose a menu using the available components.
import {
IconButton,
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuItem,
ContextMenuTriggerItem,
ContextMenuTopicSelectionItem,
ContextMenuColorSelectionItem,
ContentColors,
} from '@minddrop/ui';
export default () => {
return (
<ContextMenu>
<ContextMenuTrigger>
<Drop />
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
label="Add title"
icon="title"
keyboardShortcut={['Ctrl', 'T']}
onSelect={() => console.log('Add title')}
/>
<ContextMenuItem
label="Add note"
icon="note"
keyboardShortcut={['Ctrl', 'Shift', 'N']}
onSelect={() => console.log('Add note')}
/>
<ContextMenu>
<ContextMenuTriggerItem
label="Color"
icon="color-palette"
/>
<ContextMenuContent className="color-selection-submenu">
{ContentColors.map((color) => (
<ContextMenuColorSelectionItem
key={color.value}
color={color.value}
onSelect={() => console.log(color.value)}
/>
))}
</ContextMenuContent>
</ContextMenu>
<ContextMenu>
<ContextMenuTriggerItem
label="Turn into"
icon="turn-into"
/>
<ContextMenuContent>
<ContextMenuItem
label="Text"
onSelect={() => console.log('Turn into text')}
/>
<ContextMenuItem
label="Image"
onSelect={() => console.log('Turn into image')}
/>
<ContextMenuItem
label="Equation"
onSelect={() => console.log('Turn into equation')}
/>
</ContextMenuContent>
</ContextMenu>
<ContextMenuSeparator />
<ContextMenuLabel>Actions</ContextMenuLabel>
<ContextMenuItem
label="Copy link"
icon="link"
keyboardShortcut={['Ctrl', 'Shift', 'C']}
onSelect={() => console.log('Copy link')}
tooltipTitle="Copy drop link"
tooltipDescription="Paste the link into other drops to create a network or related information."
/>
<ContextMenu>
<ContextMenuTriggerItem
label="Move to"
icon="arrow-up-right"
/>
<ContextMenuContent className="topic-selection-submenu">
<ContextMenuTopicSelectionItem
label="Sailing"
onSelect={() => console.log("Move to 'Sailing'")}
>
<ContextMenuTopicSelectionItem
label="Navigation"
onSelect={() => console.log("Move to 'Navigation'")}
>
// ...
</ContextMenuTopicSelectionItem>
// ...
</ContextMenuTopicSelectionItem>
// ...
</ContextMenuContent>
</ContextMenu>
<ContextMenuItem
label="Delete"
icon="trash"
keyboardShortcut={['Del']}
onSelect={() => console.log('Delete')}
tooltipTitle="Delete drop"
tooltipDescription="Shift + Click to delete from all topics"
/>
</ContextMenuContent>
</ContextMenu>
);
};
Context menus can also be generated by passing an array of item configs as the ContextMenuContent
's content
prop. The generated menu below is equivalent to the composed example above.
See the menu component configs section for details on the different configuration objects.
import {
IconButton,
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
MenuContent,
ContentColors,
} from '@minddrop/ui';
const menu: MenuContent = [
{
type: 'menu-item',
label: 'Add title',
icon: 'title',
onSelect: () => console.log('Add title'),
keyboardShortcut: ['Ctrl', 'T'],
},
{
type: 'menu-item',
label: 'Add note',
icon: 'note',
onSelect: () => console.log('Add note'),
keyboardShortcut: ['Ctrl', 'Shift', 'N'],
},
{
type: 'menu-item',
label: 'Color',
icon: 'color-palette',
submenuContentClass: 'color-selection-submenu',
submenu: ContentColors.map((color) => ({
type: 'menu-color-selection-item',
color: color.value,
onSelect: () => console.log(color.value),
})),
},
{
type: 'menu-item',
label: 'Turn into',
icon: 'turn-into',
submenu: [
{
type: 'menu-item',
label: 'Text',
onSelect: () => console.log('Turn into text'),
},
{
type: 'menu-item',
label: 'Image',
onSelect: () => console.log('Turn into image'),
},
{
type: 'menu-item',
label: 'Equation',
onSelect: () => console.log('Turn into equation'),
},
],
},
{
type: 'menu-separator',
},
{
type: 'menu-label',
label: 'Actions',
},
{
type: 'menu-item',
label: 'Copy link',
icon: 'link',
onSelect: () => console.log('Copy link'),
keyboardShortcut: ['Ctrl', 'Shift', 'C'],
tooltipTitle: 'Copy drop link',
tooltipDescription:
'Paste the link into other drops to create a network or related information.',
},
{
type: 'menu-item',
label: 'Move to',
icon: 'arrow-up-right',
submenuContentClass: 'topic-selection-submenu',
onSelect: () => console.log('Move to'),
submenu: [
{
id: 'sailing',
label: 'Sailing',
onSelect: () => console.log("Move to 'Sailing'"),
subtopics: [
{
id: 'navigation',
label: 'Navigation',
onSelect: () => console.log("Move to 'Navigation'"),
subtopics: [
// ...
],
},
// ...
],
},
// ...
],
},
{
type: 'menu-item',
label: 'Delete',
icon: 'trash',
onSelect: () => console.log('Delete'),
keyboardShortcut: ['Del'],
secondaryLabel: 'Delete everywhere',
secondaryOnSelect: () => console.log('Delete everywhere'),
tooltipTitle: 'Delete drop',
tooltipDescription: 'Shift + Click to delete from all topics',
},
];
export default () => {
return (
<ContextMenu>
<ContextMenuTrigger>
<IconButton icon="more-vertical" label="Drop options" />
</ContextMenuTrigger>
<ContextMenuContent content={menu} />
</ContextMenu>
);
};
Contains all the parts of a context menu.
Prop | Type | Default |
---|---|---|
defaultOpen | boolean | |
open | boolean | |
onOpenChange | function | |
modal | boolean | true |
dir | enum | "ltr" |
id | string |
The area that opens the context menu. Wrap it around the target you want the context menu to open from when right-clicking (or using the relevant keyboard shortcuts).
Prop | Type | Default |
---|---|---|
asChild | boolean | true |
The component that pops out in an open context menu.
Prop | Type | Default |
---|---|---|
children | node | |
content | array | |
allowPinchZoom | boolean | false |
loop | boolean | false |
onCloseAutoFocus | function | |
onEscapeKeyDown | function | |
onPointerDownOutside | function | |
onFocusOutside | function | |
onInteractOutside | function | |
portalled | boolean | true |
forceMount | boolean | |
side | enum | "bottom" |
sideOffset | number | 0 |
align | enum | "center" |
alignOffset | number | 0 |
avoidCollisions | boolean | true |
collisionTolerance | number | 0 |
An item in the menu. Items can be selected using the mouse or keyboard. Items can be given a secondary action which is enabled when the Shift key is held down.
Prop | Type | Default |
---|---|---|
label* | node | |
onSelect* | function | |
icon | IconName | ReactElement | |
keyboardShortcut | string[] | |
secondaryLabel | node | |
secondaryOnSelect | function | |
secondaryIcon | IconName | ReactElement | |
secondaryKeyboardShortcut | string[] | |
tooltipTitle | node | |
tooltipDescription | node | |
disabled | boolean | |
textValue | string |
An item that opens a submenu. Used in combination with a nested ContextMenu
. Must be rendered inside a root ContextMenu
. Has the same props API as a regular ContextMenuItem
.
Prop | Type | Default |
---|---|---|
label* | node | |
onSelect | function | |
icon | IconName | ReactElement | |
keyboardShortcut | string[] | |
tooltipTitle | node | |
tooltipDescription | node | |
disabled | boolean | |
textValue | string |
A menu item designed for selecting a topic. Can be expanded to reveal subtopics.
Prop | Type | Default |
---|---|---|
label* | string | |
onSelect | function | |
children | React.ComponentType<ContextMenuTopicSelectionItemProps> |
A menu item designed for selecting a ContentColor
.
Prop | Type | Default |
---|---|---|
color* | ContentColor | |
onSelect | function |
Used to render a label. It won't be focusable using arrow keys.
Prop | Type | Default |
---|---|---|
children* | node |
Used to visually separate items in the context menu. Does not take any props.
Context menus can also be generated by passing an array of menu component configuration objects and React components as the ContextMenuContent
's content
prop.
To create a menu label, add a MenuLabelConfig
config object to the menu array.
const menu = [
// ...
{
type: 'menu-label',
label: 'Actions',
},
// ...
];
Prop | Type | Default |
---|---|---|
type* | menu-label | |
label* | string |
To create a horizontal separator, add a MenuSeparatorConfig
object to the menu array.
const menu = [
// ...
{
type: 'menu-separator',
},
// ...
];
Prop | Type | Default |
---|---|---|
type* | menu-separator |
To create a menu item, add a MenuItemConfig
object to the menu array.
const menu = [
// ...
{
type: 'menu-item',
label: 'Copy link',
icon: 'link',
onSelect: () => console.log('Copy link'),
keyboardShortcut: ['Ctrl', 'Shift', 'C'],
tooltipTitle: 'Copy drop link',
tooltipDescription:
'Paste the link into other drops to create a network or related information.',
},
// With submenu
{
type: 'menu-item',
label: 'Turn into',
icon: 'turn-into',
submenu: [
{
type: 'menu-item',
label: 'Text',
onSelect: () => console.log('Turn into text'),
},
{
type: 'menu-item',
label: 'Image',
onSelect: () => console.log('Turn into image'),
},
{
type: 'menu-item',
label: 'Equation',
onSelect: () => console.log('Turn into equation'),
},
],
},
// With secondary action
{
type: 'menu-item',
label: 'Delete',
icon: 'trash',
onSelect: () => console.log('Delete'),
keyboardShortcut: ['Del'],
secondaryLabel: 'Delete everywhere',
secondaryOnSelect: () => console.log('Delete everywhere'),
tooltipTitle: 'Delete drop',
tooltipDescription: 'Shift + Click to delete from all topics',
},
// ...
];
Prop | Type | Default |
---|---|---|
type* | menu-item | |
label* | node | |
icon | IconName | ReactElement | |
onSelect* | function | |
keyboardShortcut | string[] | |
secondaryLabel | node | |
secondaryOnSelect | function | |
secondaryIcon | IconName | ReactElement | |
secondaryKeyboardShortcut | string[] | |
tooltipTitle | node | |
tooltipDescription | node | |
submenu | array | |
disabled | boolean |
To create a topic selection menu item, add a MenuTopicSelectionItemConfig
object to the menu array.
const menu = [
// ...
{
type: 'menu-topic-selection-item',
label: 'Sailing',
onSelect: () => console.log("Move to 'Sailing'"),
subtopics: [
{
type: 'menu-topic-selection-item',
label: 'Sailing',
onSelect: () => console.log("Move to 'Sailing'"),
subtopics: [],
},
],
},
// ...
];
Prop | Type | Default |
---|---|---|
type* | menu-topic-selection-item | |
title* | string | |
id* | string | |
subtopics* | TopicSelectionItem[] | |
onSelect | function |
To create a color selection menu item, add a MenuColorSelectionItemConfig
object to the menu array.
const menu = [
// ...
{
type: 'menu-color-selection-item',
color: 'green',
onSelect: () => console.log('green'),
},
// ...
];
Prop | Type | Default |
---|---|---|
type* | menu-color-selection-item | |
color* | ContentColor | |
onSelect | function |
Radix exposes a CSS custom property --radix-context-menu-content-transform-origin
. Use it to animate the content from its computed origin based on side
, sideOffset
, align
, alignOffset
and any collisions.
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
.context-menu {
transform-origin: var(
--radix-context-menu-content-transform-origin
);
animation: scaleIn 0.5s ease-out;
}
Radix exposes data-side
and data-align
attributes. Their values will change at runtime to reflect collisions. Use them to create collision and direction-aware animations.
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(0);
}
to {
opacity: 1;
transform: translateY(-10px);
}
}
.context-menu {
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.context-menu[data-side='bottom'] {
animation: slideDown;
}
.context-menu[data-side='top'] {
animation: slideUp;
}
Adheres to the Menu WAI-ARIA design pattern and uses roving tabindex to manage focus movement among menu items.
Key | Description |
---|---|
Space | Activates the focused item. |
Enter | Activates the focused item. |
ArrowDown | Moves focus to the next item. |
ArrowUp | Moves focus to the previous item. |
ArrowRightArrowLeft | When focus is on DropdownMenuTriggerItem , opens or closes the submenu depending on reading direction. When focus is on DropdownMenuTopicSelectionItem , expands or collapses the topic's subtopics. |
Esc | Closes the context menu |