GrContextMenu

  • core
  • overlays
  • API · 15 props
  • ⌨ keys

Brings actions to the pointer: right-click a row, a tree node or a canvas.

import { GrContextMenu } from '@feugene/granularity/components/GrContextMenu'

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 — beforeOpen gives the target away before the opening, and items have time to be assembled for the particular row.

When to take something else

NeedTake
The menu hangs on a visible button or ”⋯”GrDropdownMenu
The items are non-standard, you write the markupGrDropdown
There is a form, a filter or a palette inside — not a menuGrPopover
A search across the commands of the whole applicationGrCommandPalette
A confirmation of a dangerous actionGrConfirmDialog

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 3 each with a live demo and its code

  • A menu built for what was clicked

    A folder and a file have different actions. before-open arrives before the menu opens, and the page rebuilds items for the row under the pointer — read from the DOM, so Shift+F10 on a focused row takes the same path. The model carries a group, a «Move to» submenu, shortcuts and a dangerous item; the empty space below the list gets its own menu.

    ContractsRight-click a row or the empty space · Shift+F10 on a focused row
    • Clients18 files
    • Suppliers7 files
    • Northwind MSA 2026.pdf412 KB · signed
    • Contoso NDA.pdf96 KB · awaiting signature
  • Slots the page opens the menu for

    The week is drawn by the page, so it opens the menu itself with openAt(event) and remembers the slot; trigger="manual" turns off opening by the wrapper. An empty slot offers «Book» and «Block», a taken one offers «Clear» — the keyboard path stays in either case.

    Lena Brandt · week 41Right-click a slot to book or block it
    MonTueWedThuFri09:00
     
     
     
     
     
    11:00
     
    Boiler service · Kalla 
     
     
     
    13:00
     
     
     
    Install · Orsa 
     
    15:00
     
     
     
     
     
  • The same actions from a right click and a button

    A touch screen has no right button, so every row also has «⋯»: openAtElement anchors the same menu to it instead of the pointer. «Pin to top» is a menuitemcheckbox whose checked follows the row.

    Sent invoicesRight-click a row, or use its «⋯» button
    Northwind Traders · pinnedINV-0418 · €1,284.50
    Tailspin ToysINV-0417 · €2,150.00
    Contoso LtdINV-0415 · €432.00

API generated from the package · cannot drift

Props

PropTypedefaultDescription
placementPlacement | 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.
disabledboolean | undefinedfalseThe menu does not open by any means — neither by mouse, nor by keyboard, nor from code.
openv-modelboolean | undefinedundefinedWhether the menu is open. Without the prop the component runs the state itself.
ariaLabelstring | undefinedundefinedThe accessible name of the `role="menu"` panel. By default — the translated "Context menu".
itemsGrDropdownMenuEntry[] | undefinedundefinedA declarative menu. The `#content` slot is stronger.
triggerGrContextMenuTrigger | 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.
offsetPxnumber | undefined0The gap from the anchor. Zero by default: the menu sticks to the cursor.
minWidthstring | number | undefined"11rem"A menu made of short words must not be one word wide.
labelledBystring | undefinedundefinedThe `id` of a visible heading that names the menu — an alternative to `ariaLabel`.
teleportTostring | HTMLElement | undefinedundefinedA targeted override of the mount point; by default — the shared overlay portal.
contentClassstring | undefinedundefinedExtra classes for the panel.
listClassstring | undefinedundefinedExtra classes for the list of items inside the panel.
dividersboolean | undefinedfalseSeparators between the items.
closeOnScrollboolean | undefinedtrueClose 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.

Slots

SlotTypeDescription
default{ open: boolean; }The area the menu is called over.
content{ close: () => void; }The content of the menu.

Events

EventSignatureDescription
update:open(event: "update:open", value: boolean): void—
select(event: "select", item: GrDropdownMenuAction): void—
beforeOpen(event: "beforeOpen", context: GrContextMenuOpenContext): void—

Methods

MethodsSignatureDescription
openAt(at: MouseEvent | GrFloatingAnchorRect) => voidOpen at the cursor point (`MouseEvent`) or at an arbitrary viewport rectangle.
openAtElement(element: Element) => voidOpen at the rectangle of an element — the keyboard path for external composites.
close() => voidClose the menu from code; `update:open` will arrive with `false`.
toggle() => voidOpen 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
import type { GrContextMenuTrigger, GrContextMenuSource, GrContextMenuOpenContext } from '@feugene/granularity/components/GrContextMenu'

Filedist/types/components/GrContextMenu/GrContextMenu.vue.d.ts

GrContextMenuTriggertype
type GrContextMenuTrigger = 'contextmenu' | 'manual';
GrContextMenuSourcetype
type GrContextMenuSource = 'pointer' | 'keyboard' | 'api';
GrContextMenuOpenContextinterface
interface GrContextMenuOpenContext {
    source: GrContextMenuSource;
    target: HTMLElement | null;
    anchor: GrFloatingAnchorRect;
    event: MouseEvent | KeyboardEvent | null;
}
GrContextMenuPropsinterface
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;
}
GrContextMenuEmitsinterface
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

grContextMenuConfigconst
const grContextMenuConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrContextMenu">;

Filedist/types/components/GrContextMenu/index.d.ts

GrContextMenuInstancetype
type GrContextMenuInstance = ComponentExposed<typeof GrContextMenuComponent>;

Accessibility

APG pattern

menu

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.

vue
<GrContextMenu :items="items" @before-open="onBeforeOpen" @select="onSelect">
  <GrTree :data="data" node-key="id"/>
</GrContextMenu>
ts
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.

vue
<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.

Scrolling closes the menu

The anchor is a point of the viewport that has no element, so on scrolling the panel would keep hanging on the screen while the content under it moves away. Native menus behave the same way; the behaviour is switched off with the closeOnScroll prop.

The second level: submenus

The model of the items is shared with GrDropdownMenu, so submenus here are the same thing: an item with non-empty children unfolds the next level, and select does not arrive for it.

ts
const items: GrDropdownMenuEntry[] = [
  { key: 'rename', label: 'Rename' },
  { key: 'export', label: 'Export', children: [{ key: 'pdf', label: 'PDF' }] },
]

The keyboard, the corridor for the cursor and the rules for closing the chain are described there too — “The second level: submenus”. Only the root differs here: a choice in a submenu closes the context menu, and Escape on the top level returns to the lower one without closing everything.

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. contextmenu on 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 Esc for a way out.