Copy-paste that behaves like Excel — and where it doesn't

Excel-like editing can mean anything from a rectangle you can drag to a fill handle that continues a series. Here is what our grid actually does with a drag, a fill handle and a clipboard — and the specific places the comparison to Excel breaks down.

KanuniLabs6 min read

"Excel-like editing" doesn't say which parts of Excel it means. It can mean you can drag to select a rectangle. It can also mean Ctrl+C puts something usable on the clipboard, or that the fill handle extends a numeric series the way years of spreadsheet habit expect. Those are very different amounts of behaviour, so this is what ours actually does, gesture by gesture, plus the specific places where the Excel comparison stops being accurate.

One switch, three behaviours

There's no separate rangeSelection, fillHandle or clipboard prop. The rectangle, the fill handle and the clipboard gestures all ride on editing.range:

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 turning on cell editing already gives you the rectangle — you don't opt into it separately, and turning editing off turns it off too. See it running in the playground.

The gestures

GestureResult
Drag from a cellDraws the rectangle
Shift-clickExtends it to the clicked cell
Drag the small square on the anchor cellFill handle — extends the values
Ctrl+CCopies the rectangle as TSV
Ctrl+XCopies, then clears the cells the user is allowed to write
Ctrl+VPastes from the rectangle's top-left cell (or the focused cell when there's no rectangle), repeating the clipboard if the rectangle is bigger
Arrow / Home / End / PageUp / PageDown / TabCollapses the rectangle

For the arrow keys, Home and PageUp/PageDown that last row is Excel's rule too: press one with a range selected and Excel drops back to a single cell and moves it. (Tab differs — in Excel it walks the active cell through the selection without collapsing it.) We adopted the rule for a concrete reason. Clicking a cell starts a rectangle, and the rectangle's anchor is outlined the same way the focused cell is. The arrow keys used to move the focus and leave the anchor where it was, so two cells sat there outlined at once and neither looked more current than the other. Collapsing on navigation leaves one marker on screen.

Who's actually allowed to write

allowUpdating is one predicate over row and column, and the cell editor, cut, paste and the fill handle all ask it:

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

A cell it refuses, or a column with editable: false, is still copied — a locked column isn't invisible to Ctrl+C. It just isn't cleared, pasted over or filled. The rest of the gesture goes ahead, so one read-only column inside a paste doesn't cancel the paste.

What you can observe

Gestures publish a bracket — a Start event and an End event. Once a Start has fired, its End always follows, including when the gesture wrote nothing and when it threw partway through, so a listener that pauses its own work between the two (to batch the cellValueChanged events a paste produces, say) is never left paused:

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) => {
    // e.source is 'edit' | 'cut' | 'fill' | 'paste' | 'undo' | 'redo'
    if (e.source === 'paste') audit(e.rowKey, e.columnId, e.value);
  });
}}

The same pattern covers cutStart/cutEnd, copyStart/copyEnd and pasteStart/pasteEnd. Worth knowing if you're building undo/redo on top of this: pasting 200 cells is one undo step, not 200.

Turn on statusBar and the rectangle reports its own statistics — cell count, sum, average, min, max — without you wiring anything:

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

Non-numeric cells inside the rectangle are skipped in those calculations, not counted as zero, which matters the moment a text column sits next to a numeric one in the same drag.

Where "like Excel" stops being literal

This is the part worth reading before you integrate, not after a user reports a "bug" that's actually a documented boundary.

  • No keyboard path to the rectangle at all. Shift+Up/Down extends the row selection (in multiple-selection mode), not the cell rectangle, and Ctrl+A selects rows. The rectangle is mouse-only, and the fill handle is aria-hidden — there's no keyboard or screen-reader path to it at all, unlike Excel's own keyboard fill shortcuts.
  • One rectangle at a time. No Ctrl-click to build a disjoint, multi-region selection the way Excel allows.
  • Paste needs the async clipboard APInavigator.clipboard.readText(), with no fallback to the older clipboardData event. That means a secure context, plus whatever permission prompt or confirmation the browser shows before a page may read the clipboard; where the browser refuses, paste silently does nothing rather than erroring.
  • Paste crops, it never grows the grid. Copy 100 rows and paste onto the second-to-last row, and only the rows that physically fit get written — there's no "add more rows to fit the paste" behaviour.
  • Type coercion covers numbers and booleans, nothing else. A pasted date is written as plain text, and so is a number the parser can't read — no silent reformatting into a date type.
  • Fill only extrapolates an evenly spaced numeric series. Two or more numbers with a constant step (1, 2 or 10, 20, 30) continue as an arithmetic sequence. Unevenly spaced numbers repeat as a pattern, where Excel fits a linear trend, and so do text, dates, booleans and blanks — no month-name sequences, no Item 1 incrementing to Item 2, no date series. Fill writes values, never cell formats.
  • What gets copied is what the user sees, meaning a lookup column's label and any valueFormatter output — not the raw stored value. That's one-way: pasting the label Antalya back in doesn't resolve it to whatever code is actually stored underneath.
  • Sorting or filtering doesn't clear the rectangle. The coordinates are view indices, so after a sort the rectangle sits in the same screen position over different rows underneath it — worth clearing yourself if that would be confusing in your app.
  • No dedicated event for the rectangle's shape. selectionChanged is row selection; there's no rangeSelectionChanged. You can observe the gestures happening, not query the current rectangle's bounds directly from an event.
  • Statistics stop at 100,000 cells and report truncated instead of a number past that point, rather than silently taking longer to compute.
  • Community doesn't have any of this. No rectangle, no fill handle, no cut, no paste-as-TSV. Community's Ctrl+C copies whole selected rows, which is a different feature with the same keyboard shortcut.

None of these are hidden behind a licence check, either — without a licence key the features still run, watermarked, so you can evaluate the actual behaviour above before you decide whether it fits, not just read about it.

What to actually test before you commit to a grid

"Excel-like copy-paste" on a feature list doesn't tell you whether paste and fill respect a read-only cell or write straight through it, whether a locked column is copy-only or invisible, whether paste can grow the grid, or whether fill recognises a date series. Each of those takes about a minute to check by hand once you have a running example — ours is in the playground.


Range selection, fill and clipboard sit on the same editing layer covered in the events reference — the same cellValueChanged event fires whether the edit came from a keystroke, a fill or a paste, and its source tells them apart. Full API and prop shapes are on the range selection page.

Tagsguideclipboardediting