@aiquants/spreadsheet 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +14 -5
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +128 -25
- package/dist/index.d.ts +128 -25
- package/dist/index.js +3774 -3710
- package/package.json +1 -1
- package/src/Spreadsheet.tsx +164 -33
- package/src/editor/useCellEditor.ts +6 -1
- package/src/grid/CellView.tsx +22 -5
- package/src/grid/GridBody.tsx +24 -9
- package/src/index.ts +1 -1
- package/src/keyboard/useGridKeyboard.ts +9 -4
- package/src/model/dataAdapter.ts +98 -7
- package/src/popover/usePopovers.ts +22 -16
- package/src/selection/selectionState.ts +28 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiquants/spreadsheet` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.12.0 (2026-09-22)
|
|
6
|
+
|
|
7
|
+
v0.12 はセルの見た目をホストへ渡す 2 つの継ぎ目 — `getCellAttributes` (ホスト `data-*` 属性の素通し) と
|
|
8
|
+
`renderCell` のセル単位オプトアウト — の列車 (ADR-32)。消費側がセル内へ器 (ラッパー div) を敷いて地色・
|
|
9
|
+
ホバー・テーマトークンを描いていた DOM 税と、「1 列でも自前描画なら全列自前」の全か無かを解消する。
|
|
10
|
+
|
|
11
|
+
- **新機能 — `SpreadsheetDataAdapter.getCellAttributes` (12 番目の optional・非 aspect 能力)**: セル毎の
|
|
12
|
+
ホスト所有 `data-*` 属性をパッケージ所有のセル要素 (`[data-aqsp-cell]`) へそのまま載せる継ぎ目。値の
|
|
13
|
+
解釈はパッケージが持たない — 行状態の塗り分け・`:hover`・テーマトークン (`var(--…)`) は全てホスト
|
|
14
|
+
CSS 側で書ける (`[data-aqsp-cell][data-row-state="dirty"] { background: var(--…) }`)。render 時
|
|
15
|
+
fail-fast 検証 (`resolveCellAttributes` — 戻りはプレーン record か null、キーは小文字 `data-[a-z0-9-]+` のみ、
|
|
16
|
+
パッケージ / エンジン放出の `data-aqsp*` / `data-aqvs*` / `data-row` / `data-col` は予約、値は string のみ —
|
|
17
|
+
違反は名指しの RangeError)。描画セル毎・スライス毎 1 読み (getCellText と
|
|
18
|
+
同格予算)、変更は `dataVersion` で公開、セル毎 `null` = 追加属性なし。未宣言 = v0.11 のセル DOM と
|
|
19
|
+
バイト同一。
|
|
20
|
+
- **新機能 — `renderCell` セル単位オプトアウト + `RenderCellContext.displayText`**: `undefined` を返した
|
|
21
|
+
セルはパッケージ既定描画 (値書式エンジン込み) へ降りる — 「要る列だけカスタム、残りは既定」が器なしで
|
|
22
|
+
構成できる (v0.7 の「renderCell は値書式を全滅させる」グリッド単位ゲートは退役。オプトアウトは
|
|
23
|
+
`undefined` だけ — `null` / `false` / 他の ReactNode はカスタム出力 = 空のカスタムセル)。ctx は素の `text` (v0.7 恒等) に加えて `displayText` (値書式の表示文字列 — 既定描画・
|
|
24
|
+
AT 読み上げ・HTML クリップボード・autofit 計測と同一) を運び、「画面には効かないのに読み上げとコピー
|
|
25
|
+
には効く」無言のずれを構造的に断つ。autofit は `renderCell` 指定中はグリッド全体 no-op のまま
|
|
26
|
+
(カスタム DOM の幅は測れない)。
|
|
27
|
+
- **修正 — 行数 / 列数が縮むと保持中の選択で `Index out of bounds` (ADR-33)**: 同一 adapter のまま `rowCount` / `colCount`
|
|
28
|
+
が選択・編集セッション・ポップオーバー・クリップの位置より小さくなる (絞り込み / カテゴリ切替 / 期間変更) と、
|
|
29
|
+
オーバーレイ幾何が消えた行を Fenwick 木へ測りに行って送出し React のエラー境界へ落ちていた (0.10.0 / 0.11.0 で
|
|
30
|
+
再現 — 消費側 issue)。パッケージが座標を新グリッドへ畳む: 選択は `clampSelectionToGrid` (各レンジの隅・anchor・
|
|
31
|
+
active を末尾行 / 列へ、丸ごと外のレンジは端へ潰す。範囲外なしは identity = 通知なし、畳んだら `onSelectionChange`
|
|
32
|
+
1 回)、範囲外セルの編集セッションは値破棄 (`getSessionCell` 新設)、アンカーが範囲外のポップオーバー / tip は閉鎖、
|
|
33
|
+
自シートの範囲外クリップは破棄、飛行中ドラッグは取消、空グリッドへの縮小は通知なしの A1 直接リセット。範囲内に
|
|
34
|
+
残るものは保持し (絞り込みで入力中のセルを失わせない)、成長は何もしない。適用点はシート切替 effect 直後の
|
|
35
|
+
`useLayoutEffect` (畳込みを取消 / 閉鎖 / 破棄より先に置く — 畳んだ選択は新旧どちらの軸でも範囲内) — passive effect
|
|
36
|
+
では Fenwick の範囲外 throw が再現する (変異 CS-4 実測)。測る側の fail-fast は緩めない (黙って 0 を返すと選択枠が
|
|
37
|
+
誤位置に描かれる)。特性: 畳込みは `onSelectionChange` を layout effect から 1 回発火 (ホスト起点)、ポップオーバー
|
|
38
|
+
閉鎖はフォーカスがグリッド外 (絞り込み入力欄) にある間は打鍵先を奪わない (`close({ restoreFocus: false })`)、保持
|
|
39
|
+
された選択 / セッションは index 意味論、生存セッションの数式トークンが消えた行を指してもキーボード参照
|
|
40
|
+
ポインティングは消費点で `clampCoord` に畳んでから歩く (`useGridKeyboard`)。新規 [countShrink.spec.tsx](<./src/countShrink.spec.tsx>) 13
|
|
41
|
+
(issue の再現手順そのまま / レンジ切詰め / 通知 1 回・非影響 0 回 / 列軸 / 空グリッド往復 (通知ゼロ) / 編集セッションの
|
|
42
|
+
取消と保持 / クリップ破棄 (dataVersion 据え置き経路) と範囲内保持と foreign 不触 / ポップオーバー閉鎖と範囲内保持と
|
|
43
|
+
フォーカス中立 / 数式ポインティングの消費点 clamp) + `selectionState` +3。変異 CS-1 〜 CS-5 (分冊 07
|
|
44
|
+
§5 台帳) red 実測 — クリップ破棄は dataVersion 増分経路では既存契約が先に破棄するため、ピンを据え置き経路へ是正。
|
|
45
|
+
- **修正 (v0.11 の検証所見)**: `validateOverscan` の挿入で切り離されていた `clampZoom` の JSDoc を復旧。overscan の
|
|
46
|
+
説明を是正 — エンジンは overscan を検証しない (負値は描画窓を無言で縮める)、overscan は行数で数え非表示行も枠を
|
|
47
|
+
消費する、予算超過はエンジンが有界化でなく**打切り** (先頭側を残す — 行予算超過は無言、セル上限超過は列脱落) で
|
|
48
|
+
応じる — を明記し、Infinity も拒否 (メッセージは生入力を映す — 数値はそのまま、文字列は引用符つき)。`background` の identity 安定契約 (インライン要素は
|
|
49
|
+
GridBody memo を貫通) と凍結列域まで広がる範囲を明記。**新設 `onRenderTruncated`** — VirtualGrid の打切り通知の
|
|
50
|
+
素通し (列脱落を可観測に — ピンはテストシーム `__maxRenderedCells` で 3 万セルを実体化せずに固定)。background 省略
|
|
51
|
+
テストを空砲から直下要素数の対照ピンへ置換。
|
|
52
|
+
- **検証 — 走行台帳 (census の錨は本未リリース節)**: 単体 / 統合 **82 ファイル 3,851 テスト** 全緑 —
|
|
53
|
+
0.11.0 出荷時の 79 ファイル 3,815 に v0.12 進行分 +36 (新規 `renderCellOptOut` 5 + `cellAttributes` 5 +
|
|
54
|
+
`countShrink` 13、既存増分 `dataAdapter` +7 / `CellView` +1 / `selectionState` +3 / `overscanPassthrough` +1、
|
|
55
|
+
0.11.0 節の出荷時凍結ピンで `hxCensus` +1)。変異 4 本 red
|
|
56
|
+
実測 (キー文法ガード除去 / GridBody のホスト属性 merge 脱落 / CellView オプトアウト着地除去 / 値書式の
|
|
57
|
+
renderCell 再ゲート) — バイト復元済み。
|
|
58
|
+
|
|
5
59
|
## 0.11.0 (2026-09-22)
|
|
6
60
|
|
|
7
61
|
v0.11 はエンジン素通し 3 プロップ (`overscanRows` / `overscanCols` / `background`) — 高速スクロールで
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ deep-reach proof route.
|
|
|
13
13
|
- 📋 **Real clipboard, Excel-compatible TSV**: only cells containing tabs/newlines are quoted (a literal `"hi"` survives untouched), dual `text/plain` + `text/html` payloads (`mso-data-placement:same-cell`), tiling paste onto integer-multiple ranges, cut-move — and **both copy and cut arm an animated marching-ants marquee** (Excel-style; a copy's ants survive paste for repeated pasting, Escape or editing dismisses them; static under `prefers-reduced-motion`)
|
|
14
14
|
- ✏️ **In-place editing**: F2 / double-click (caret lands on the clicked character in real browsers) / type-to-replace, Alt+Enter multiline, full IME composition support (Safari and Chrome event orders both handled)
|
|
15
15
|
- 🧮 **Consumer-owned data**: a 4-member adapter (`rowCount` / `colCount` / `getCellText` / `applyEdits`) is the whole data contract — bring your own store, undo stack, and validation; an optional `findDataEdge` capability unlocks Excel Ctrl+Arrow data-edge jumps, and an optional `getCellFormat` capability (v0.7) adds built-in, totally fail-soft display formats —
|
|
16
|
-
value-aware auto-alignment (values that actually parse go right, labels stay left) plus number/currency/percent/scientific/date value formats over the display string (exact-decimal Intl formatting, interned formatters, timezone-proof pure-calendar dates), editing and copy still carrying the raw value —
|
|
16
|
+
value-aware auto-alignment (values that actually parse go right, labels stay left) plus number/currency/percent/scientific/date value formats over the display string (exact-decimal Intl formatting, interned formatters, timezone-proof pure-calendar dates), editing and copy still carrying the raw value — for every cell that is not custom-rendered (since v0.12 a `renderCell` hands individual cells back to this default by returning `undefined`)
|
|
17
17
|
|
|
18
18
|
- 🕳️ **Hidden tracks are first-class**: zero-size rows/columns render nothing, arrows and Shift-arrows jump a hidden run in one keystroke, Home/End land on visible tracks, and rendering stays fast across 100,000-column hidden runs
|
|
19
19
|
- 🛡️ **Never silently freezes**: every bulk operation (copy / cut / delete / paste / fill / resize fan-out / autofit) is size-estimated **before** any expansion and rejected past its `maxOperationCells`-derived ceiling with a typed callback
|
|
@@ -26,6 +26,9 @@ deep-reach proof route.
|
|
|
26
26
|
and every write whose target set touches a read-only cell (Delete / paste / cut both walls / fill with diagonal L-legs decomposed / format / self-calc / annotations) refuses the **whole operation** before the adapter is reached (Excel-style, no per-cell skip); painted cells carry
|
|
27
27
|
`aria-readonly="true"` + `data-aqsp-readonly` (style it yourself — no package CSS), refusals speak through the single sealed `onReadOnlyRejected` (+ opt-in `formatReadOnlyAnnouncement`), and copying or viewing notes / comments / links stays legal (content-aware chords,
|
|
28
28
|
viewer-mode popovers with the commit affordance disabled); undeclared ⇒ byte-identical v0.9 behavior
|
|
29
|
+
- 🎛️ **Host-owned cell appearance without wrappers** (v0.12 — ADR-32): one optional adapter read `getCellAttributes(row, col)` stamps host `data-*` attributes verbatim onto the package-owned cell element, so row-state tints, `:hover` and theme tokens (`var(--…)`) are plain host CSS
|
|
30
|
+
(`[data-aqsp-cell][data-row-state="dirty"] { background: var(--…) }`) — no per-cell wrapper divs; and `renderCell` gains a per-cell opt-out: return `undefined` and that one cell falls back to the package default rendering, value-format engine included (`ctx.displayText` carries the exact
|
|
31
|
+
display string the default rendering, announcements, the HTML clipboard and autofit share)
|
|
29
32
|
- 🎨 **Rich cells as capability declarations** (v0.8 — ADR-13): eight optional adapter reads (`getCellFormat` / `getCellFormula` / `getCellNote` / `getCellComments` / `getCellHyperlink` / `getCellValidation` / `getConditionalFormats` / `getRangeExtrema` + `evaluateFormula`) unlock the full cell
|
|
30
33
|
format model (value format, alignment, vertical alignment, wrap, background / font colour, bold / italic / underline / strikethrough, font family / size in pt, per-side borders with Excel-style ownership folding), note / comment / link indicators, paint-time validation marks and sheet-level
|
|
31
34
|
conditional formats (cell value / text / empty / colour scale / custom formula, 2-D bucket index, first-match-per-property precedence) — every undeclared aspect keeps the v0.7 DOM byte-identical, and writes go through a patch-typed `applyEdits(edits, context)`
|
|
@@ -106,7 +109,7 @@ The host element must have a resolvable height — the grid fills its parent.
|
|
|
106
109
|
| `getCellText(row, col)` | Pure, synchronous read. Called for visible cells during render, for **every cell of a copied/cut range** (bounded by `maxOperationCells`), for the source cells of a fill and the measured cells of an autofit (both bounded — see "Operation limits"), and for the active cell when announcements are enabled — a viewport-only lazy store is not sufficient. |
|
|
107
110
|
| `applyEdits(edits, context)` | One call = **one transaction** (a paste, a Delete, a fill, a cell commit, a format command, a self-calc, an annotation write each arrive as a single call). Each `CellEdit` is a **patch** (`{ row, col } & CellPatch` — `text?` / `format?` / `note?` / `comments?` / `hyperlink?` / `validation?`): an absent key leaves that aspect untouched, `null` clears it (`text` has no `null` — clear is `""`), `format` is always the WHOLE canonical `CellFormat` the package computed (store it verbatim and return it from `getCellFormat`), `comments` is the whole thread (entries with an `id` are kept verbatim, entries without one are new — YOU mint `id` / `author` / `createdAt`). Edits apply in array order with **per-key last-wins** duplicates (a later edit's keys override, its omitted keys keep the earlier value — cut-move relies on it). `context` is the discriminated `EditContext` (see "Editing, Undo, and rejection"): `{ transaction, source }` or, for a paste, `{ transaction, source: "paste", sameSheet, conditionalFormats }`. Throw to reject — the grid keeps or discards the in-flight state safely and fires `onEditRejected(error, rejection)`. |
|
|
108
111
|
| `findDataEdge(from, direction)` | **Optional capability.** When present, Ctrl+Arrow / Ctrl+Shift+Arrow jump to the Excel data edge instead of the grid edge; absence keeps the grid-edge jump (a capability declaration, not a value fallback). You implement the Excel rule — non-empty cell with a non-empty neighbor: end of the contiguous run; otherwise: the next non-empty cell in `direction`; none: the grid edge — because only the store can jump in O(occupied cells). Returns are validated **fail-fast before dispatch**: non-integer, out-of-axis, or off-row/column coordinates throw a `RangeError` that propagates uncaught. Reference implementation: the `/edit` demo store. |
|
|
109
|
-
| `getCellFormat(row, col)` | **Optional capability** (v0.7) — the cell's built-in display format: `{ align?, value? }`. `align` (`"left"`/`"center"`/`"right"`, undefined = **auto by the actual value**: text that parses under the cell's numeric/date format — or as a strict decimal with no format — goes right; labels stay left, so a `"Total"` in a currency column is never a right-aligned impostor) lands on the cell via inline `justify-content` + a `data-aqsp-align` attribute, and the in-place editor's textarea adopts the same alignment while editing (Excel parity); `value` (`number`/`percent`/`currency` (**`currency` code required** — no region default is assumed)/`scientific`/`date`/`text`) formats the **display** string. Absence keeps the v0.1 plain, left-aligned rendering. The engine is **exact**: the validated raw string is handed to `Intl.NumberFormat` **as a string** (NumberFormat v3 mathematical values — no binary-double round-trip, so percent `scale: false` boundaries and >2^53 integers stay exact), formatters are **interned** per option tuple (construction failures too — a broken column throws once, not per cell), negative zero is suppressed via `signDisplay: "negative"` (locale-proof), and dates are **pure calendar math** — no `Date` object, so host timezones, DST gaps, and two-digit-year remapping can never corrupt or reject a real date; with `decimals` omitted, `number` keeps every typed fraction digit (Excel General parity). Formatting is **totally fail-soft** — a non-numeric string (hex/binary like `"0x10"` stays raw), an invalid/rollover/mixed-separator date (`"2026-02-30"`, `"2026-09/02"`), or an out-of-range `Intl` option all render the raw text. It applies to **display only** — editing reads the raw `getCellText` (Excel parity), and copy/TSV carry the raw value too (a **deliberate deviation** from Excel — see the deviations table). Column autofit measures the **formatted** display string, and screen-reader announcements receive both the raw `text` and the formatted `displayText`. `renderCell`
|
|
112
|
+
| `getCellFormat(row, col)` | **Optional capability** (v0.7) — the cell's built-in display format: `{ align?, value? }`. `align` (`"left"`/`"center"`/`"right"`, undefined = **auto by the actual value**: text that parses under the cell's numeric/date format — or as a strict decimal with no format — goes right; labels stay left, so a `"Total"` in a currency column is never a right-aligned impostor) lands on the cell via inline `justify-content` + a `data-aqsp-align` attribute, and the in-place editor's textarea adopts the same alignment while editing (Excel parity); `value` (`number`/`percent`/`currency` (**`currency` code required** — no region default is assumed)/`scientific`/`date`/`text`) formats the **display** string. Absence keeps the v0.1 plain, left-aligned rendering. The engine is **exact**: the validated raw string is handed to `Intl.NumberFormat` **as a string** (NumberFormat v3 mathematical values — no binary-double round-trip, so percent `scale: false` boundaries and >2^53 integers stay exact), formatters are **interned** per option tuple (construction failures too — a broken column throws once, not per cell), negative zero is suppressed via `signDisplay: "negative"` (locale-proof), and dates are **pure calendar math** — no `Date` object, so host timezones, DST gaps, and two-digit-year remapping can never corrupt or reject a real date; with `decimals` omitted, `number` keeps every typed fraction digit (Excel General parity). Formatting is **totally fail-soft** — a non-numeric string (hex/binary like `"0x10"` stays raw), an invalid/rollover/mixed-separator date (`"2026-02-30"`, `"2026-09/02"`), or an out-of-range `Intl` option all render the raw text. It applies to **display only** — editing reads the raw `getCellText` (Excel parity), and copy/TSV carry the raw value too (a **deliberate deviation** from Excel — see the deviations table). Column autofit measures the **formatted** display string, and screen-reader announcements receive both the raw `text` and the formatted `displayText`. A `renderCell` cell that returns custom output shows it instead of the built-in **value format** (both the raw `text` and the formatted `displayText` reach the renderer); a cell whose `renderCell` returns `undefined` falls back to the formatted default (v0.12 per-cell opt-out); the resolved **alignment** is a cell property that applies under `renderCell` regardless. Return values are re-read only on a re-slice, so publish a live format change by bumping `dataVersion` (or swapping the adapter). Return a shared/interned `CellFormat` per column. The exported `formatCellValue(text, value)` is the same engine, reusable inside `renderCell`. Reference: the `/format` demo. **v0.8** widens `CellFormat` to the full rich model — `verticalAlign` (`"top"` / `"middle"` / `"bottom"`), `wrap`, `background`, `color`, `bold` / `italic` / `underline` / `strikethrough`, `fontFamily`, `fontSize` (pt), `borders` (`{ top?, right?, bottom?, left? }` of `{ style: "thin" \| "medium" \| "thick" \| "dashed" \| "dotted" \| "double" \| "hair"; color? }`, default colour `BORDER_DEFAULT_COLOR` = `#000000`) — painted as inline styles + `data-aqsp-valign` / `data-aqsp-wrap` state attributes; borders follow Excel ownership (a cell draws its own right / bottom, a neighbour's left / top folds onto them, own wins) and an unparseable colour or style skips that property alone. Formats are compared and interned by their **canonical form** (`canonicalizeFormat` — sorted keys, no `undefined` / `false`), and `mergeFormat(base, patch)` returns `base` by identity when nothing changes, so a command over 10,000 cells sharing one interned base writes ONE format object. This capability also gates every format command, `handle.applyFormat` / `setBorders`, the `formats` / `noBorders` paste modes and conditional formats. |
|
|
110
113
|
| `getCellFormula(row, col)` | **Optional capability** (v0.8) — the cell's formula source, which MUST start with `=` (any other string is a `RangeError` — fail-fast like `findDataEdge`); `undefined` = no formula. Declaring it makes the editor open the formula source instead of `getCellText` (Excel parity), enables **reference pointing** (see Keymap), `fillDown` / `fillRight` / `fillSelection` and the fill handle shift relative references (`shiftFormula`), self-calc commands wrap formulas instead of rewriting values, copy writes an origin-relative R1C1 `f` per cell and paste resolves it at the target (`fromR1C1`), and the `formulas` paste mode becomes available. Without it a `"="`-prefixed text is just text — the package never sniffs `=`. |
|
|
111
114
|
| `getCellNote(row, col)` | **Optional capability** (v0.8) — the note text or `undefined`. Unlocks the `data-aqsp-note` indicator, the hover note tip, the note popover (`Shift+F2` / `insertNote`), `handle.setNote` and the `commentsAndNotes` paste aspect. |
|
|
112
115
|
| `getCellComments(row, col)` | **Optional capability** (v0.8) — the thread as `readonly CellComment[]` (`{ id, author, text, createdAt }`, all stamped by you) or `undefined`; an empty thread shows no indicator. Unlocks `data-aqsp-comment`, the comment popover (`Ctrl+Alt+M` / `insertComment`), `handle.addComment` (sends `[...existing, { text }]` — you mint the id) and the comments paste aspect (pasted comments arrive id-less, all new). |
|
|
@@ -115,10 +118,15 @@ The host element must have a resolvable height — the grid fills its parent.
|
|
|
115
118
|
| `getConditionalFormats()` | **Optional capability** (v0.8) — the sheet's `ConditionalFormatRule[]` in priority order (`{ id, ranges, condition, style, stopIfTrue? }`; conditions `cellValue` / `text` / `empty` / `notEmpty` / `colorScale` / `customFormula`; style = background / color / bold / italic / underline / strikethrough). **Requires `getCellFormat`** (declaring it alone throws a `RangeError` at mount). Keep the array identity stable and publish changes through `dataVersion`; the grid rebuilds a lazy 2-D 256 × 256 bucket index per (rules identity, render version) and, inside each bucket, a lazily materialised per-column slice, so per-cell cost is O(rules intersecting the cell's column within its 256-row band) — for whole-column rules exactly the rules on that column, whatever the sheet width (a sheet of ≤ 256 columns has a single column band, so the bucket alone would be the whole rule set). Every true rule contributes, an earlier rule wins per property, `stopIfTrue` ends the walk. Pasted rules arrive as `context.conditionalFormats` (`ConditionalFormatRuleInput` with `sourceRuleId` — you mint the new id). |
|
|
116
119
|
| `getRangeExtrema(range)` / `evaluateFormula(formula, cell)` | **Optional capabilities** (v0.8) consumed by the rules above: `getRangeExtrema` returns `{ min, max }` (or `null` = no numeric value) for a `colorScale` point without `value` — called once per rule range per render version and memoised; `evaluateFormula` receives a `customFormula` verbatim (no reference shifting — you own relative semantics) and a `custom` validation formula at commit, truth = `true` or a finite non-zero number. Undeclared ⇒ the rules that need them are skipped (nothing is substituted). |
|
|
117
120
|
| `isCellReadOnly(row, col)` | **Optional capability** (v0.10 — the 11th optional member, a non-aspect capability). `true` marks the cell read-only: no edit session can open on it (all six entries, including the touch on-screen-keyboard invitation, refuse **before** any side effect), an already-open session's commit is live re-checked, and every write producer whose target set contains one read-only cell — Delete, paste, cut (both the Ctrl+X arming and the move-time source re-check), fill (diagonal L-legs decomposed exactly), format, self-calc, annotations — refuses the **whole operation** before the adapter is reached (Excel-style, never a per-cell skip). Painted read-only cells carry `aria-readonly="true"` + `data-aqsp-readonly` (no package CSS — style the attribute yourself); refusals fire the single sealed callback `onReadOnlyRejected` (+ opt-in `formatReadOnlyAnnouncement` for the live region) and never reuse `onEditRejected` / `onOperationRejected`. Reading stays legal: copying from read-only cells, filling **from** a read-only source into editable cells, and viewing existing notes / comments / links (content-aware chords open viewer-mode popovers with the commit affordance disabled). The predicate is a **live read** — no memoization; publish changes through `dataVersion` (or an adapter swap) like every other read. It is called with in-grid coordinates only, on gesture paths and for painted cells, so budget O(1) per call without allocating; a throw propagates uncaught (fail-fast, never swallowed). Undeclared ⇒ every cell is editable and the DOM / callbacks are byte-identical to v0.9 (a capability branch, not a fallback). Reference: the `/readonly` demo. |
|
|
121
|
+
| `getCellAttributes(row, col)` | **Optional capability** (v0.12 — the 12th optional member, a non-aspect capability, ADR-32). Returns host-owned `data-*` attributes for one cell (or `null` for none); they are stamped **verbatim** onto the package-owned cell element (`[data-aqsp-cell]`), so host CSS can target cell state, `:hover` and theme tokens without wrapper elements inside the cell — the package never interprets the values. Validated fail-fast at render (RangeError): the return must be a plain record or `null`, keys are lowercase `data-[a-z0-9-]+` only, the package- and engine-emitted cell attributes (`data-aqsp*`, `data-aqvs*`, `data-row`, `data-col`) are reserved, and values must be strings. It is read once per painted cell per slice (the `getCellText` cost budget — keep it O(1)); publish changes through `dataVersion`. Undeclared ⇒ the cell DOM is byte-identical to v0.11. |
|
|
118
122
|
|
|
119
123
|
- **Adapter identity is the sheet identity.** Replacing the adapter object means "sheet switch": selection resets to A1, any in-flight drag, editing session and open popover are terminated (value-discarding, exactly like Excel switching sheets — nothing is ever committed across a swap), an armed
|
|
120
124
|
**copy** is discarded, and an armed **cut** is kept as a *foreign* clip (`onClipChange({ kind: "cut", range, foreign: true })`, its marquee hidden) so the next `Ctrl+V` on the new sheet is a **cross-sheet move** — the target sheet is written first, then the source sheet is cleared under the same
|
|
121
125
|
transaction id (see "Cut matching rule").
|
|
126
|
+
- **A count shrink keeps the identity.** When `rowCount` / `colCount` shrink under the SAME adapter (a filter, category or period change removes rows), the package clamps its own coordinates instead of resetting (ADR-33): every selection range, the anchor and the active cell are pulled onto the last row / column (a range entirely beyond the edge collapses onto it; one `onSelectionChange` when anything moved, none otherwise),
|
|
127
|
+
an editing session, a popover or a note tip whose cell left the grid is cancelled / closed (those still inside are kept), an armed clip whose range left the grid is discarded (a foreign cut is untouched), in-flight drags are cancelled, and a shrink to zero rows / columns lands on the empty-grid state. Growth changes nothing. The engine's out-of-range fail-fast stays intact — the package clamps its state before anything is measured.
|
|
128
|
+
Traits to know: the fold fires `onSelectionChange` once from a layout effect (host-driven, not a gesture); closing a popover this way takes the popover's normal close path, which returns focus to the grid; a kept selection / session is index-based — after a filter, row r shows whichever item now occupies row r (your data model decides). A kept editing session whose formula token references a vanished row keeps working — keyboard reference pointing clamps the token-derived cursor onto the grid before it steps.
|
|
129
|
+
A popover closed this way leaves your focus alone while it sits outside the grid (a filter input); only a close from inside the grid returns focus to the sink.
|
|
122
130
|
- **External writes** (your toolbar Undo, collaborative updates) are published by incrementing the `dataVersion` prop. This re-renders visible cells, rebuilds the conditional-format index, **and cancels any armed clip** — otherwise a stale cut could "move" cells whose content has changed underneath.
|
|
123
131
|
|
|
124
132
|
## Editing, Undo, and rejection
|
|
@@ -689,7 +697,7 @@ Keymap notes:
|
|
|
689
697
|
| `frozenRows` | `number` | Freezes the first N rows at the top edge (v0.6; default 0, LIVE — the row-axis twin of `frozenColumns`). Engine-validated (integer in [0, 128] — `RangeError` otherwise; values beyond `rowCount` freeze every row). Frozen rows stay visible at any vertical scroll; keyboard ensure-visible treats them as vertical no-ops. |
|
|
690
698
|
| `frozenTrailingColumns` | `number` | Freezes the **last** N columns at the right edge (v0.9; default 0, LIVE). Engine-validated pass-through, no second validation layer (integer in [0, 128] — `RangeError` otherwise); the effective count is clamped so the leading freeze wins the count space (`min(N, colCount − effectiveFrozenColumns)`). Trailing columns stay visible at any horizontal scroll — **except** when the leading + trailing extent exceeds the viewport: occluded trailing cells cannot be scrolled into view and keyboard focus draws no visible frame there (registered overlap consequence). `scrollToCell` / keyboard ensure-visible treat trailing targets as horizontal no-ops. An in-flight change of any of the four freeze props cancels every drag without restore. |
|
|
691
699
|
| `frozenTrailingRows` | `number` | Freezes the **last** N rows at the bottom edge (v0.9; default 0, LIVE — the row-axis twin of `frozenTrailingColumns`; a pinned totals row is the canonical use). Same engine validation and leading-wins clamp; trailing rows stay visible at any vertical scroll (same overlap exception); keyboard ensure-visible treats them as vertical no-ops. |
|
|
692
|
-
| `renderCell` | `(ctx: RenderCellContext) => ReactNode` — `RenderCellContext = { row; col; text }` (exported) | Custom cell rendering. **Must be identity-stable** (`useCallback`) — an inline arrow defeats the cell memo and re-renders every visible cell each pass. |
|
|
700
|
+
| `renderCell` | `(ctx: RenderCellContext) => ReactNode` — `RenderCellContext = { row; col; text; displayText }` (exported) | Custom cell rendering. **Must be identity-stable** (`useCallback`) — an inline arrow defeats the cell memo and re-renders every visible cell each pass. **Per-cell opt-out** (v0.12 — ADR-32): return `undefined` and that one cell falls back to the package default rendering, value-format engine included (only `undefined` opts out — `null`, `false` and any other ReactNode are custom output, i.e. an empty custom cell). `ctx.text` is the raw canonical string; `ctx.displayText` is the value-format display — the exact string the default rendering, announcements, the HTML clipboard and autofit share, so a custom cell that renders it can never drift from what gets copied. Autofit stays a whole-grid no-op while `renderCell` is set. |
|
|
693
701
|
| `maxOperationCells` | `number` | Bulk-operation ceiling (default 100,000). Row fan-out resize gates at 1/10 of it and autofit at 1/4 — see "Operation limits". |
|
|
694
702
|
| `enableResizeUI` | `boolean` | Default `true`. `false` removes resize strips **and unhide markers** — hidden tracks then have no in-package restore path (imperative API only). |
|
|
695
703
|
| `zoom` / `defaultZoom` / `onZoomChange` | `number` / `number` / `(zoom: number) => void` | Package zoom (v0.3): visual transform scale, logic stays in logical px. Controlled (`zoom`) or uncontrolled (`defaultZoom`, default 1). Gestures — Alt+Wheel and Ctrl+Alt+`=` / `+` / `-` / `0` (reset; the four keys match the produced character, Shift-agnostic; AltGraph-state chords are never claimed — legacy-shape defense in depth, see Keymap notes) — request via `onZoomChange`; Ctrl+Wheel is deliberately unbound (the browser owns it). Clamped to [0.25, 4]; a non-finite value (NaN / ±Infinity) throws a `RangeError` (fail-fast — no silent substitution). |
|
|
@@ -720,8 +728,9 @@ Keymap notes:
|
|
|
720
728
|
| `onReadOnlyRejected` | `(rejection: ReadOnlyRejection) => void` | v0.10 — the single read-only refusal channel: fired once per refused gesture, the adapter was never called (neither an `EditRejection` nor an operation limit). The edit variant carries the cell and the required `editSession: "none" \| "kept" \| "discarded"` fate; the operation variant carries the explicit 7-literal `op` (`"cut"` has two producers — the Ctrl+X arming refusal and the move-time source re-check) and the producer’s overlap-naive `cellCount`. Never fired when `isCellReadOnly` is undeclared. |
|
|
721
729
|
| `formatReadOnlyAnnouncement` | `(rejection: ReadOnlyRejection) => string` | v0.10 — opt-in live-region text for a read-only refusal (inherits the 200 ms announce debounce + rAF collapse, so autorepeat / IME refusal storms cannot spam the live region). Omit for silence (no built-in strings). |
|
|
722
730
|
| `tapScrollCircleOptions` | `ScrollBarTapCircleOptions` | v0.10 — position / enable-disable tuning of the touch tap-scroll circle; an engine-validated pass-through onto VirtualGrid’s `scrollBarOptions.tapScrollCircleOptions` (the engine’s `resolveTapScrollCircleOptions` is the validation SSOT — no second layer here). LIVE prop. |
|
|
723
|
-
| `overscanRows` / `overscanCols` | `number` | v0.11 — per-axis overscan pass-through onto VirtualGrid’s `overscanRows` / `overscanCols` (undefined ⇒ the engine default 3). This slack is what absorbs the engine’s one-frame rendering-window lag during fast scrolling — raise it when fast flicks show blank bands (slack per edge
|
|
724
|
-
| `background` | `ReactNode` | v0.11 — background layer rendered behind the scroll-band rows inside the engine content element (the VirtualGrid `background` pass-through). VIEWPORT-PINNED: the engine translates only its row wrapper, so this layer does not follow the scroll offset — periodic row ruling placed here slides against the real rows. Suited to a static “content still arriving” texture for the overscan-exhausted band, not to row-aligned gridlines. LIVE prop. |
|
|
731
|
+
| `overscanRows` / `overscanCols` | `number` | v0.11 — per-axis overscan pass-through onto VirtualGrid’s `overscanRows` / `overscanCols` (undefined ⇒ the engine default 3). This slack is what absorbs the engine’s one-frame rendering-window lag during fast scrolling — raise it when fast flicks show blank bands. Overscan counts rows / columns (hidden 0-size tracks consume slots too), so the px slack per edge is overscan × row height / column width only where tracks are uniform and visible. The engine does not validate overscan (a negative value silently shrinks the rendering window), so THIS surface fail-fasts: a non-integer, negative or non-finite value throws a RangeError at render. Cost multiplies across the two axes, and the engine truncates rather than bounds: a window beyond its per-axis row budget or its total cell cap keeps the leading side and drops the trailing side — the row-budget case has no callback (the engine only logs a warning), the cell-cap case reports dropped columns through `onRenderTruncated`. Keep overscan to the slack you need (hundreds per side approach the budget on tall viewports). LIVE props. |
|
|
732
|
+
| `background` | `ReactNode` | v0.11 — background layer rendered behind the scroll-band rows inside the engine content element (the VirtualGrid `background` pass-through). VIEWPORT-PINNED: the engine translates only its row wrapper, so this layer does not follow the scroll offset — periodic row ruling placed here slides against the real rows. Suited to a static “content still arriving” texture for the overscan-exhausted band, not to row-aligned gridlines. It spans the full width of the scroll rows (the frozen-column region included — frozen columns live inside the scroll rows) but not the frozen row bands. **Identity-stable** like `renderCell`: an inline element pierces the GridBody memo boundary on every host re-render — hoist it or `useMemo` it. LIVE prop. |
|
|
733
|
+
| `onRenderTruncated` | `(info: { renderedCells: number; droppedCols: number }) => void` | v0.12 — pass-through of the VirtualGrid truncation signal: fires when the engine drops trailing columns to stay under its total cell cap (`MAX_RENDERED_CELLS`); truncation is never silent in the engine, and an oversized `overscanCols` × row span is the typical cause. Undefined ⇒ no callback. |
|
|
725
734
|
| `testId` / `className` | `string` | Test hook / root class extension. |
|
|
726
735
|
|
|
727
736
|
## Imperative handle
|