Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@

## [Unreleased]

## [3.11.0] — 2026-10-26

### Added

- **Context menu** (`contextMenu`). Right-click, Shift+F10 / the Menu key on a focused cell, or Ctrl+click on Apple devices opens a menu of cell actions: Copy, Copy with headers, and Undo / Redo with `undoRedo` (disabled when there is nothing to undo or redo). Inside the current cell range, Copy writes the range (the same text as Ctrl/Cmd+C) and the range and focus stay; elsewhere it copies the clicked cell and focus moves there. `getContextMenuItems(params, defaultItems)` returns the entries (items and `'divider'`; leading, trailing and consecutive dividers are dropped); a list with no items shows no menu and leaves the browser's own menu. Headers, the header filter row, the footer, detail panels, editors, checkbox / expand / reorder cells and nested grids keep the browser menu. The menu closes on a choice, Escape or Tab (focus returns to the cell), an outside press, grid scroll, window resize and when its row leaves the rendered rows. New public types `GridContextMenuParams`, `GridContextMenuItem`, `GridContextMenuEntry`; `apiRef.openContextMenu(id, field)` and `closeContextMenu()`; `localeText` keys `contextMenuCopy`, `contextMenuCopyWithHeaders`, `contextMenuUndo`, `contextMenuRedo`, `contextMenuLabel`. Not covered: the header menu, touch long-press, Paste and Export built-ins. Guide: `docs/features/context-menu.md`; demo page **Context Menu**.

- **Dev warning for re-created `localeText` functions.** A `localeText` whose function entries (`paginationOf`, `rowSelectLabel`, …) are written inline is a new object on every render, and every rendered row reads the locale, so each parent render re-rendered all of them. The grid now warns once in development, naming the entries, after three renders in a row re-create the same function. Behaviour is unchanged: the functions are not frozen, so one that reads new state (a language switch) still updates the rows.

### Changed

- `GridApi` gained `openContextMenu` / `closeContextMenu`; this affects only code that implements `GridApi` itself. `openContextMenu` opens the menu on a later animation frame, after the cell is scrolled into view, and opens nothing if the cell is not rendered within a few frames or its row moved; tests should wait for `role="menu"`.
- `ColumnMenu` and the context menu share one keyboard handler (`useMenuKeyboard`). Internal; the column menu's behaviour is unchanged.

## [3.10.1] — 2026-10-13

### Fixed
Expand Down
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This file is auto-loaded by Claude Code and other AI coding assistants. It provi
- **Build:** `npm run build:lib` produces the published `dist/opengridx.es.js` / `dist/opengridx.umd.js`. `npm run build` type-checks the lib and builds the **demo site**.
- **Published:** yes — on npm as `@opencorestack/opengridx` with external consumers. Treat public types and behaviour changes as semver-relevant.
- **Lint:** `npm run lint` (ESLint). Must pass before every commit.
- **Current version:** 3.10.1
- **Current version:** 3.11.0

---

Expand Down Expand Up @@ -157,6 +157,14 @@ The GitHub wiki is generated from `docs/` + `wiki/` by `scripts/build-wiki.mjs`

---

## v3.11.0 — significant changes

