Skip to content

Context menu ​

contextMenu: true shows a menu with clipboard, undo / redo and export commands when a cell is right clicked. The menu can be extended with your own items or replaced completely.

vue
<template>
  <Datagrid :columns="columns" :rows="rows" :options="options" @update:rows="rows = $event" />
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const rows = ref(createEmployees(200));

const columns: Array<ColumnConfig<Employee>> = [
  { field: "id", text: "ID", type: "number", width: 70, editable: false },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number" },
];

const options: GridOptions<Employee> = {
  rowId: (data) => data.id,
  selection: "Cell",
  singleSelect: false,
  contextMenu: true,
  defaultColumn: { editable: true, flex: 1 },
};
</script>
tsx
import { useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const columns: Array<ColumnConfig<Employee>> = [
  { field: "id", text: "ID", type: "number", width: 70, editable: false },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number" },
];

const options: GridOptions<Employee> = {
  rowId: (data) => data.id,
  selection: "Cell",
  singleSelect: false,
  contextMenu: true,
  defaultColumn: { editable: true, flex: 1 },
};

export default function ContextMenuDefault() {
  const [rows, setRows] = useState(() => createEmployees(200));
  return <Datagrid columns={columns} rows={rows} options={options} onRowsChange={setRows} />;
}

Behaviour ​

  • A right click on a cell that is not part of the selection selects the cell and focuses it, like in a spreadsheet. A right click inside the selected range keeps the selection, so the commands work on the whole range.
  • The menu is closed with Escape, a click outside, scrolling or after an item was clicked.
  • There is no grid menu for pinned rows, the total row and the header (for the header see the column menu).
  • Items which can't be used are disabled: cut, paste and clear if no selected cell is editable, undo / redo if there is nothing to undo / redo, paste if the browser does not allow reading the clipboard.

Default items ​

With contextMenu: true the menu contains depending on the options:

ItemsShown when
copy, copyWithHeaders, cut, pasteclipboard: true (default)
undo, redoundoRedo: true (default)
expandAll, collapseAllrows are grouped or tree data is shown
exportCsvalways

api.getDefaultContextMenuItems() returns this list for the current options.

Built-in items ​

IdAction
copycopy the selected cells (Ctrl + C)
copyWithHeaderscopy the selected cells including the column headers
cutcopy and clear the selected cells (Ctrl + X)
pastepaste the clipboard at the focused cell (Ctrl + V)
clearclear the selected cells (Delete)
undo, redoundo / redo the last change (Ctrl + Z / Y)
exportCsvdownload the rows as CSV (export)
autosizeColumnsfit all columns to their content
resetColumnsrestore the initial column widths, order, pinning and visibility
expandAll, collapseAllexpand / collapse all groups or tree rows
separatora line between items

Separators at the beginning, at the end and directly after another separator are removed, so it is safe to combine lists. The labels are localized (keys menu_copy, menu_cut, …).

Custom items ​

contextMenu can be

  • an array of built-in ids and custom items – the menu is the same for all cells
  • a function (params, defaultItems) => items – called every time the menu opens, e.g. to add items to the defaults or to show items depending on the clicked cell

A custom item is an object:

PropertyDescription
labeltext of the item
action(params)called when the item is clicked
iconoptional: an emoji / text, or css classes of a grid icon, e.g. "ut-icon filter-icon"
shortcutoptional: hint shown on the right side (only displayed, it doesn't register a key)
disabledoptional: shows the item disabled

params is { cell, api }: cell is the right clicked cell { item, column } (the row is cell.item, its data object cell.item.data), api the grid api.

ts
contextMenu: [
  "copy",
  "paste",
  "separator",
  { label: "Open", icon: "📂", action: ({ cell }) => open(cell!.item.data) },
],
vue
<template>
  <div class="demo">
    <Datagrid :columns="columns" :rows="rows" :options="options" @update:rows="rows = $event" />
    <div class="demo-log">{{ log }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, getText, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

type Person = Employee & { highlighted?: boolean };

const rows = ref<Array<Person>>(createEmployees(200));
const log = ref("Right click a cell");

function toggleHighlight(person: Person) {
  rows.value = rows.value.map((row) => (row.id === person.id ? { ...row, highlighted: !row.highlighted } : row));
}

function deleteRow(person: Person) {
  rows.value = rows.value.filter((row) => row.id !== person.id);
  log.value = `Deleted ${person.firstName} ${person.lastName}`;
}

const columns: Array<ColumnConfig<Person>> = [
  { field: "id", text: "ID", type: "number", width: 70, editable: false },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number" },
];

const options: GridOptions<Person> = {
  rowId: (data) => data.id,
  rowClass: (row) => (row.data?.highlighted ? "row-highlighted" : undefined),
  selection: "Cell",
  singleSelect: false,
  defaultColumn: { editable: true, flex: 1 },
  // called whenever the menu is opened
  contextMenu: ({ cell }, defaultItems) => {
    const person = cell?.item.data;
    return [
      {
        label: person?.highlighted ? "Remove highlight" : "Highlight row",
        icon: "★",
        disabled: !person,
        action: () => toggleHighlight(person!),
      },
      {
        label: "Delete row",
        icon: "ut-icon remove-icon",
        disabled: !person,
        action: () => deleteRow(person!),
      },
      {
        label: "Show cell value",
        disabled: !cell,
        action: ({ cell }) => (log.value = `${cell!.column.text}: ${getText(cell!.column, cell!.item)}`),
      },
      "separator",
      ...defaultItems, // copy, cut, paste, undo, redo, export
      "separator",
      "clear",
      "autosizeColumns",
    ];
  },
};
</script>

<style>
.row-highlighted .ut-cell {
  background-color: rgba(250, 204, 21, 0.25);
}
</style>
tsx
import { useMemo, useState } from "react";
import { Datagrid, getText, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

type Person = Employee & { highlighted?: boolean };

const columns: Array<ColumnConfig<Person>> = [
  { field: "id", text: "ID", type: "number", width: 70, editable: false },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number" },
];

export default function ContextMenuCustom() {
  const [rows, setRows] = useState<Array<Person>>(() => createEmployees(200));
  const [log, setLog] = useState("Right click a cell");

  const options = useMemo((): GridOptions<Person> => {
    function toggleHighlight(person: Person) {
      setRows((rows) => rows.map((row) => (row.id === person.id ? { ...row, highlighted: !row.highlighted } : row)));
    }

    function deleteRow(person: Person) {
      setRows((rows) => rows.filter((row) => row.id !== person.id));
      setLog(`Deleted ${person.firstName} ${person.lastName}`);
    }

    return {
      rowId: (data) => data.id,
      rowClass: (row) => (row.data?.highlighted ? "row-highlighted" : undefined),
      selection: "Cell",
      singleSelect: false,
      defaultColumn: { editable: true, flex: 1 },
      // called whenever the menu is opened
      contextMenu: ({ cell }, defaultItems) => {
        const person = cell?.item.data;
        return [
          {
            label: person?.highlighted ? "Remove highlight" : "Highlight row",
            icon: "★",
            disabled: !person,
            action: () => toggleHighlight(person!),
          },
          {
            label: "Delete row",
            icon: "ut-icon remove-icon",
            disabled: !person,
            action: () => deleteRow(person!),
          },
          {
            label: "Show cell value",
            disabled: !cell,
            action: ({ cell }) => setLog(`${cell!.column.text}: ${getText(cell!.column, cell!.item)}`),
          },
          "separator",
          ...defaultItems, // copy, cut, paste, undo, redo, export
          "separator",
          "clear",
          "autosizeColumns",
        ];
      },
    };
  }, []);

  return (
    <div className="demo">
      <Datagrid columns={columns} rows={rows} options={options} onRowsChange={setRows} />
      <div className="demo-log">{log}</div>
      <style>{`
        .row-highlighted .ut-cell {
          background-color: rgba(250, 204, 21, 0.25);
        }
      `}</style>
    </div>
  );
}

Your own menu ​

The cell-contextmenu event is emitted before the grid menu opens. Call event.preventDefault() in the handler to show your own menu component instead (the grid menu and the browser menu are not shown). See the example in Events & state.

vue
<Datagrid @cell-contextmenu="({ event, row, column }) => { event.preventDefault(); openMyMenu(event, row); }" ... />
tsx
<Datagrid
  onCellContextMenu={({ event, row }) => {
    event.preventDefault();
    openMyMenu(event, row);
  }}
/>

The event is emitted with and without the contextMenu option. Without the option, the right click neither selects the cell nor prevents the browser menu.

API ​

ts
api.getDefaultContextMenuItems();   // ids of the default menu for the current options
api.getContextMenuItems(cell);      // resolved items for a cell: { id, label, icon, shortcut, disabled, action } or { separator: true }

getContextMenuItems can be used to render the grid menu items in your own menu component.

Styling ​

ClassElement
.ut-context-menuthe popup
.ut-menu__iteman item (a button, :disabled when disabled)
.ut-menu__icon, .ut-menu__label, .ut-menu__shortcutparts of an item
.ut-menu__separatorseparator

Released under the ISC License.