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
| Gesture | Result |
|---|---|
| Drag from a cell | Draws the rectangle |
| Shift-click | Extends it to the clicked cell |
| Drag the small square on the anchor cell | Fill handle — extends the values |
| Ctrl+C | Copies the rectangle as TSV |
| Ctrl+X | Copies, then clears the cells the user is allowed to write |
| Ctrl+V | Pastes 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 / Tab | Collapses 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 API —
navigator.clipboard.readText(), with no fallback to the olderclipboardDataevent. 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 1incrementing toItem 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
valueFormatteroutput — not the raw stored value. That's one-way: pasting the labelAntalyaback 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.
selectionChangedis row selection; there's norangeSelectionChanged. 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
truncatedinstead 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.