# Context Menu Plugin

> Add right-click context menus to the grid with customizable items.

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

```ts
import '@toolbox-web/grid/features/context-menu';
```

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

#### TypeScript

```ts
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) },
      ],
    },
  },
};
```

#### React

```tsx
import '@toolbox-web/grid-react/features/context-menu';
import { DataGrid, useGrid } from '@toolbox-web/grid-react';
import type { ContextMenuConfig } from '@toolbox-web/grid/plugins/context-menu';

function MyGrid({ data, onDelete }) {
  const { ref, element } = useGrid();

  const contextMenu: ContextMenuConfig = {
    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
      ref={ref}
      rows={data}
      columns={[
        { field: 'name', header: 'Name' },
        { field: 'email', header: 'Email' },
        { field: 'status', header: 'Status' }
      ]}
      contextMenu={contextMenu}
      style={{ height: '400px' }}
    />
  );
}
```

#### Vue

```html
<script setup>
import '@toolbox-web/grid-vue/features/context-menu';
import { TbwGrid, TbwGridColumn } from '@toolbox-web/grid-vue';

const data = [
  { name: 'Alice', email: 'alice@example.com', status: 'active' },
  { name: 'Bob', email: 'bob@example.com', status: 'inactive' },
];

const contextMenuConfig = {
  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" :context-menu="contextMenuConfig">
    <TbwGridColumn field="name" header="Name" />
    <TbwGridColumn field="email" header="Email" />
    <TbwGridColumn field="status" header="Status" />
  </TbwGrid>
</template>
```

#### Angular

```typescript
// Feature import - enables the [contextMenu] input
import { GridContextMenuDirective } from '@toolbox-web/grid-angular/features/context-menu';
import { Component, output } from '@angular/core';
import { Grid } from '@toolbox-web/grid-angular';
import type { ColumnConfig } from '@toolbox-web/grid';
import type { ContextMenuItem } from '@toolbox-web/grid/plugins/context-menu';

@Component({
  selector: 'app-my-grid',
  imports: [Grid, GridContextMenuDirective],
  template: `
    <tbw-grid
      #grid
      [rows]="rows"
      [columns]="columns"
      [contextMenu]="contextMenuConfig"
      style="height: 400px; display: block;">
    </tbw-grid>
  `,
})
export class MyGridComponent {
  deleteRow = output<number>();
  rows = [...];

  columns: ColumnConfig[] = [
    { field: 'name', header: 'Name' },
    { field: 'email', header: 'Email' },
    { field: 'status', header: 'Status' }
  ];

  contextMenuConfig = {
    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[],
  };
}
```

## Demos

### Default Context Menu

```ts
// ContextMenuDefaultDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';

import type { ContextMenuParams } from '@toolbox-web/grid/plugins/context-menu';
import '@toolbox-web/grid/features/context-menu';

const container = document.getElementById('context-menu-default-demo');
if (container) {
  // Sample data for context menu demos
  const 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 items
  const 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', container);

      grid.gridConfig = {
        columns,
        features: { contextMenu: { items: defaultMenuItems } },
      };
      grid.rows = sampleData;
}
```

Right-click any cell to see the context menu.

### With Submenus

```ts
// ContextMenuWithSubmenusDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';

import '@toolbox-web/grid/features/context-menu';

const container = document.getElementById('context-menu-with-submenus-demo');
if (container) {
  // Sample data for context menu demos
  const 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', container);

      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

```ts
// ContextMenuConditionalItemsDemo.astro
import '@toolbox-web/grid';
import { queryGrid } from '@toolbox-web/grid';

import '@toolbox-web/grid/features/context-menu';

const container = document.getElementById('context-menu-conditional-items-demo');
if (container) {
  // Sample data for context menu demos
  const 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', container);

      const menuItems = [
        {
          id: 'activate',
          name: 'Activate',
          icon: '✅',
          disabled: (params) => (params.row as { status?: string })?.status === 'active',
          action: (p) => console.log('Activate', p.row),
        },
        {
          id: 'deactivate',
          name: 'Deactivate',
          icon: '⏸️',
          disabled: (params) => (params.row as { status?: string })?.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

```ts
// PluginContributedItemsDemo.astro
  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/visibility';

  const grid = queryGrid('#demo-context-menu-plugin-items');
  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: {
        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

| Option  | Type         | Default | Description           |
| ------- | ------------ | ------- | --------------------- |
| `items` | `ContextMenuItem[]` | Copy + Export CSV | Menu items to display |

## TypeScript Interfaces

- [`ContextMenuItem`](/grid/plugins/context-menu/interfaces/contextmenuitem.md) — Menu item definition (id, label, icon, action, submenus, etc.)
- [`ContextMenuParams`](/grid/plugins/context-menu/interfaces/contextmenuparams.md) — Context passed to action callbacks (row, column, value, selection, etc.)
- [`ContextMenuConfig`](/grid/plugins/context-menu/interfaces/contextmenuconfig.md) — Plugin configuration options

### 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

```ts
features: {
  contextMenu: {
    items: [
      {
        id: 'edit',
        name: 'Edit',
        hidden: (params) => params.column.editable !== true,
      },
      {
        id: 'delete',
        name: 'Delete',
        disabled: (params) => params.row.locked === true,
      },
    ],
  },
},
```

## Events

| Event               | Detail              | Description |
| ------------------- | ------------------- | ----------- |
| `context-menu-open` | `{ params, items }` | Menu opened |

## Styling

The context menu supports CSS custom properties for theming. Override these on `tbw-grid` or a parent container:

### 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

```css
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

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

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](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/):

- `aria-haspopup="menu"` is set on the grid root once the plugin is attached.
- `aria-expanded` is toggled between `"false"` and `"true"` to reflect the
  menu's open state — including when it closes via <kbd>Esc</kbd>, click
  outside, or a scroll event.
- Menu items inside the popup use `role="menuitem"` and are reachable with
  the arrow keys; <kbd>Enter</kbd> / <kbd>Space</kbd> activates and
  <kbd>Esc</kbd> closes (see [Accessibility guide](/grid/guides/accessibility.md)).

The plugin also respects the keyboard-open path: <kbd>Shift</kbd> +
<kbd>F10</kbd> and the dedicated <kbd>☰ Menu</kbd> key open the menu at the
focused cell with focus moved to the first item.

## See Also

- **[Selection](/grid/plugins/selection.md)** — Row and cell selection
- **[Editing](/grid/plugins/editing.md)** — Inline cell editing
- **[Clipboard](/grid/plugins/clipboard.md)** — Copy/paste cells
