Documentation

Range selection, fill handle and clipboard

Three behaviours, one switch. There is no rangeSelection, fillHandle or clipboard prop — all of it is editing.range, and it rides on the editing layer:

import { EnterpriseDataGrid } from '@kanunilabs/datagrid-react-enterprise';

<EnterpriseDataGrid
  licenseKey={key}
  dataSource={rows}
  rowKey="id"
  columns={columns}
  editing={{ mode: 'cell', range: true }}
/>

range defaults to true, so enabling editing already gives you the rectangle. Turning editing off turns range off with it: internally it resolves to editingEnabled && (editing.range ?? true).

See it working →

What the user can do

GestureResult
Drag from a cellDraws the rectangle
Shift-clickExtends it to the clicked cell
Ctrl-click or Ctrl-drag (Cmd on a Mac)Adds another rectangle; the ones already selected stay
Shift+arrowGrows it one cell from its anchor (from the focused cell when there is none)
Ctrl+Shift+arrowGrows it to the first or last row or column
Drag the small square on the anchor cellFill handle — extends the values
Ctrl+DFills down: copies the rectangle's first row into the rows below it — with one cell, copies the cell above
Ctrl+RFills right: copies the first column into the columns to its right — with one cell, copies the cell to the left
Ctrl+CCopies the rectangle as TSV
Ctrl+XCopies, then clears the cells the user may write
Ctrl+VPastes from the rectangle's top-left cell — or from the focused cell when there is no rectangle. A rectangle bigger than the clipboard gets the clipboard repeated to fill it
Arrow / Home / End / PageUp / PageDown / TabCollapses the selection — every rectangle

For the arrow keys, Home and PageUp/PageDown that last row is Excel's rule, and it surprises people who expect "select, then nudge with the arrows". (Tab differs from Excel, which walks the active cell through the selection without collapsing it.)

Several rectangles

Hold Ctrl (Cmd on a Mac, where Ctrl-click is the right-click) and click or drag to add a rectangle; a plain click starts over with one. What each gesture does with several:

GestureWith several rectangles
StatisticsCount and total every rectangle; a cell two of them share counts once
Ctrl+CRectangles over the same columns copy as one block, top to bottom; over the same rows, side by side. Rectangles that do not line up have no single block to paste back, so only the newest is copied
Ctrl+XClears every rectangle (the cells the user may write)
Ctrl+V, Shift+arrow, Ctrl+D / R, "Chart selected cells"Act on the newest rectangle
Fill handleHidden — a fill copies one block, and a handle on one of several would hide which

Whole columns from the header

Turn on columnSelection and a click on a column header selects that column — every row, top to bottom — as one rectangle, the way clicking a column letter does in a spreadsheet:

<EnterpriseDataGrid columnSelection />

The header is tinted while its column is selected, and everything above works on it: the statistics total the column, Ctrl+C copies it, Ctrl+X clears it, "Chart selected cells" charts it.

GestureDoes
Click a headerSelects that column; the other rectangles go
Shift+clickWidens the newest rectangle to every column between it and this one, all rows
Ctrl+click (Cmd on a Mac)Adds the column and keeps the rest
Ctrl+SpaceSelects the column of the focused cell, or of the focused header

The header click stops sorting while this is on — one click cannot mean both. Sort from the column menu (right-click the header), or focus the header and press Enter. The grid's own columns (selection checkbox, row numbers, commands) and the group column still sort or do nothing, as before.

A column means the rows the grid shows now. Clear a filter afterwards and the rectangle still covers the rows it had; the header stops showing as selected, because the column now holds more than that.

Which cells may change

allowUpdating is the single gate for editing, fill, paste and cut:

editing={{
  mode: 'cell',
  allowUpdating: (row, columnId) => columnId !== 'id' && !row?.locked,
}}

A cell the predicate refuses — or a column with editable: false — is still copied; it is just not cleared, pasted over or filled. The rest of the gesture goes ahead: one read-only column inside a paste does not cancel it.

Validation rules run when the change is saved, not during the gesture. Cut writes null, so a required-field rule refuses the save, the grid shows why, and the change stays pending until it is corrected or undone with Ctrl+Z.

Watching it happen

Gestures are published as a bracket — a Start and an End. Once a Start has fired, its End always follows, even when the gesture wrote nothing or threw:

onReady={(controller) => {
  controller.events.on('fillStart', (e) => console.log('filling', e.cellCount));
  controller.events.on('fillEnd', (e) => console.log('done', e.performed));

  controller.events.on('cellValueChanged', (e) => {
    // 'edit' | 'cut' | 'fill' | 'paste' | 'undo' | 'redo'
    if (e.source === 'paste') audit(e.rowKey, e.columnId, e.value);
  });
}}

