Context Menu
ContextMenuManager provides a right-click context menu rendered as a DOM overlay. The menu supports keyboard navigation (arrow keys to move, Enter to select, Escape to close) and adapts its items based on click context — cell, header, row-number, or corner.
View source code
import { useRef, useEffect, useState } from 'react';import { Spreadsheet } from '@witqq/spreadsheet-react';import type { SpreadsheetRef } from '@witqq/spreadsheet-react';import { DemoWrapper } from './DemoWrapper';import { generateEmployees, employeeColumns } from './generate-data';import { useSiteTheme } from './useSiteTheme';
const data = generateEmployees(50);
export function ContextMenuDemo() { const { witTheme } = useSiteTheme(); const tableRef = useRef<SpreadsheetRef>(null); const [lastAction, setLastAction] = useState( 'Right-click any cell, header, or row number to see the context menu.', );
useEffect(() => { const engine = tableRef.current?.getInstance(); if (!engine) return; const cm = engine.getContextMenuManager(); if (!cm) return;
cm.registerItem({ id: 'highlight-row', label: '🟡 Highlight Row', contexts: ['cell', 'row-number'], action: (ctx) => setLastAction(`Highlighted row ${(ctx.row ?? 0) + 1}`), });
cm.registerItem({ id: 'column-info', label: 'ℹ️ Column Info', contexts: ['header'], action: (ctx) => { const col = employeeColumns[ctx.col ?? 0]; setLastAction(`Column: ${col?.title ?? 'unknown'}, type: ${col?.type ?? 'string'}`); }, });
cm.registerItem({ id: 'select-all', label: '☐ Select All', shortcut: 'Ctrl+A', contexts: ['corner'], action: () => setLastAction('Select all triggered from corner'), });
return () => { cm.unregisterItem('highlight-row'); cm.unregisterItem('column-info'); cm.unregisterItem('select-all'); }; }, []);
return ( <DemoWrapper title="Live Demo" description="Right-click cells, headers, row numbers, or the corner to see context-specific menus with custom items." height={440} > <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ padding: '0.5rem 0.75rem', fontSize: '0.8rem', color: '#64748b', borderBottom: '1px solid #e2e8f0', flexShrink: 0, }} > {lastAction} </div> <div style={{ flex: 1 }}> <Spreadsheet theme={witTheme} ref={tableRef} columns={employeeColumns} data={data} showRowNumbers style={{ width: '100%', height: '100%' }} /> </div> </div> </DemoWrapper> );}Registering Custom Items
Section titled “Registering Custom Items”Add custom items to the context menu through the engine API:
import { useRef, useEffect } from 'react';import { Spreadsheet, SpreadsheetRef } from '@witqq/spreadsheet-react';
function App() { const ref = useRef<SpreadsheetRef>(null);
useEffect(() => { const cm = ref.current?.getInstance().getContextMenuManager();
cm?.registerItem({ id: 'insert-row-above', label: 'Insert Row Above', shortcut: 'Ctrl+Shift+I', contexts: ['cell', 'row-number'], action: (ctx) => { console.log('Insert row above', ctx.row); }, });
cm?.registerItem({ id: 'format-column', label: 'Format Column', icon: '🎨', contexts: ['header'], action: (ctx) => { console.log('Format column', ctx.col); }, isDisabled: (ctx) => ctx.col === 0, });
return () => { cm?.unregisterItem('insert-row-above'); cm?.unregisterItem('format-column'); }; }, []);
return <Spreadsheet ref={ref} columns={columns} data={data} />;}Context Regions
Section titled “Context Regions”Items appear based on their contexts array:
| Context | Trigger Area | Typical Actions |
|---|---|---|
cell | Any data cell | Copy, paste, insert, delete |
header | Column header | Sort, filter, resize, format |
row-number | Row number gutter | Insert row, delete row |
corner | Top-left corner cell | Select all, clear all |
API Reference
Section titled “API Reference”ContextMenuManager
Section titled “ContextMenuManager”| Method | Signature | Description |
|---|---|---|
registerItem | (item: ContextMenuItem) => void | Add menu item |
unregisterItem | (id: string) => void | Remove menu item |
getItems | () => ReadonlyMap<string, ContextMenuItem> | Get all registered items |
close | () => void | Programmatically close menu |
isOpen | boolean (getter) | Whether the menu is currently open |
ContextMenuItem
Section titled “ContextMenuItem”interface ContextMenuItem { readonly id: string; readonly label: string; readonly icon?: string; readonly shortcut?: string; readonly separator?: boolean; readonly contexts: ReadonlyArray<'cell' | 'header' | 'row-number' | 'corner'>; readonly submenu?: ReadonlyArray<ContextMenuItem>; action?: (ctx: MenuActionContext) => void; isDisabled?: (ctx: MenuActionContext) => boolean; isVisible?: (ctx: MenuActionContext) => boolean;}MenuActionContext
Section titled “MenuActionContext”The callback context passed to action, isDisabled, and isVisible:
interface MenuActionContext { readonly row: number; readonly col: number; readonly region: string; readonly engine: SpreadsheetEngine;}Submenus
Section titled “Submenus”Items with a submenu property render a nested menu panel. Submenus are recursive — each child can have its own submenu:
cm?.registerItem({ id: 'format-menu', label: 'Format', contexts: ['cell'], submenu: [ { id: 'format-bold', label: 'Bold', shortcut: 'Ctrl+B', contexts: ['cell'], action: (ctx) => { /* apply bold */ }, }, { id: 'format-align', label: 'Alignment', contexts: ['cell'], submenu: [ { id: 'align-left', label: 'Left', contexts: ['cell'], action: () => {} }, { id: 'align-center', label: 'Center', contexts: ['cell'], action: () => {} }, { id: 'align-right', label: 'Right', contexts: ['cell'], action: () => {} }, ], }, ],});Submenu items display a ▸ chevron; leaf items display their keyboard shortcut.
Submenu Positioning
Section titled “Submenu Positioning”Submenus open to the right of the parent item. If there is not enough space on the right, they flip to the left. Vertical position is clamped to the container bounds.
Mouse Interaction
Section titled “Mouse Interaction”- Hover: Opens the submenu after a 200ms delay. Moving to a sibling item closes deeper submenus immediately.
- Leave: Closes the submenu after 150ms. Cancelled if the mouse enters the submenu panel.
- Click: On a submenu parent, opens the submenu immediately. On a leaf item, executes the action and closes the entire menu.
Keyboard Navigation
Section titled “Keyboard Navigation”| Key | Behavior |
|---|---|
| Arrow Right | Open submenu of focused item, move focus to first child |
| Arrow Left | Close current submenu, return focus to parent |
| Arrow Down/Up | Move focus within current level (wraps around, skips disabled items) |
| Escape | Close current submenu; if at root level, close entire menu |
| Enter | Execute focused item action (or open submenu) |
Visibility
Section titled “Visibility”Empty submenus (all children hidden via isVisible returning false) are automatically hidden from the menu.
Plugin Integration
Section titled “Plugin Integration”When using the context menu as a plugin, use createContextMenuPlugin and its helper functions:
import { createContextMenuPlugin } from '@witqq/spreadsheet-plugins';import { createDefaultMenuItems } from '@witqq/spreadsheet';
const plugin = createContextMenuPlugin();engine.installPlugin(plugin);
// Get the default items (copy, paste, insert row, delete row, etc.)const defaultItems = createDefaultMenuItems();registerMenuItem / unregisterMenuItem
Section titled “registerMenuItem / unregisterMenuItem”Register or unregister items via the plugin API:
import { registerMenuItem, unregisterMenuItem } from '@witqq/spreadsheet-plugins';
registerMenuItem(pluginApi, { id: 'custom-action', label: 'Custom Action', contexts: ['cell'], action: (ctx) => console.log('Custom action on', ctx.row, ctx.col),});
unregisterMenuItem(pluginApi, 'custom-action');