Grid Clipboard
Clipboard
Cmd+C copy with TSV defaults, per-column format, grid-level onCopy override.
Cmd/Ctrl+C copies the current selection as TSV. The synthetic row-select column is filtered out of the output, so a copy from a checkbox-enabled grid produces the same text as the same selection from an unchecked grid.
Default TSV format
The default serializer emits tab-separated cells, newline-separated rows, with a blank line between blocks for multi-range selections. Default cell coercion:
null/undefined→""Date→value.toISOString()string/number/boolean/bigint→String(value)- plain object →
JSON.stringify(value)(best-effort fallback)
This matches what Excel and Sheets accept on paste, so a copy out of a Pretable grid pastes cleanly into a spreadsheet.
Per-column format
For domain-specific formatting (a Date that should copy as YYYY-MM-DD instead of full ISO, a number with grouping separators, a status enum that should copy as a label), supply format on the column:
const columns: PretableColumn<Event>[] = [
{
id: "timestamp",
header: "Time",
value: (r) => r.timestamp,
format: ({ value }) =>
value instanceof Date ? value.toISOString().slice(0, 10) : String(value),
},
// ...
];format({ value, row, column }) is called per cell before the default coercion runs. Return a string. The same format function drives display rendering (in a follow-up phase) and copy serialization, so a single definition keeps the on-screen text and the clipboard text in sync. If you don't supply one, the default coercion above applies.
Grid-level onCopy override
For full control — a custom delimiter, a JSON payload, an HTML clipboard payload alongside the TSV — pass onCopy on the surface. It receives the same SerializeRangesArgs the default serializer gets and returns a CopyPayload ({ text, html? }) or null to cancel the copy:
import { serializeRangesAsTsv } from "@pretable/react";
<PretableSurface
ariaLabel="Inspection grid"
columns={columns}
rows={rows}
getRowId={(row) => row.id}
onCopy={(args) => {
// args is SerializeRangesArgs: { ranges, visibleRows, columns, copyWithHeaders }
const tsv = serializeRangesAsTsv(args);
if (!tsv) return null; // empty selection → cancel the copy
// Reuse the built-in TSV, but write CSV instead.
return { text: tsv.text.replace(/\t/g, ",") };
}}
/>;onCopy returns a CopyPayload or null:
{ text, html? }—textis written astext/plain; whenhtmlis present, the surface also writestext/htmlvia the Clipboard API. Excel and Sheets prefertext/htmlwhen both are present.null— skip the clipboard write entirely (suppress copy in this mode).
serializeRangesAsTsv is the same helper the default path uses, so calling it inside onCopy (as above) keeps the built-in row/column/range handling — including filtering out the synthetic row-select column — and lets you post-process its output.
Building your own serializer
serializeRangesAsTsv, defaultCoerceForCopy, and the SerializeRangesArgs / CopyPayload types are exported from @pretable/react, so you can build a serializer from the same primitives the default uses.
SerializeRangesArgs<TRow> is the argument both onCopy and serializeRangesAsTsv receive:
| Field | Type | Notes |
|---|---|---|
ranges | readonly PretableCellRange[] | the selected ranges, in the order they were added |
visibleRows | readonly PretableVisibleRow<T>[] | the currently materialized rows |
columns | readonly PretableColumn<T>[] | column defs (the row-select column is among them) |
copyWithHeaders | boolean | mirror of the copyWithHeaders prop |
defaultCoerceForCopy(value) is the fallback value→string coercion (the null / Date / primitive / object rules listed under Default TSV format). Reach for it in a custom serializer so values without a per-column format still stringify consistently:
import { defaultCoerceForCopy } from "@pretable/react";
const text = column.format
? column.format({ value, row, column })
: defaultCoerceForCopy(value);copyToClipboard override
By default the surface writes the CopyPayload to the system clipboard via the async Clipboard API. Pass copyToClipboard to intercept that write — to route copies through your own clipboard shim, log them, or target a non-DOM environment:
<PretableSurface
copyToClipboard={async ({ text, html }) => {
await myClipboard.write({ text, html });
}}
/* ... */
/>It receives the CopyPayload produced by the default serializer or your onCopy, and may return a promise.
copyWithHeaders
When copyWithHeaders is true, each block in the output is prefixed with a row of column headers, separated from the body by a blank line:
<PretableSurface copyWithHeaders /* ... */ />Use this when consumers paste into a spreadsheet that needs labeled columns, or into a doc where the data wants a built-in legend. Off by default — most copies want the body only.
Multi-range serialization
A discontiguous selection (Cmd/Ctrl+click, multiple drags) serializes as one block per range, blocks separated by a blank line:
A1\tB1\nA2\tB2
D5\tE5\nD6\tE6Each block is its own TSV grid; range order matches the order the ranges were added. With copyWithHeaders, the header row is repeated at the top of each block.
aria-live announcements
The surface renders an off-screen aria-live="polite" region that announces copy and select-all events for assistive technology. Defaults are English; pass a messages?: PretableSurfaceMessages prop to override:
<PretableSurface
messages={{
selectAllAnnouncement: ({ rowCount, columnCount, isAll }) =>
isAll
? "Tutto selezionato"
: `${rowCount} righe × ${columnCount} colonne selezionate`,
copyAnnouncement: ({ rowCount, columnCount }) =>
`Copiato ${rowCount} × ${columnCount}`,
copyFailedAnnouncement: () => "Copia non riuscita",
}}
/>| Event | Default announcement |
|---|---|
| Cmd/Ctrl+A (or header-checkbox select-all) | "All rows selected" (or {n} rows × {m} columns selected for partial coverage) |
| Cmd/Ctrl+C success | {n} rows × {m} columns copied |
| Cmd/Ctrl+C failure (clipboard rejects) | "Copy failed" |
Announcements are debounced (~500ms) to avoid screen-reader thrashing on held shift+arrow extends. Programmatic mutations via state.selection do not announce — only user-triggered events do.
Paste (deferred)
Paste lands in a follow-up phase. The shape will mirror this one symmetrically:
parseFromCopy?(text, row): unknownper column — inverse offormat.onPaste?({ text, html, target })at the surface — full override, returning the rows to apply.
Until paste ships, the browser's default paste applies (which means nothing happens in the grid).
See also
- Selection — what gets serialized when you copy.
- Keyboard — the
Cmd/Ctrl+Cbinding lives here too. - API reference —
CopyPayload,SerializeRangesArgs,PretableSurfaceMessagestypes.