Repository navigation
Commit 767fd06
feat(react): CSV export on the surface, with docs and browser proof (#368)
* feat(react): CSV export on the surface, with docs
Slice 3. Three props mirroring clipboard — `csvOptions`, `onExport`, `saveFile`
— and `exportCsv(options?)` on the grid handle. This grid has no toolbar, so an
imperative method the consumer calls from their own button is the trigger.
`exportCsv` takes columns from the DRAWN order (never the `columns` prop),
passes the scope `resolveDataScope` computed rather than assuming "all", builds
`rowIds` from the selection when asked, and announces — saying so when the file
is partial.
Two things came out of review of the wiring and are worth naming:
- `onlySelected` and `rowIds` together used to merge `rowIds` last, so the
caller's explicit set vanished with nothing said. That is the silent
narrowing `serializeCsv` refuses for an unknown `columnIds`; it now throws
rather than picking a winner.
- A SYMBOLIC all-selection (header checkbox, Cmd+A) leaves `selectedRowIds`
empty, so it lands on the empty-selection branch and exports everything. That
is the right answer, but it arrived by accident rather than by a rule — now
stated in-source so a later change to either side is a decision.
Docs: `grid/export.mdx`, registered in `_nav.ts` and in the guard roster — the
options table bound to `PretableCsvOptions`, the omissions table bound to
`PretableCsvOmission` through the discriminant-union check. All four
registration mutations fail. Its `serializeCsv` fences are transcribed into a
compile-time fixture; the `<PretableSurface>` fence is registered as
untranscribed with a written reason, because the surface's generic inference
needs a row type that does NOT extend `Record<string, unknown>` while
`serializeCsv` needs exactly that, and one shared preamble cannot be both. That
API is covered by `csv-export-surface.test.tsx` instead.
1001 react tests, 369 website tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(website): prove CSV export downloads a real file in a real browser
jsdom can assert that `saveFile` got the right `PretableCsvFile`, and
`csv.test.ts` does so exhaustively. It cannot answer whether a browser accepts
the synthetic `<a download>` click as a download at all, whether it honours the
name we computed, or whether the bytes survive the Blob URL round trip. Each of
those is somewhere a green unit suite still ships a broken button.
The fixture leaves `saveFile` unset so `defaultSaveFile` itself is what runs.
Four assertions, all mutation-proven by breaking the fixture and watching the
matching test go red:
- Filename arrives exactly as built. Confirms the compact `20260813T202142Z`
stamp is the right call: Chromium rewrites `:` to `_` on every OS, so a raw
`toISOString()` would have been mangled here. (`name: "wrong"` → red.)
- Byte 0 is 0xEF. Without the BOM Excel reads UTF-8 in the OS legacy codepage
and `Größe` becomes mojibake. (`bom: false` → byte 0 is 83 → red.)
- `=1+1` reaches disk apostrophe-prefixed while `-450` reaches it intact — both
halves of the value-vouched rule in one file. (`escapeFormulas: false` → red.)
- `onlySelected` exports the two checked rows, and an empty selection exports
everything rather than a zero-row file. (Dropping `onlySelected` → red.)
8 passed across chromium and webkit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(react): close four export defects review found, and a false guard excuse
Adversarial review of #368. Every item below was proven by a test that fails
first; the four behavioural ones are in `csv-export-surface.test.tsx`.
**The conflict refusal was watching one of two doors.** `onlySelected` +
`rowIds` threw only for a call-site `rowIds`; a `rowIds` on the surface's
`csvOptions` was overwritten silently — the exact silent narrowing the throw
exists to refuse. It now reads the merged value. This also fixes the second
half: with a surface `rowIds` set, an empty selection exported that other
subset rather than everything, so the button quietly downloaded someone else's
rows.
**A synchronous throw from `saveFile` bypassed the failure path.**
`Promise.resolve(saveFile(file))` evaluates the call before wrapping it, so a
sync throw escaped `exportCsv` with nothing warned, nothing announced, and the
rest of the click handler dead. `defaultSaveFile` is entirely synchronous DOM
work, so this is the shape a real failure takes. Caught around the call rather
than moved into a `.then` — the first attempt did exactly that and the suite
caught it: deferring delivery a microtask is wrong for `<a download>` (which is
chosen BECAUSE it survives an await, a property worth spending deliberately not
accidentally) and dangerous for the identical clipboard path, where `writeText`
IS activation-gated.
**`.then(ok).catch(err)` reported a delivered file as failed.** A consumer's
`exportAnnouncement` that throws was caught by the export's own failure branch:
file on disk, screen-reader user told otherwise. Split into `.then(ok, err)` —
which leaves the success branch to handle its own throw or it escapes as an
unhandled rejection, so it warns under its own distinct message.
The clipboard path had both bugs verbatim, being where the shape was copied
from. Fixed there too.
**A documented snippet that did not compile, behind an excuse that was false.**
The `<PretableSurface>` fence was exempt from transcription on the recorded
grounds that its row type could not extend `Record<string, unknown>`.
Transcribing it showed the constraint was never the problem: the snippet was
missing its explicit type argument and referenced an undeclared global. Both
fixed, the fence transcribed, `UNTRANSCRIBED_FENCES` now empty. Two mutations
kill it — a fixture that diverges from the page, and a fixture that stops
naming the fence at all.
That is the second time a guard escape hatch has been the thing that failed
rather than the guard, and the first where the written reason was itself wrong.
Also, smaller: `columnCount`'s TSDoc claimed to be the columns actually written
and is the columns requested, which an `onExport` returning a hand-built file
can contradict — stated, along with which fields ARE exact. `exportCsv` now
documents both throws and says a save failure is not one of them. Two e2e
comments asserted things the file disproves (the escaped field is not quoted;
the buttons are `disabled` pre-hydration), and the escaping assertion is now
the whole line, since `toContain` passed under both readings and so tested
neither. `csvOptions`/`onExport`/`saveFile` are now documented in prose and
listed beside their clipboard twins in the grid API reference.
Not fixed here, filed instead: `rowIds` is typed `ReadonlySet<PretableRowId>`
rather than the grid's `TRowId`, so a `Set<number>` on a string-id grid is
accepted and yields an empty selection. Making `PretableCsvOptions` generic is
a wider change than this PR should carry.
1023 react tests, 369 website tests, 8 e2e across chromium and webkit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(changeset): record the clipboard half of the delivery fix
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>1 parent 2c30944 commit 767fd06
13 files changed
Lines changed: 1548 additions & 15 deletions
File tree
- .changeset
- apps/website
- app
- docs
- __tests__
- fixtures/csv-export
- content/docs/grid
- e2e
- lib/docs/__tests__
- packages/react
- src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
| 62 | + | |
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| |||
0 commit comments