GrContextMenu
Brings actions to the pointer: right-click a row, a tree node or a canvas.
Overview
When to take it
- actions over a row of a list or a node of a tree — a right click where the user is already looking, instead of a trip to the ”⋯” button at the end of the row;
- quick operations on the selection — rename, duplicate, delete: the same items as in the toolbar, but without moving the mouse across the whole screen;
- actions over an area — a canvas, a map, the free space of a list: there is nothing to click, and a menu at the cursor remains the only place for “Paste”;
- a menu that depends on what was clicked —
beforeOpengives the target away before the opening, anditemshave time to be assembled for the particular row.
When to take something else
| Need | Take |
|---|---|
| The menu hangs on a visible button or ”⋯” | GrDropdownMenu |
| The items are non-standard, you write the markup | GrDropdown |
| There is a form, a filter or a palette inside — not a menu | GrPopover |
| A search across the commands of the whole application | GrCommandPalette |
| A confirmation of a dangerous action | GrConfirmDialog |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrContextMenu :items="items"><GrButton variant="outline">Row actions</GrButton></GrContextMenu>Examples 3 each with a live demo and its code
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
placement | Placement | undefined | "bottom-start" | The side to open towards from the point of invocation. `bottom-start` by default — down and to the right of the cursor. |
disabled | boolean | undefined | false | The menu does not open by any means — neither by mouse, nor by keyboard, nor from code. |
openv-model | boolean | undefined | undefined | Whether the menu is open. Without the prop the component runs the state itself. |
ariaLabel | string | undefined | undefined | The accessible name of the `role="menu"` panel. By default — the translated "Context menu". |
items | GrDropdownMenuEntry[] | undefined | undefined | A declarative menu. The `#content` slot is stronger. |
trigger | GrContextMenuTrigger | undefined | "contextmenu" | What opens the menu: `contextmenu` — a right click on the area, `manual` — only from code. `Shift+F10` and the `ContextMenu` key work in both modes. `contextmenu` by default. |
offsetPx | number | undefined | 0 | The gap from the anchor. Zero by default: the menu sticks to the cursor. |
minWidth | string | number | undefined | "11rem" | A menu made of short words must not be one word wide. |
labelledBy | string | undefined | undefined | The `id` of a visible heading that names the menu — an alternative to `ariaLabel`. |
teleportTo | string | HTMLElement | undefined | undefined | A targeted override of the mount point; by default — the shared overlay portal. |
contentClass | string | undefined | undefined | Extra classes for the panel. |
listClass | string | undefined | undefined | Extra classes for the list of items inside the panel. |
dividers | boolean | undefined | false | Separators between the items. |
closeOnScroll | boolean | undefined | true | Close on scrolling of the page. The anchor is a point of the viewport, and the panel would keep hanging on the screen while the content under it moves away. Native menus behave the same way. |
allowNativeMenu | boolean | undefined | true | Pass `Shift`+right click through to the browser. |
Slots
| Slot | Type | Description |
|---|---|---|
default | { open: boolean; } | The area the menu is called over. |
content | { close: () => void; } | The content of the menu. |
Events
| Event | Signature | Description |
|---|---|---|
update:open | (event: "update:open", value: boolean): void | — |
select | (event: "select", item: GrDropdownMenuAction): void | — |
beforeOpen | (event: "beforeOpen", context: GrContextMenuOpenContext): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
openAt | (at: MouseEvent | GrFloatingAnchorRect) => void | Open at the cursor point (`MouseEvent`) or at an arbitrary viewport rectangle. |
openAtElement | (element: Element) => void | Open at the rectangle of an element — the keyboard path for external composites. |
close | () => void | Close the menu from code; `update:open` will arrive with `false`. |
toggle | () => void | Open a closed menu or close an open one. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrContextMenuTrigger, GrContextMenuSource, GrContextMenuOpenContext } from '@feugene/granularity/components/GrContextMenu'Filedist/types/components/GrContextMenu/GrContextMenu.vue.d.ts
type GrContextMenuTrigger = 'contextmenu' | 'manual';type GrContextMenuSource = 'pointer' | 'keyboard' | 'api';interface GrContextMenuOpenContext {
source: GrContextMenuSource;
target: HTMLElement | null;
anchor: GrFloatingAnchorRect;
event: MouseEvent | KeyboardEvent | null;
}interface GrContextMenuProps {
items?: GrDropdownMenuEntry[];
open?: boolean;
trigger?: GrContextMenuTrigger;
disabled?: boolean;
placement?: UseFloatingPlacement;
offsetPx?: number;
minWidth?: number | string;
ariaLabel?: string;
labelledBy?: string;
teleportTo?: string | HTMLElement;
contentClass?: string;
listClass?: string;
dividers?: boolean;
closeOnScroll?: boolean;
allowNativeMenu?: boolean;
}interface GrContextMenuEmits {
(e: 'update:open', value: boolean): void;
(e: 'beforeOpen', context: GrContextMenuOpenContext): void;
(e: 'select', item: GrDropdownMenuAction): void;
}Filedist/types/components/GrContextMenu/config.d.ts
const grContextMenuConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrContextMenu">;Filedist/types/components/GrContextMenu/index.d.ts
type GrContextMenuInstance = ComponentExposed<typeof GrContextMenuComponent>;Accessibility
APG pattern
menu
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
The items are assembled for the target rather than set once and for all
beforeOpen arrives before the opening, and it is the only moment at which the menu can be assembled for whatever
was clicked: a folder has one set of actions, a file another. No separate way to cancel the opening is needed — an empty
model simply will not open.
<GrContextMenu :items="items" @before-open="onBeforeOpen" @select="onSelect">
<GrTree :data="data" node-key="id"/>
</GrContextMenu>function onBeforeOpen(context: GrContextMenuOpenContext): void {
// The target is taken from the DOM rather than from the mouse event: the same
// code serves Shift+F10, which has no mouse event at all.
const row = context.target?.closest('[data-gr-tree-node-key]')
items.value = row ? itemsFor(row.dataset.grTreeNodeKey!) : []
}The props reach the component on the next render, so the menu opens one tick after the call — otherwise it would show the items of the previous target.
Two ways in, and they do not duplicate each other
The wrapper — the content of the default slot. It catches the right click and, more importantly, the keyboard:
keydown bubbles from the focused element, and only an ancestor can catch it.
openAt(event) through a ref — when the target is not derived from the DOM: the event came from something other than
a pointer, the coordinates are computed by the page itself (a canvas, a map), or the menu belongs to an object that is
not in the markup.
<GrContextMenu ref="menu" trigger="manual" :items="items" @select="onSelect">
<canvas @contextmenu.prevent="onCanvasMenu"/>
</GrContextMenu>trigger="manual" switches off opening by pointer only. The keyboard path always stays — otherwise the menu would be
unavailable from the keyboard, and that handler would have to be written again on every page.
The keyboard is an acceptance condition, not a polish
A menu available by right click alone does not exist for the keyboard. The wrapper listens for Shift+F10 and the
ContextMenu key, and the anchor becomes the rectangle of the focused element rather than a point: the menu belongs
to the row and flips together with it when there is not enough room below.
Inside an input field the call is not intercepted — the native menu with spell checking and the clipboard is more useful there.
The rows a menu opens over are worth making focusable — otherwise there is nowhere to return the focus after closing,
and Tab will start from the beginning of the document. GrTree already meets that condition.
Shift+right click is given to the browser
The native menu is not an obstacle but a tool: translating the page, viewing the source, saving an image.
Shift+right click is passed through to the browser (in Firefox that is a documented way), and it is switched off with
the allowNativeMenu prop.
Ctrl is deliberately not part of the escape hatch: on macOS Ctrl+click is the right click, and the menu would stop
opening for part of the users.
Limits
- a long menu at the bottom edge of the screen is shifted rather than shrunk with a scroll. Limiting the height of a panel is not implemented for any anchored layer in the package;
- the component does not emulate a long press on a touch screen.
contextmenuon a long press is sent by Android Chrome and is not sent by iOS Safari, and an emulation of your own conflicts with scrolling and text selection. Duplicate the actions with a visible button; - an empty menu does not open. A panel without a single item has nothing to focus, and it would become a trap with
only
Escfor a way out.
Props in this example