Context Menu
A menu opened by right-clicking or long-pressing an area, built from a slotted c2-menu or a render function per clicked spot.
c2-context-menu wraps an area and opens a menu where that area is right-clicked, long-pressed on a touch screen, or asked for with the keyboard’s context-menu key or Shift+F10. Only its default-slot children open it; the rest of the page, and the menu itself, keep the browser’s behaviour. The element draws no box of its own (display: contents), so it can wrap a table, a canvas or a whole panel without changing the layout.
The menu is a c2-menu placed at the pointer, so it has the same keyboard support, submenus, checkable rows and light dismiss, and it flips to stay on screen. Give it one of two ways, or both:
- Static — a
c2-menuslotted intomenu: the same commands wherever the area is clicked, such as zoom in and zoom out on a chart or canvas. - Dynamic — a
renderContextMenufunction, called on every opening with the context (the element under the pointer, the data it stands for, the coordinates and the trigger), which returns the rows for that spot. When it returns nothing the slotted menu is used, and when there is neither the browser shows its own menu.
Every row activated fires context-menu-select, which carries the row’s value together with the context the menu was opened on, so one listener knows both what was chosen and where.
Import
import '@c2n/components/context-menu'The entry also registers c2-menu, which it renders internally.
Usage
Right-click (or long-press) inside each example. The table row builds its menu from the cell that was clicked.
A menu per clicked spot
renderContextMenu is a property, since it holds a function. It receives a ContextMenuContext and returns rows — a Lit template, a DOM node or an array of them — or null to fall back to the slotted menu:
import { html } from 'lit'
import type { ContextMenu } from '@c2n/components/context-menu'
import type { TableCellEventDetail } from '@c2n/components/table'
const menu = document.querySelector<ContextMenu>('c2-context-menu')!
menu.renderContextMenu = ({ data, source }) => {
if (source?.localName !== 'c2-table') return null // the header or empty space: no menu
const cell = data as TableCellEventDetail
return html`
<c2-menu-item value="copy">Copy “${cell.value}”</c2-menu-item>
<c2-menu-item value="filter">Filter by ${cell.column.header}</c2-menu-item>
<hr />
<c2-menu-item value="remove" destructive>Remove ${cell.key}</c2-menu-item>
`
}
menu.addEventListener('context-menu-select', ({ detail }) => {
const cell = detail.context.data as TableCellEventDetail
if (detail.value === 'copy') navigator.clipboard.writeText(String(cell.value))
})The context holds:
| Field | Meaning |
|---|---|
target | The innermost element under the pointer, inside shadow roots included. |
data | What that element stands for, as answered by the component that owns it — see below. |
source | The component that answered, or null. |
x, y | Viewport coordinates the menu opens at. |
trigger | pointer, touch (long press), keyboard (context-menu key or Shift+F10) or api (show(x, y, target)). |
originalEvent | The event that opened the menu. |
For plain markup, read target directly — target.closest('[data-file]') finds the row of a file list.
Data from the component under the pointer
Before opening, the menu dispatches a composed c2-context-menu-request event on the node under the pointer. It crosses shadow roots, so a component that owns that node can say what it stands for. c2-table answers for its body cells with the same detail as cell-click — row, rowIndex, key, column and value — so a wrapped table needs no wiring at all. The innermost answer wins, so a component inside a table cell can answer for itself.
A component of your own answers with provideContextMenuData from @c2n/core/context-menu-helper.js, without depending on this package:
import { provideContextMenuData } from '@c2n/core/context-menu-helper.js'
// Inside a Lit template: this chart answers with the point under the pointer.
html`<canvas @c2-context-menu-request=${(event: Event) => provideContextMenuData(event, this, this.pointAt(event))}></canvas>`Because the data is asked for rather than pushed, a right click, a long press, the keyboard and show() all carry the same context.
Which menu wins
A nested c2-context-menu opens instead of the one around it, and any handler that already called preventDefault() on the contextmenu event keeps both closed. context-menu-open fires before the menu opens and is cancelable, and disabled lets the browser’s own menu through. A long press waits long-press-duration milliseconds (500 by default) and is cancelled by a finger that moves, which is a scroll; the click the lifted finger produces is swallowed, so a long press on a row does not also open it.
The surface is styled through the --c2-menu--* variables, set on the c2-context-menu or anywhere above it.