Context Menu Plugin
The Context Menu plugin adds a customizable right-click menu to your grid cells. Build anything from simple copy/paste actions to complex nested menus with conditional visibility, icons, and keyboard shortcuts.
Installation
Section titled “Installation”import '@toolbox-web/grid/features/context-menu';Basic Usage
Section titled “Basic Usage”Define your menu items as an array—each item has an id, label, and action callback that receives context about the clicked cell (row data, column info, cell value, etc.). Add separators between groups of actions for visual clarity.
import { queryGrid } from '@toolbox-web/grid';
const grid = queryGrid('tbw-grid');
function removeRow(rowIndex: number) { grid.rows = grid.rows.filter((_, i) => i !== rowIndex);}
grid.gridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' } ], features: { contextMenu: { items: [ { id: 'copy', name: 'Copy Cell', action: (ctx) => navigator.clipboard.writeText(String(ctx.value)) }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete Row', action: (ctx) => removeRow(ctx.rowIndex) }, ], }, },};import '@toolbox-web/grid-react/features/context-menu';import { DataGrid } from '@toolbox-web/grid-react';import type { GridConfig } from '@toolbox-web/grid-react';
function MyGrid({ data, onDelete }) { const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' } ], features: { contextMenu: { items: [ { id: 'copy', name: 'Copy Cell', action: (ctx) => navigator.clipboard.writeText(String(ctx.value)) }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete Row', action: (ctx) => onDelete(ctx.rowIndex) }, ], }, }, };
return <DataGrid rows={data} gridConfig={gridConfig} style={{ height: '400px' }} />;}<script setup>import '@toolbox-web/grid-vue/features/context-menu';import { TbwGrid } from '@toolbox-web/grid-vue';import type { GridConfig } from '@toolbox-web/grid-vue';
const data = [ { name: 'Alice', email: 'alice@example.com', status: 'active' }, { name: 'Bob', email: 'bob@example.com', status: 'inactive' },];
const gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' }, ], features: { contextMenu: { items: [ { id: 'copy', name: 'Copy Cell', action: (ctx) => navigator.clipboard.writeText(String(ctx.value)) }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete Row', action: (ctx) => console.log('Delete row:', ctx.rowIndex) }, ], }, },};</script>
<template> <TbwGrid :rows="data" :grid-config="gridConfig" style="height: 400px" /></template>import '@toolbox-web/grid-angular/features/context-menu';import { Component, output } from '@angular/core';import { Grid } from '@toolbox-web/grid-angular';import type { GridConfig } from '@toolbox-web/grid-angular';import type { ContextMenuItem } from '@toolbox-web/grid/plugins/context-menu';
@Component({ selector: 'app-my-grid', imports: [Grid], template: ` <tbw-grid [rows]="rows" [gridConfig]="gridConfig" style="height: 400px; display: block;"> </tbw-grid> `,})export class MyGridComponent { deleteRow = output<number>(); rows = [...];
gridConfig: GridConfig = { columns: [ { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' } ], features: { contextMenu: { items: [ { id: 'copy', name: 'Copy Cell', action: (ctx: any) => navigator.clipboard.writeText(String(ctx.value)) }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete Row', action: (ctx: any) => this.deleteRow.emit(ctx.rowIndex) }, ] as ContextMenuItem[], }, }, };}Default Context Menu
Section titled “Default Context Menu”<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/context-menu';
// Sample data for context menu demosconst sampleData = [ { id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' }, { id: 2, name: 'Bob', email: 'bob@example.com', status: 'pending' }, { id: 3, name: 'Carol', email: 'carol@example.com', status: 'active' }, { id: 4, name: 'Dan', email: 'dan@example.com', status: 'inactive' },];const columns = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' },];// Default menu itemsconst defaultMenuItems = [ { id: 'copy', name: 'Copy Row', icon: '📋', shortcut: 'Ctrl+C', action: (params: ContextMenuParams) => console.log('Copy', params.row), }, { id: 'edit', name: 'Edit Row', icon: '✏️', action: (params: ContextMenuParams) => console.log('Edit', params.row) }, { id: 'sep1', name: '', separator: true }, { id: 'duplicate', name: 'Duplicate', icon: '📄', action: (params: ContextMenuParams) => console.log('Duplicate', params.row), }, { id: 'sep2', name: '', separator: true }, { id: 'delete', name: 'Delete', icon: '🗑️', cssClass: 'danger', action: (params: ContextMenuParams) => console.log('Delete', params.row), },];
const grid = queryGrid('tbw-grid');
grid.gridConfig = { columns, features: { contextMenu: { items: defaultMenuItems } }, }; grid.rows = sampleData;Right-click any cell to see the context menu.
With Submenus
Section titled “With Submenus”<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/context-menu';
// Sample data for context menu demosconst sampleData = [ { id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' }, { id: 2, name: 'Bob', email: 'bob@example.com', status: 'pending' }, { id: 3, name: 'Carol', email: 'carol@example.com', status: 'active' }, { id: 4, name: 'Dan', email: 'dan@example.com', status: 'inactive' },];const columns = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' },];const grid = queryGrid('tbw-grid');
const menuItems = [ { id: 'view', name: 'View', icon: '👁️', action: () => console.log('View') }, { id: 'edit', name: 'Edit', icon: '✏️', action: () => console.log('Edit') }, { id: 'sep1', name: '', separator: true }, { id: 'export', name: 'Export', icon: '📤', subMenu: [ { id: 'csv', name: 'As CSV', action: () => console.log('Export CSV') }, { id: 'json', name: 'As JSON', action: () => console.log('Export JSON') }, { id: 'excel', name: 'As Excel', action: () => console.log('Export Excel') }, ], }, { id: 'share', name: 'Share', icon: '🔗', subMenu: [ { id: 'email', name: 'Email', icon: '📧', action: () => console.log('Share Email') }, { id: 'slack', name: 'Slack', icon: '💬', action: () => console.log('Share Slack') }, ], }, ];
grid.gridConfig = { columns, features: { contextMenu: { items: menuItems } }, }; grid.rows = sampleData;Nested menu items for complex actions.
Conditional Items
Section titled “Conditional Items”<tbw-grid style="height: 350px;"></tbw-grid>import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';
import '@toolbox-web/grid/features/context-menu';
// Sample data for context menu demosconst sampleData = [ { id: 1, name: 'Alice', email: 'alice@example.com', status: 'active' }, { id: 2, name: 'Bob', email: 'bob@example.com', status: 'pending' }, { id: 3, name: 'Carol', email: 'carol@example.com', status: 'active' }, { id: 4, name: 'Dan', email: 'dan@example.com', status: 'inactive' },];const columns = [ { field: 'id', header: 'ID', type: 'number' }, { field: 'name', header: 'Name' }, { field: 'email', header: 'Email' }, { field: 'status', header: 'Status' },];const grid = queryGrid('tbw-grid');
const menuItems = [ { id: 'activate', name: 'Activate', icon: '✅', disabled: (params) => (params.row)?.status === 'active', action: (p) => console.log('Activate', p.row), }, { id: 'deactivate', name: 'Deactivate', icon: '⏸️', disabled: (params) => (params.row)?.status !== 'active', action: (p) => console.log('Deactivate', p.row), }, { id: 'sep1', name: '', separator: true }, { id: 'delete', name: 'Delete', icon: '🗑️', cssClass: 'danger', action: (p) => console.log('Delete', p.row) }, ];
grid.gridConfig = { columns, features: { contextMenu: { items: menuItems } }, }; grid.rows = sampleData;Show/hide items based on context.
Plugin-Contributed Items
Section titled “Plugin-Contributed Items”Right-click any cell to see the context menu. Items from FilteringPlugin, VisibilityPlugin, and PinnedColumnsPlugin are automatically contributed alongside custom items.
<tbw-grid style="height: 350px;"></tbw-grid> import '@toolbox-web/grid';import { queryGrid } from '@toolbox-web/grid';import '@toolbox-web/grid/features/context-menu';import '@toolbox-web/grid/features/filtering';import '@toolbox-web/grid/features/pinned-columns';import '@toolbox-web/grid/features/shell';import '@toolbox-web/grid/features/visibility';
const grid = queryGrid('tbw-grid'); if (grid) { grid.gridConfig = { columns: [ { field: 'id', header: 'ID', type: 'number', width: 80 }, { field: 'name', header: 'Name', sortable: true }, { field: 'email', header: 'Email', sortable: true }, { field: 'status', header: 'Status', sortable: true }, ], features: { shell: true, filtering: true, visibility: true, pinnedColumns: true, contextMenu: { items: [ { id: 'copy-row', name: 'Copy Row', icon: '📋', action: (params) => { console.log('Copy row:', params.row); }, }, { id: 'highlight', name: 'Highlight Row', icon: '🔆', action: (params) => { const rowEl = params.rowElement; if (rowEl) { rowEl.style.background = 'light-dark(#fff3cd, #4a3f00)'; setTimeout(() => { rowEl.style.background = ''; }, 2000); } }, }, ], }, }, };
grid.rows = [ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', status: 'active' }, { id: 2, name: 'Bob Smith', email: 'bob@example.com', status: 'pending' }, { id: 3, name: 'Carol Davis', email: 'carol@example.com', status: 'active' }, { id: 4, name: 'Dan Wilson', email: 'dan@example.com', status: 'inactive' }, { id: 5, name: 'Eve Brown', email: 'eve@example.com', status: 'active' }, { id: 6, name: 'Frank Miller', email: 'frank@example.com', status: 'pending' }, ]; }Plugins like FilteringPlugin, VisibilityPlugin, and PinnedColumnsPlugin can contribute their own items to the context menu automatically.
Configuration Options
Section titled “Configuration Options”| Option | Type | Default | Description |
|---|---|---|---|
items | ContextMenuItem[] | Copy + Export CSV | Menu items to display |
TypeScript Interfaces
Section titled “TypeScript Interfaces”ContextMenuItem— Menu item definition (id, label, icon, action, submenus, etc.)ContextMenuParams— Context passed to action callbacks (row, column, value, selection, etc.)ContextMenuConfig— Plugin configuration options
Selection Sync
Section titled “Selection Sync”When used with the SelectionPlugin (row mode), the context menu automatically syncs
the selection on right-click:
| Scenario | Behavior |
|---|---|
| Right-click a selected row | Multi-selection preserved |
| Right-click an unselected row | Selects only that row |
| No SelectionPlugin loaded | selectedRows = [rowIndex] |
| Right-click on header | selectedRows = [] |
This uses the plugin query system for loose coupling — no hard dependency on SelectionPlugin.
Conditional Items
Section titled “Conditional Items”features: { contextMenu: { items: [ { id: 'edit', name: 'Edit', hidden: (params) => params.column.editable !== true, }, { id: 'delete', name: 'Delete', disabled: (params) => params.row.locked === true, }, ], },},Events
Section titled “Events”| Event | Detail | Description |
|---|---|---|
context-menu-open | { params, items } | Menu opened |
Styling
Section titled “Styling”The context menu supports CSS custom properties for theming. Override these on tbw-grid or a parent container:
CSS Custom Properties
Section titled “CSS Custom Properties”| Property | Default | Description |
|---|---|---|
--tbw-context-menu-bg | var(--tbw-color-panel-bg) | Menu background |
--tbw-context-menu-fg | var(--tbw-color-fg) | Menu text color |
--tbw-context-menu-border | var(--tbw-color-border) | Menu border |
--tbw-context-menu-hover | var(--tbw-color-row-hover) | Item hover background |
--tbw-menu-min-width | 10rem | Minimum menu width |
--tbw-menu-item-padding | 0.375rem 0.75rem | Menu item padding |
--tbw-menu-item-gap | 0.5rem | Gap between icon and label |
--tbw-font-size-sm | 0.9285em | Menu font size |
--tbw-font-size-xs | 0.7857em | Shortcut text size |
--tbw-icon-size | 1em | Icon width |
Example
Section titled “Example”tbw-grid { /* Custom context menu styling */ --tbw-context-menu-bg: #2d2d2d; --tbw-context-menu-fg: #ffffff; --tbw-context-menu-border: #444444; --tbw-context-menu-hover: #3d3d3d; --tbw-menu-item-padding: 0.5rem 1rem;}CSS Classes
Section titled “CSS Classes”The menu uses these class names for advanced customization:
| Class | Element |
|---|---|
.tbw-context-menu | Menu container |
.tbw-context-menu-item | Menu item row |
.tbw-context-menu-item.disabled | Disabled item |
.tbw-context-menu-item.danger | Danger/delete action |
.tbw-context-menu-icon | Item icon container |
.tbw-context-menu-label | Item label text |
.tbw-context-menu-shortcut | Keyboard shortcut |
.tbw-context-menu-separator | Divider line |
Accessibility
Section titled “Accessibility”When the plugin is loaded, the grid’s right-click target advertises the popup trigger to assistive technologies following the WAI-ARIA Menu Button pattern:
aria-haspopup="menu"is set on the grid root once the plugin is attached.aria-expandedis toggled between"false"and"true"to reflect the menu’s open state — including when it closes via Esc, click outside, or a scroll event.- Menu items inside the popup use
role="menuitem"and are reachable with the arrow keys; Enter / Space activates and Esc closes (see Accessibility guide).
The plugin also respects the keyboard-open path: Shift + F10 and the dedicated ☰ Menu key open the menu at the focused cell with focus moved to the first item.
Touch input
Section titled “Touch input”On a touch device the menu opens with a long-press, with no extra
configuration. This is not a polyfill: browsers already synthesise a native
contextmenu event from a long-press, exactly as they do from a right-click, so
the same listener serves both.
Because a long-press is an overloaded gesture, the grid resolves it in a fixed order:
| You long-press | What opens | When |
|---|---|---|
| A header cell | Column header menu | Once the header-menu plugin ships (tracked in #270) |
| A row | Touch selection mode | SelectionPlugin is registered with mode: 'row' |
| A cell | Cell range painting | SelectionPlugin is registered with mode: 'cell' or 'range' |
| A row or cell | This menu | None of the above applies |
The grid offers a long-press to its plugins after 400 ms, and only suppresses
the browser’s contextmenu if one of them claims it. So this menu needs no
opt-in — it simply opens whenever nothing else wanted the gesture.
See the Touch input guide for the full gesture table.
See Also
Section titled “See Also”- Selection — Row and cell selection
- Editing — Inline cell editing
- Clipboard — Copy/paste cells
- Touch input — Long-press and gesture reference