- **Context menu** (`contextMenu`, `getContextMenuItems`; guide `docs/features/context-menu.md`). Hook step 37a `useGridContextMenu`: a native `contextmenu` listener on the grid root; the row comes from `data-rowindex`, which already indexes [top-pinned, page rows, bottom-pinned], so there is no pinned offset. `openAt` reaches keyboard navigation (Shift+F10 / Menu key) through a ref created before `useGridKeyboardNavigation`
- `useMenuKeyboard` (`lib/components/ui`) is shared by `ColumnMenu` and `GridContextMenu`; its Escape handler is document-level
- A right press (or Apple Ctrl+left) inside the cell range is `preventDefault`-ed on mousedown in the capture phase, so focus does not collapse the range. Menu state is cleared during render when its row leaves the rendered rows (no effect)
- Copy goes through `buildCellCopyText` / `getCopyText({ includeHeaders })` (header line `headerName ?? field`); synthetic rows are skipped
- Browser tests right-click with `userEvent.click(el, { button: 'right' })` (`pointer({ keys: '[MouseRight]' })` does not exist in Vitest's Playwright provider)

## v3.10.0 — significant changes

- **CI guards** (each fails the build): `check:versions` (`llms.txt` + its demo copy, CLAUDE.md "Current version" and a CHANGELOG entry must match `package.json`; bump them together), `check:compiler` (React Compiler: only the allowlisted `EXPECTED` results in `scripts/check-react-compiler.mjs`), `check:package` (no test artifacts, DataGrid-only size budget), ESLint `no-explicit-any` (now `error`, also in the demo) and `no-restricted-syntax` against `as unknown as` in `lib/` outside the boundary files listed in `eslint.config.js`
Expand Down
2 changes: 2 additions & 0 deletions demo/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const PdfExportDemo = lazy(() => import('./examples/PdfExportDemo/PdfExportDemo'
const MultiSortDemo = lazy(() => import('./examples/MultiSortDemo/MultiSortDemo'));
const HeaderWrapDemo = lazy(() => import('./examples/HeaderWrapDemo/HeaderWrapDemo'));
const HeaderFiltersDemo = lazy(() => import('./examples/HeaderFiltersDemo/HeaderFiltersDemo'));
const ContextMenuDemo = lazy(() => import('./examples/ContextMenuDemo/ContextMenuDemo'));
const RtlDemo = lazy(() => import('./examples/RtlDemo/RtlDemo'));

// Sidebar order: categories and pages most developers look for first come first. Paths never change
Expand Down Expand Up @@ -103,6 +104,7 @@ const examplesConfig = [
// Selection & spreadsheet
{ path: '/cell-selection', name: 'Cell Range Selection', component: CellSelectionDemo, category: 'Selection & spreadsheet' },
{ path: '/fill-handle', name: 'Fill Handle', component: FillHandleDemo, category: 'Selection & spreadsheet' },
{ path: '/context-menu', name: 'Context Menu', component: ContextMenuDemo, category: 'Selection & spreadsheet' },

// Layout
{ path: '/col-grouping', name: 'Column Grouping', component: ColumnGroupingExample, category: 'Layout' },
Expand Down
145 changes: 145 additions & 0 deletions demo/examples/ContextMenuDemo/ContextMenuDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { useState } from 'react';
import {
DataGrid,
exportToCsv,
useGridApiRef,
type GridColDef,
type GridContextMenuEntry,
type GridContextMenuParams,
} from '@opencorestack/opengridx';
import { DocsLayout } from '../../components/DocsLayout';
import { CodeBlock } from '../../components/CodeBlock';
import '../PasteDemos.css';

import sourceCode from './ContextMenuDemo.tsx?raw';

interface Employee {
id: number;
name: string;
department: string;
salary: number;
city: string;
}

const initialRows: Employee[] = [
{ id: 1, name: 'Ada Lovelace', department: 'Engineering', salary: 128000, city: 'London' },
{ id: 2, name: 'Grace Hopper', department: 'Engineering', salary: 134000, city: 'New York' },
{ id: 3, name: 'Alan Turing', department: 'Research', salary: 121000, city: 'Manchester' },
{ id: 4, name: 'Margaret Hamilton', department: 'Engineering', salary: 139000, city: 'Boston' },
{ id: 5, name: 'Katherine Johnson', department: 'Research', salary: 117000, city: 'Hampton' },
{ id: 6, name: 'Edsger Dijkstra', department: 'Research', salary: 125000, city: 'Austin' },
{ id: 7, name: 'Barbara Liskov', department: 'Design', salary: 119000, city: 'Cambridge' },
{ id: 8, name: 'Donald Knuth', department: 'Design', salary: 131000, city: 'Stanford' },
{ id: 9, name: 'Radia Perlman', department: 'Support', salary: 98000, city: 'Seattle' },
{ id: 10, name: 'Tim Berners-Lee', department: 'Support', salary: 102000, city: 'London' },
];

const columns: GridColDef<Employee>[] = [
{ field: 'name', headerName: 'Name', width: 180, editable: true },
{ field: 'department', headerName: 'Department', width: 140, editable: true },
{ field: 'salary', headerName: 'Salary', type: 'number', width: 120, editable: true },
{ field: 'city', headerName: 'City', width: 140, editable: true },
];

const snippet = `<DataGrid
rows={rows}
columns={columns}
cellSelection // Copy then copies the selected range
undoRedo // adds Undo and Redo to the menu
contextMenu
getContextMenuItems={(params, defaultItems) => [
...defaultItems,
'divider',
{ id: 'export', label: 'Export CSV',
onClick: () => exportToCsv(params.api.getAllFilteredRows(), columns) },
{ id: 'delete', label: 'Delete row', danger: true,
onClick: () => removeRow(params.id) },
]}
/>

// Return [] to show no menu (the browser menu then shows instead).`;

export default function ContextMenuDemo() {
const apiRef = useGridApiRef<Employee>();
const [rows, setRows] = useState(initialRows);
const [log, setLog] = useState<string[]>([]);

const note = (text: string) => setLog(prev => [text, ...prev].slice(0, 6));

const getContextMenuItems = (
params: GridContextMenuParams<Employee>,
defaultItems: readonly GridContextMenuEntry[],
): GridContextMenuEntry[] => [
...defaultItems,
'divider',
{
id: 'export',
label: 'Export CSV',
onClick: () => {
exportToCsv(params.api.getAllFilteredRows(), columns, { fileName: 'employees.csv' });
note('Exported employees.csv');
},
},
{
id: 'delete',
label: `Delete ${params.row.name}`,
danger: true,
onClick: () => {
setRows(prev => prev.filter(r => r.id !== params.id));
note(`Deleted row ${params.id} (${params.row.name})`);
},
},
];

return (
<DocsLayout
title="Context Menu"
description={
<>
With <code>contextMenu</code> on, right-clicking a body cell (or pressing <kbd>Shift + F10</kbd>)
opens a menu of cell actions: Copy, Copy with headers, and Undo / Redo when <code>undoRedo</code> is
on. <code>getContextMenuItems</code> adds your own items, here Export CSV and a danger Delete row.
</>
}
sourceCode={sourceCode}
>
<div className="paste-demo">
<section className="paste-demo__sample">
<strong>Try it</strong>
<ul>
<li>Drag over a few cells, then right-click inside the range: Copy copies the whole range and the range stays.</li>
<li>Right-click a cell outside the range: the range collapses to that cell.</li>
<li>Edit a cell, then right-click and choose Undo.</li>
<li>Focus a cell with the arrow keys and press <kbd>Shift + F10</kbd>; <kbd>Esc</kbd> closes the menu and returns to the cell.</li>
<li>Right-click a column header: the browser's own menu shows.</li>
</ul>
</section>

<DataGrid<Employee>
rows={rows}
columns={columns}
apiRef={apiRef}
cellSelection
undoRedo
contextMenu
getContextMenuItems={getContextMenuItems}
processRowUpdate={(row) => {
setRows(prev => prev.map(r => (r.id === row.id ? row : r)));
return row;
}}
height={380}
/>

<section className="paste-demo__summary">
<strong>Actions (latest first)</strong>
{log.length === 0 && <p>Choose Export CSV or Delete from the menu.</p>}
<ol className="paste-demo__history">
{log.map((entry, i) => <li key={`${i}-${entry}`}>{entry}</li>)}
</ol>
</section>

<CodeBlock code={snippet} />
</div>
</DocsLayout>
);
}
3 changes: 2 additions & 1 deletion demo/public/llms.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenGridX — AI Agent Context
# Package: @opencorestack/opengridx
# Version: 3.10.1
# Version: 3.11.0
# Docs: https://opencorestack.github.io/OpenGridX/
# This file is machine-readable. AI developers: start here.

Expand Down Expand Up @@ -736,3 +736,4 @@ Arrows move between cells (skipping hidden columns); Home/End, Ctrl+Home/End, Pa
28. **3.10**: the AI assistant's change chips are `localeText` keys too (`aiAssistantChipSort(field, direction)`, `aiAssistantChipSummary(fn, field)`, …). Select-all at 1M rows takes ~130 ms. The library forbids `as unknown as` outside documented boundaries and `any` everywhere (ESLint), and CI checks that `llms.txt`, CLAUDE.md and the CHANGELOG name the `package.json` version.
29. **Large client-side datasets**: sorting or filtering 1M rows blocks for ~0.4–0.5 s (one native sort). There is no time-sliced mode (measured and rejected: 1.5–2× slower overall). For interactive sorting and filtering at that size use a `dataSource`.
30. **3.10.1**: at 1M rows select-all ~60–90 ms, deselect-all < 50 ms, applying a grouping ~190 ms, the quick filter's first search ~1.6–2 s (later keystrokes ~210 ms). Grouping creates leaf tree nodes and row metadata only when read, still one `params.rowMeta` object per row. A Next.js 16 App Router fixture in the package smoke suite re-checks the Next.js guide on every CI run.
31. **Context menu (3.11+)**: `contextMenu` shows Copy, Copy with headers and (with `undoRedo`) Undo / Redo on right-click, Shift+F10 / the Menu key or macOS Ctrl+click over a body cell. `getContextMenuItems(params, defaultItems)` returns items and `'divider'`; an empty list shows no menu and the browser menu shows. Right-click inside a cell range copies the range and keeps it; elsewhere it copies the clicked cell. `apiRef.openContextMenu(id, field)` / `closeContextMenu()`; `localeText` keys `contextMenu*`. Types `GridContextMenuParams`, `GridContextMenuItem`, `GridContextMenuEntry`. Headers, footer, editors and detail panels keep the browser menu; no touch long-press. Guide: `docs/features/context-menu.md`.
28 changes: 28 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@ interface GridHistoryChangeParams { canUndo: boolean; canRedo: boolean; size: nu
interface GridUndoRedoOptions { limit?: number }
```

#### Context Menu (v3.11)

| Prop | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `contextMenu` | `boolean` | `false` | Shows a menu on right-click, Shift+F10 / the Menu key or macOS Ctrl+click over a body cell: Copy, Copy with headers, and Undo / Redo with `undoRedo`. Headers, the header filter row, the footer, detail panels, editors, checkbox / expand / reorder cells and nested grids keep the browser menu. See [Context Menu](features/context-menu.md). |
| `getContextMenuItems` | `(params: GridContextMenuParams<R>, defaultItems: readonly GridContextMenuEntry[]) => readonly GridContextMenuEntry[]` | — | Builds the menu for the cell it opens on from the built-in items. An empty list shows no menu (the browser menu shows). A throw falls back to the built-in items with one dev warning. Does nothing without `contextMenu` (dev warning). |

```ts
interface GridContextMenuParams<R> {
id: GridRowId; field: string; row: R; colDef: GridColDef<R>;
value: unknown; // read through valueGetter
rowMeta: GridRowMeta | undefined;
cellRange: GridCellSelectionModel | null;
api: GridApi<R>;
}
interface GridContextMenuItem {
id: string; // built-ins: 'copy', 'copyWithHeaders', 'undo', 'redo'
label: string; onClick?: () => void; disabled?: boolean;
shortcut?: string; icon?: React.ReactNode; danger?: boolean;
}
type GridContextMenuEntry = GridContextMenuItem | 'divider';
```

Public CSS: `ogx-context-menu`, `ogx-context-menu__item` (`--danger`, `--disabled`), `ogx-context-menu__shortcut`. Locale keys: `contextMenuCopy`, `contextMenuCopyWithHeaders`, `contextMenuUndo`, `contextMenuRedo`, `contextMenuLabel`.

#### Fill Handle (v3.5)

| Prop | Type | Default | Description |
Expand Down Expand Up @@ -419,6 +444,8 @@ Access these methods via the `apiRef` prop.
| `setPivotModel(model)` | `void` | Replace the pivot model. Fires `onPivotModelChange` (v3.5). |
| `getGridAiState()` | `GridAiState` | The current `filterModel`, `sortModel`, `rowGroupingModel`, `aggregationModel`, `pivotModel` and `columnVisibilityModel`, as the AI toolkit reads them. Like the other getters it sees a setter's value in the same tick (v3.5). |
| `openAiAssistant()` / `closeAiAssistant()` | `void` | Open or close the `aiAssistant` panel; `openAiAssistant` does nothing without `aiAssistant` (v3.5). |
| `openContextMenu(id, field)` | `void` | Scroll a cell into view and open the context menu on it on a later animation frame. Does nothing without `contextMenu`, and the menu does not open if the cell is not rendered within a few frames or its row moved in the meantime; tests should wait for `role="menu"` (v3.11). |
| `closeContextMenu()` | `void` | Close the context menu (v3.11). |
| `copySelectedRows()` | `Promise<void>` | Copy every selected row that passes the filter (other pages, collapsed groups and pinned rows included) as TSV, with the visible columns in screen order. Resolves without writing when no selected row is found; rejects when the clipboard write fails. |

---
Expand Down Expand Up @@ -998,6 +1025,7 @@ interface GridLocaleText {
paginationLastPageLabel; paginationLastPageTooltip;
// Cell selection stats bar
cellSelectionStatsCount; cellSelectionStatsSum; cellSelectionStatsAverage;
contextMenuCopy; contextMenuCopyWithHeaders; contextMenuUndo; contextMenuRedo; contextMenuLabel; // v3.11
}
```

Expand Down
Loading
Loading