The pairs are cutStart/cutEnd, copyStart/copyEnd, pasteStart/pasteEnd and fillStart/fillEnd. A whole paste is one undo step, not one per cell.

Selection statistics

Set statusBar and the rectangle reports itself — cells, sum, average, min, max:

statusBar={{ items: ['totalRows', 'selectedRows', 'rangeStats'] }}

Without a status bar the stats appear in their own strip instead, so the numbers show up exactly once either way. Non-numeric cells are skipped, not counted as zero.

Clipboard callbacks

By default a copy writes what the cells show — the lookup label and the valueFormatter text; a renderCell drawing can be anything, so it is not copied — and a paste reads the text as the column's type. The clipboard option lets your code change both — for every copy in the grid: a cell range, a cut, the selected rows, "Copy with headers".

<EnterpriseDataGrid
  clipboard={{
    // A price shown through a valueFormatter: copy the number, not "$1,234.50".
    processCellForClipboard: ({ columnId, value, text }) =>
      columnId === 'price' ? String(value) : text,
    processHeaderForClipboard: ({ text }) => text.toUpperCase(),
    // Drop empty lines and trim before anything is written.
    processDataFromClipboard: ({ data }) =>
      data.filter((row) => row.some((cell) => cell.trim() !== '')).map((row) => row.map((c) => c.trim())),
    // Read "12,5" as 12.5 in a Turkish spreadsheet's paste.
    processCellFromClipboard: ({ columnId, text, value }) =>
      columnId === 'price' ? Number(text.replace(/\./g, '').replace(',', '.')) : value,
  }}
/>
CallbackGetsReturns
processCellForClipboardrow, rowKey, columnId, value, text (shown)The text to copy
processHeaderForClipboardcolumnId, textThe header text to copy
processDataFromClipboarddata — rows of cell textsThe table to paste, or null to paste nothing
processCellFromClipboardrow, rowKey, columnId, text (pasted), value (what the grid would write)The value to write
sendToClipboardtext, kind ('copy' or 'cut')Nothing — the grid leaves the system clipboard alone

Tabs and line breaks in a copied cell become spaces, whatever the callback returns: they would split the cell in a spreadsheet. A pasted cell still goes through the permission check first (editable, allowUpdating) and validation after; the callback chooses the value, not whether the cell may change. With sendToClipboard the text stays in your app — keep it for an in-app paste, or write HTML beside it. The copyStart / copyEnd events fire either way.

What this does not do

Written down because finding out mid-integration costs more than reading it here.

  • The fill handle itself is mouse-only. It is aria-hidden; from the keyboard, Ctrl+D and Ctrl+R (Cmd on a Mac) fill the way a spreadsheet does — they COPY the first row or column, they do not extend a numeric series the way a drag does. They write through the same path as the handle: the same allowUpdating check, one undo step. With nothing to copy from (one cell in the first row or column) the key is left to the browser. The rectangle is built with Shift+arrow, and the statistics strip is a polite live region, so a screen reader hears the cell count and sum as it grows. With range selection on, Shift+Up/Down extends the rectangle, not the row selection, and Ctrl+A still selects rows.
  • Several rectangles are a selection, not a shape. Paste, fill and the chart take the newest one; copy joins them only when they line up (see Several rectangles). There is no "add mode" key (Excel's Shift+F8), so adding a rectangle needs the mouse.
  • Paste needs the async clipboard. It reads navigator.clipboard.readText() with no clipboardData fallback, so it needs a secure context and whatever permission or confirmation the browser asks for. Where the browser refuses, paste does nothing.
  • Paste crops, it never grows the grid. Copy 100 rows onto the last row and only the rows that fit are written.
  • Type coercion covers numbers and booleans. A pasted date is written as text; so is a number the parser cannot read.
  • Fill extrapolates evenly spaced numbers only. Two or more numbers with a constant step become an arithmetic sequence. Unevenly spaced numbers, text, dates, booleans and blanks repeat the pattern. No date series, no month names, no Item 1 → Item 2. Fill writes values, never formats.
  • Fill is single-axis, as in Excel. A diagonal drag resolves to whichever direction moved further.
  • Copied text is what the user sees — the lookup label, then the valueFormatter. That is one-way: pasting Antalya back does not turn it into the stored code.
  • Sorting or filtering does not clear the rectangle. Range coordinates are view indices and only columnStateChanged clears them, so after a sort the rectangle sits on the same positions over different rows. Clear it yourself if that matters.
  • No event for the rectangle. selectionChanged is row selection; there is no rangeSelectionChanged. You can observe the gestures, not the shape.
  • Statistics stop at 100,000 cells and report truncated instead.
  • Community has none of it — no rectangle, no fill, no cut, no paste. Community copies whole rows with Ctrl+C.

Without a licence key the features still run, watermarked. The boundary is distribution and the watermark, not a code path that refuses you.