@aiquants/spreadsheet 0.8.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 +1103 -0
- package/LICENSE.md +155 -0
- package/README.md +817 -0
- package/dist/index.cjs +21 -0
- package/dist/index.d.cts +1960 -0
- package/dist/index.d.ts +1960 -0
- package/dist/index.js +7711 -0
- package/dist/styles/spreadsheet.css +2 -0
- package/dist/styles/spreadsheet.standalone.css +3 -0
- package/package.json +94 -0
- package/src/Spreadsheet.tsx +2313 -0
- package/src/clipboard/clipPayload.ts +1242 -0
- package/src/clipboard/fixtures/excel.fixture.ts +301 -0
- package/src/clipboard/fixtures/html.fixture.ts +90 -0
- package/src/clipboard/fixtures/sheets.fixture.ts +83 -0
- package/src/clipboard/htmlInterop.ts +1694 -0
- package/src/clipboard/numberFormatCodec.ts +870 -0
- package/src/clipboard/pasteSpecial.ts +608 -0
- package/src/clipboard/tiling.ts +370 -0
- package/src/clipboard/tsv.ts +186 -0
- package/src/clipboard/useClipboard.ts +1154 -0
- package/src/commands/selfCalc.ts +279 -0
- package/src/commands/useCommands.ts +946 -0
- package/src/editor/useCellEditor.ts +708 -0
- package/src/grid/CellView.tsx +51 -0
- package/src/grid/GridBody.tsx +489 -0
- package/src/header/ColumnHeader.tsx +167 -0
- package/src/header/RowHeader.tsx +144 -0
- package/src/header/columnName.ts +31 -0
- package/src/header/resizeGates.ts +146 -0
- package/src/header/resizeStrips.ts +79 -0
- package/src/header/useHeaderResize.ts +598 -0
- package/src/header/useHeaderSelection.ts +454 -0
- package/src/index.ts +122 -0
- package/src/keyboard/compositionGuard.ts +53 -0
- package/src/keyboard/ensureCellVisible.ts +77 -0
- package/src/keyboard/shortcuts.ts +718 -0
- package/src/keyboard/useGridKeyboard.ts +940 -0
- package/src/model/axisModel.ts +186 -0
- package/src/model/cellAnnotations.ts +92 -0
- package/src/model/cellCoord.ts +114 -0
- package/src/model/cellFormat.ts +562 -0
- package/src/model/cellPaint.ts +537 -0
- package/src/model/conditionalFormat.ts +805 -0
- package/src/model/dataAdapter.ts +478 -0
- package/src/model/dataValidation.ts +542 -0
- package/src/model/formulaRefs.ts +984 -0
- package/src/model/frozenPartition.ts +100 -0
- package/src/model/labels.ts +228 -0
- package/src/model/viewStore.ts +124 -0
- package/src/overlay/SelectionOverlay.tsx +1425 -0
- package/src/overlay/useFillHandle.ts +432 -0
- package/src/popover/CommentPopover.tsx +109 -0
- package/src/popover/LinkPopover.tsx +108 -0
- package/src/popover/ListPopover.tsx +161 -0
- package/src/popover/MessagePopover.tsx +36 -0
- package/src/popover/NotePopover.tsx +106 -0
- package/src/popover/NoteTip.tsx +26 -0
- package/src/popover/PopoverLayer.tsx +210 -0
- package/src/popover/placement.ts +138 -0
- package/src/popover/usePopovers.ts +642 -0
- package/src/selection/selectionState.ts +239 -0
- package/src/selection/useAutoScroller.ts +226 -0
- package/src/selection/usePointDrag.ts +273 -0
- package/src/selection/useSelectionEngine.ts +687 -0
- package/src/styles/components.entry.css +10 -0
- package/src/styles/spreadsheet.css +1029 -0
- package/src/styles/standalone.entry.css +9 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Single cell CONTENT leaf. Since v0.4 the positioned cell element itself is
|
|
3
|
+
* OWNED by VirtualGrid (left/width from the quantized column anchor; attributes from
|
|
4
|
+
* GridBody's getCellProps) — this component renders only what goes INSIDE it, memoized on
|
|
5
|
+
* primitives so an unchanged cell skips the renderCell execution during re-slices. Selection
|
|
6
|
+
* state NEVER flows here (ADR-4: selection is painted by the viewport-space overlay).
|
|
7
|
+
*
|
|
8
|
+
* セルの中身専用の葉コンポーネント。v0.4 から配置済みセル要素そのものは VirtualGrid の所有
|
|
9
|
+
* (left / width は量子化列アンカー由来、属性は GridBody の getCellProps) — 本コンポーネントは
|
|
10
|
+
* その**内側**だけを描画し、プリミティブ memo で不変セルの renderCell 再実行を再スライス時に
|
|
11
|
+
* スキップする。選択状態は流れない (ADR-4: 選択はビューポート空間オーバーレイが描く)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { memo, type ProfilerOnRenderCallback, type ReactNode, useEffect, useRef } from "react"
|
|
15
|
+
|
|
16
|
+
/** Public custom cell renderer context (ADR-8). / 公開セルレンダラーの ctx (ADR-8)。 */
|
|
17
|
+
export type RenderCellContext = { row: number; col: number; text: string }
|
|
18
|
+
|
|
19
|
+
type CellViewProps = {
|
|
20
|
+
row: number
|
|
21
|
+
col: number
|
|
22
|
+
/** Canonical text the grid's getCell reads from adapter.getCellText (updated by re-slice). / グリッドの getCell が adapter.getCellText から読む正準文字列。 */
|
|
23
|
+
text: string
|
|
24
|
+
/** The public cell renderer (ADR-8); this is its execution point. / 公開レンダラーの実行点。 */
|
|
25
|
+
renderCell?: (ctx: RenderCellContext) => ReactNode
|
|
26
|
+
/** Test-only re-render counter injection point (§8.2); undefined in production. / テスト専用注入点。 */
|
|
27
|
+
__onCellRender?: ProfilerOnRenderCallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Renders one cell's content (memo target — no own positioned element since v0.4).
|
|
32
|
+
* セル 1 枚の中身を描画 (memo 対象 — v0.4 から自前の配置要素は持たない)。
|
|
33
|
+
*/
|
|
34
|
+
const CellViewInner = ({ row, col, text, renderCell, __onCellRender }: CellViewProps): ReactNode => {
|
|
35
|
+
// テスト専用カウンタ: Profiler ノードの常設を避けるコスト取引 — 代わりに常設の passive
|
|
36
|
+
// effect 1 本が毎 commit 走る (注入なしなら本体は即 no-op)。フックは条件分岐できないため
|
|
37
|
+
// この登録コスト自体は受容する (初回 = mount、以後 = update)
|
|
38
|
+
const mountedRef = useRef(false)
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const phase = mountedRef.current ? "update" : "mount"
|
|
41
|
+
mountedRef.current = true
|
|
42
|
+
__onCellRender?.(`cell-${row}-${col}`, phase, 0, 0, 0, 0)
|
|
43
|
+
})
|
|
44
|
+
return renderCell ? renderCell({ row, col, text }) : text
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Memo contract: props are primitives + renderCell (identity-stable per consumer contract).
|
|
49
|
+
* memo 契約: props はプリミティブ + renderCell (identity 安定が consumer 契約)。
|
|
50
|
+
*/
|
|
51
|
+
export const CellView = memo(CellViewInner)
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Grid body: owns the VirtualGrid wiring (both axes) and the cell renderer.
|
|
3
|
+
* Since v0.4 the horizontal axis is OWNED by VirtualGrid (quantized column anchor + residual
|
|
4
|
+
* transform + horizontal bar) — the v0.3 column stack (useColumnWindow / --aqsp-hx CSS variable
|
|
5
|
+
* / absolute axis-origin cell left / own HScrollBar) is deleted per §7-1 of the VirtualGrid
|
|
6
|
+
* design plan. Wrapped in React.memo — Spreadsheet shell re-renders (selection / edit session)
|
|
7
|
+
* are isolated by identity-stable props; edit repaints arrive from the INSIDE via the
|
|
8
|
+
* editVersion subscription (renderVersion delivery — §6.9-2).
|
|
9
|
+
*
|
|
10
|
+
* v0.8 rich cells (plan §4.2.11): `resolveRichCellProps` is the ONE place the adapter's rich-cell
|
|
11
|
+
* reads happen — at most eight per cell (painted format, right / below base formats for border
|
|
12
|
+
* ownership, note, comments, hyperlink, validation, and the lazily shared text) — and it paints
|
|
13
|
+
* them as inline style + `data-aqsp-*` state attributes. The display value format it resolved
|
|
14
|
+
* is latched per cell (`takeLatchedValue`) for the `children` renderer, which therefore reads
|
|
15
|
+
* nothing from the adapter — the engine's own `getCell` text read (v0.1) stays separate, so a
|
|
16
|
+
* mounted slice costs at most 3 format reads per cell. Every read is a capability branch
|
|
17
|
+
* (`adapter.x === undefined` ⇒ the v0.1 attribute-free cell), the conditional-format index is a
|
|
18
|
+
* declared capability of the root (absent ⇒ base format only), and unchanged cells keep
|
|
19
|
+
* identical style objects (two-tier intern: paint identity, then canonical JSON for the
|
|
20
|
+
* per-call paints of edge / folded cells).
|
|
21
|
+
*
|
|
22
|
+
* グリッド本体。VirtualGrid 結線 (両軸) とセルレンダラーを所有する。v0.4 から横軸は VirtualGrid
|
|
23
|
+
* の所有 (量子化列アンカー + 残差 transform + 横バー) — v0.3 の列スタック (useColumnWindow /
|
|
24
|
+
* --aqsp-hx CSS 変数 / 軸原点絶対 left / 自前 HScrollBar) は設計計画 §7-1 に従い削除。
|
|
25
|
+
* React.memo でラップし shell 再レンダーから隔離 — 編集反映は editVersion 購読で内側から届く
|
|
26
|
+
* (renderVersion 配送 — §6.9-2)。
|
|
27
|
+
*
|
|
28
|
+
* v0.8 リッチセル (計画 §4.2.11): `resolveRichCellProps` がアダプタのリッチセル読みを行う唯一の場所 —
|
|
29
|
+
* セル毎最大 8 読み (塗り書式・罫線所有のための右 / 下隣 base 書式・ノート・コメント・リンク・検証・
|
|
30
|
+
* 遅延共有の text) — で、インライン style + `data-aqsp-*` 状態属性として塗る。そこで解決した表示の
|
|
31
|
+
* 値書式はセル毎の掛け金 (`takeLatchedValue`) で `children` レンダラーへ渡すため children はアダプタを
|
|
32
|
+
* 一切読まない — エンジン自身の `getCell` text 読み (v0.1) は別で、マウント経路の書式読みはセル毎最大 3。
|
|
33
|
+
* 全読みは能力分岐 (`adapter.x === undefined` ⇒ v0.1 の属性なしセル)、条件付き書式索引は root の宣言能力 (未指定 ⇒
|
|
34
|
+
* base 書式のみ)、無変化セルは同一の style object を保つ (2 段 intern: paint identity → 端 / 畳込み
|
|
35
|
+
* セルの呼出毎 paint は正準 JSON)。
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { VirtualGrid, type VirtualGridHandle, type VirtualGridRange } from "@aiquants/virtualscroll"
|
|
39
|
+
import { type HTMLAttributes, memo, type ProfilerOnRenderCallback, type ReactNode, type RefObject, useCallback, useMemo, useRef, useSyncExternalStore } from "react"
|
|
40
|
+
import { type AxisModel, nextVisibleIndex } from "../model/axisModel.ts"
|
|
41
|
+
import { normalizeAdapterHyperlink } from "../model/cellAnnotations.ts"
|
|
42
|
+
import { alignToJustify, type CellAlign, type CellValueFormat, formatCellValue } from "../model/cellFormat.ts"
|
|
43
|
+
import { type CellPaint, resolveCellPaint } from "../model/cellPaint.ts"
|
|
44
|
+
import { type CfIndex, type CfMemo, createCfMemo, resolvePaintedFormat } from "../model/conditionalFormat.ts"
|
|
45
|
+
import { lazyCellText, resolveCellAlign, type SpreadsheetDataAdapter } from "../model/dataAdapter.ts"
|
|
46
|
+
import { isVisiblyInvalid } from "../model/dataValidation.ts"
|
|
47
|
+
import { SCROLLBAR_WIDTH, type ViewStore } from "../model/viewStore.ts"
|
|
48
|
+
import { CellView, type RenderCellContext } from "./CellView.tsx"
|
|
49
|
+
|
|
50
|
+
/** Inline style of a cell (React camelCase keys). / セルのインライン style (React の camelCase キー)。 */
|
|
51
|
+
type CellStyle = Readonly<Record<string, string>>
|
|
52
|
+
|
|
53
|
+
/** Alignment-only styles for cells that paint nothing else — one shared frozen object per alignment. / 塗りなしセルの揃えだけの style — 揃え毎に共有の凍結 object 1 つ。 */
|
|
54
|
+
const ALIGN_ONLY_STYLES: Readonly<Record<CellAlign, CellStyle>> = Object.freeze({
|
|
55
|
+
left: Object.freeze({ justifyContent: alignToJustify("left") }),
|
|
56
|
+
center: Object.freeze({ justifyContent: alignToJustify("center") }),
|
|
57
|
+
right: Object.freeze({ justifyContent: alignToJustify("right") }),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
/** Whole-Map clearing threshold of the canonical-JSON style intern (the CF memo's bound). / 正準 JSON style intern の全消去しきい値 (CF memo と同じ上限)。 */
|
|
61
|
+
export const STYLE_INTERN_LIMIT = 4096
|
|
62
|
+
|
|
63
|
+
/** Tier 1: paint object → alignment → merged style (WeakMap so per-call paints never leak). / 第 1 段: paint object → 揃え → 合成 style (WeakMap なので呼出毎 paint はリークしない)。 */
|
|
64
|
+
const styleByPaint = new WeakMap<CellPaint, Partial<Record<CellAlign, CellStyle>>>()
|
|
65
|
+
|
|
66
|
+
/** Tier 2: canonical JSON of the merged style → the one frozen object (bounded). / 第 2 段: 合成 style の正準 JSON → 唯一の凍結 object (有界)。 */
|
|
67
|
+
const styleByKey = new Map<string, CellStyle>()
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Interns the cell's inline style per (paint, resolved alignment): the paint's style plus the
|
|
71
|
+
* alignment's `justifyContent` (an explicit alignment in the paint carries the same value, so
|
|
72
|
+
* the merge never conflicts). Tier 1 keys by paint identity (interior cells — `resolveCellPaint`
|
|
73
|
+
* interns those per format object); tier 2 keys by the canonical JSON of the merged style so
|
|
74
|
+
* the per-call paints of edge / folded cells still resolve to ONE object. Same content ⇒ the
|
|
75
|
+
* SAME frozen object across re-slices, which is what lets React skip the style diff of
|
|
76
|
+
* unchanged cells. A null paint resolves to the shared alignment-only object. Barrel-private;
|
|
77
|
+
* exported for the pins.
|
|
78
|
+
* セルのインライン style を (paint, 解決済み揃え) で intern: paint の style + 揃えの `justifyContent`
|
|
79
|
+
* (paint 内の明示揃えは同じ値を運ぶため合成は衝突しない)。第 1 段は paint identity (内部セル —
|
|
80
|
+
* `resolveCellPaint` が書式 object 毎に intern 済み)、第 2 段は合成 style の正準 JSON で、端 / 畳込み
|
|
81
|
+
* セルの呼出毎 paint も 1 つの object へ解決する。同内容 ⇒ 再スライスを跨いで同一の凍結 object で、
|
|
82
|
+
* React が無変化セルの style diff を省ける根拠。paint null は共有の揃えだけの object。バレル非公開 —
|
|
83
|
+
* ピン用に export。
|
|
84
|
+
*/
|
|
85
|
+
export const internCellStyle = (paint: CellPaint | null, align: CellAlign): CellStyle => {
|
|
86
|
+
if (paint === null) {
|
|
87
|
+
return ALIGN_ONLY_STYLES[align]
|
|
88
|
+
}
|
|
89
|
+
let byAlign = styleByPaint.get(paint)
|
|
90
|
+
if (byAlign === undefined) {
|
|
91
|
+
byAlign = {}
|
|
92
|
+
styleByPaint.set(paint, byAlign)
|
|
93
|
+
}
|
|
94
|
+
const hit = byAlign[align]
|
|
95
|
+
if (hit !== undefined) {
|
|
96
|
+
return hit
|
|
97
|
+
}
|
|
98
|
+
// 第 2 段: paint の style キーは cellPaint が固定順で書くため JSON が決定的 (= intern キー)
|
|
99
|
+
const merged = { ...paint.style, justifyContent: alignToJustify(align) }
|
|
100
|
+
const key = JSON.stringify(merged)
|
|
101
|
+
let style = styleByKey.get(key)
|
|
102
|
+
if (style === undefined) {
|
|
103
|
+
// 有界: 上限で全消去 (CF memo と同じ規律 — 同値の再生成は identity を失うだけで正しさは不変)
|
|
104
|
+
if (styleByKey.size >= STYLE_INTERN_LIMIT) {
|
|
105
|
+
styleByKey.clear()
|
|
106
|
+
}
|
|
107
|
+
style = Object.freeze(merged)
|
|
108
|
+
styleByKey.set(key, style)
|
|
109
|
+
}
|
|
110
|
+
byAlign[align] = style
|
|
111
|
+
return style
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Conditional-format capability pair (the root-built index and its memo — one rebuild key). / 条件付き書式の能力対 (root 構築の索引と memo — 同じ再構築キー)。 */
|
|
115
|
+
export type CfCapability = { readonly indexRef: RefObject<CfIndex | null>; readonly memoRef: RefObject<CfMemo> }
|
|
116
|
+
|
|
117
|
+
/** Memo handed to the painted-format seam when the CF capability is absent (never written: a null index returns before any memo use). / CF 能力なしのとき塗り書式合流点へ渡す memo (索引 null は memo に触れる前に返るため書込は起きない)。 */
|
|
118
|
+
const NO_CF_MEMO: CfMemo = createCfMemo()
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The rich-cell patch of one cell: `data-aqsp-*` state attributes, the interned inline style
|
|
122
|
+
* (null = alignment undeclared) and the display value format of the painted format (identical to
|
|
123
|
+
* the base's — the CF vocabulary has no `value`; undefined without the format capability or for a
|
|
124
|
+
* value-less format), handed to the `children` renderer through the per-cell latch so the display
|
|
125
|
+
* costs no adapter read.
|
|
126
|
+
* セル 1 つのリッチセル差分: `data-aqsp-*` 状態属性 + intern 済みインライン style (null = 揃え能力なし) +
|
|
127
|
+
* 塗り書式の表示値書式 (base と同一 — CF 語彙に `value` は無い。書式能力なし / value なし書式は undefined)。
|
|
128
|
+
* 値書式はセル毎の掛け金で `children` レンダラーへ渡し、表示にアダプタ読みを払わない。
|
|
129
|
+
*/
|
|
130
|
+
export type RichCellProps = { readonly attrs: Readonly<Record<string, string>>; readonly style: CellStyle | null; readonly value: CellValueFormat | undefined }
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Single-entry latch carrying the display value format from `getCellProps` to `children` for
|
|
134
|
+
* the SAME cell — VirtualGrid calls `getCellProps(row, col)`, `getCell(row, col)` and
|
|
135
|
+
* `children(text, row, col)` back-to-back per rendered cell (its `renderRow` builds each cell
|
|
136
|
+
* element in one expression), so one mutable entry is enough and no per-cell allocation happens.
|
|
137
|
+
* Barrel-private; exported for the pins.
|
|
138
|
+
* `getCellProps` から `children` へ**同じセル**の表示値書式を運ぶ単一エントリの掛け金 — VirtualGrid は
|
|
139
|
+
* 描画セル毎に `getCellProps(row, col)` → `getCell(row, col)` → `children(text, row, col)` を連続で呼ぶ
|
|
140
|
+
* (`renderRow` がセル要素を 1 式で組む) ため、可変エントリ 1 つで足りセル毎の割当も起きない。
|
|
141
|
+
* バレル非公開 — ピン用に export。
|
|
142
|
+
*/
|
|
143
|
+
export type DisplayLatch = { row: number; col: number; value: CellValueFormat | undefined }
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Takes the latched display value format of (row, col). A latch written for another cell means
|
|
147
|
+
* the engine broke its per-cell call order (`getCellProps` must immediately precede the cell's
|
|
148
|
+
* renderer) — that is a contract violation, not a value to substitute, so it fails fast with a
|
|
149
|
+
* RangeError instead of re-reading the adapter. Barrel-private; exported for the pins.
|
|
150
|
+
* (row, col) の掛け金に置かれた表示値書式を取り出す。別セルの掛け金はエンジンがセル毎の呼び順
|
|
151
|
+
* (`getCellProps` がそのセルのレンダラーの直前) を破った契約違反であり代替値ではないため、アダプタを
|
|
152
|
+
* 再読せず RangeError で fail-fast する。バレル非公開 — ピン用に export。
|
|
153
|
+
*/
|
|
154
|
+
export const takeLatchedValue = (latch: DisplayLatch, row: number, col: number): CellValueFormat | undefined => {
|
|
155
|
+
// 掛け金は直前の getCellProps のセルと一致して初めて意味を持つ (不一致 = 呼び順の契約違反)
|
|
156
|
+
if (latch.row !== row || latch.col !== col) {
|
|
157
|
+
throw new RangeError(`[spreadsheet] cell renderer (${row}, ${col}) ran without its getCellProps (latched ${latch.row}, ${latch.col})`)
|
|
158
|
+
}
|
|
159
|
+
return latch.value
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolves the rich-cell attributes and style of one cell (plan §4.2.11) — the ONE place the
|
|
164
|
+
* adapter's per-cell aspect reads happen, at most eight, every one behind its capability branch:
|
|
165
|
+
* (1) the painted format through `resolvePaintedFormat` (base ⊕ CF — the base by identity
|
|
166
|
+
* without the CF capability), (2) / (3) the next VISIBLE right / below neighbours' BASE formats
|
|
167
|
+
* for border ownership (CF never paints borders), (4) note, (5) comments, (6) hyperlink,
|
|
168
|
+
* (7) validation, and (8) the text through ONE lazy thunk shared by the auto alignment (align
|
|
169
|
+
* unset), the CF conditions and the invalid marker. The painted format's `value` is returned
|
|
170
|
+
* for the display (the `children` renderer never re-reads it). The alignment attribute is always emitted
|
|
171
|
+
* under the format capability (the v0.7 contract), the indicators only when present (an empty
|
|
172
|
+
* comment thread / a blank validated cell carry nothing), and `custom` validation rules are
|
|
173
|
+
* never evaluated at paint. Returns null for the v0.1 identity (no aspect declared — the text
|
|
174
|
+
* thunk is not even built) so the caller spreads nothing. Barrel-private; exported for the pins.
|
|
175
|
+
* セル 1 つのリッチセル属性と style を解決 (計画 §4.2.11) — アダプタのセル毎側面読みが起きる唯一の
|
|
176
|
+
* 場所で、最大 8 読み、全て能力分岐の内側: (1) `resolvePaintedFormat` 経由の塗り書式 (base ⊕ CF —
|
|
177
|
+
* CF 能力なしは base を identity で)、(2) / (3) 罫線所有のための次の**可視**な右 / 下隣の base 書式
|
|
178
|
+
* (CF は罫線を塗らない)、(4) ノート、(5) コメント、(6) リンク、(7) 検証、(8) 自動揃え (align 未設定) /
|
|
179
|
+
* CF 条件 / 不正マークが共有する遅延 thunk 1 本の text。塗り書式の `value` は表示用に返す (`children`
|
|
180
|
+
* レンダラーは再読しない)。揃え属性は書式能力下で常に出し (v0.7 契約)、
|
|
181
|
+
* 指標は存在するときだけ (空スレッド / 空文字の検証セルは何も出さない)、`custom` 検証規則は塗り時に
|
|
182
|
+
* 評価しない。v0.1 恒等 (側面未宣言 — text thunk も作らない) は null で、呼び手は何も spread しない。
|
|
183
|
+
* バレル非公開 — ピン用に export。
|
|
184
|
+
*/
|
|
185
|
+
export const resolveRichCellProps = (adapter: SpreadsheetDataAdapter, rowAxis: AxisModel, colAxis: AxisModel, cf: CfCapability | null, row: number, col: number): RichCellProps | null => {
|
|
186
|
+
// 能力未宣言は v0.1 の静的属性を厳密維持 (能力分岐 — 値フォールバックではない): 側面が 1 つも
|
|
187
|
+
// 宣言されていなければ属性も style も一切出さない。判定は塗り 5 側面のメンバー直接読み —
|
|
188
|
+
// aspectCapabilities のスナップショットはセル毎の塗りで短命 7 フィールド割当になるため組まない
|
|
189
|
+
// (生読み契約は直接判定でも同一で、後から宣言された能力は次の塗りに反映される)
|
|
190
|
+
if (adapter.getCellFormat === undefined && adapter.getCellNote === undefined && adapter.getCellComments === undefined && adapter.getCellHyperlink === undefined && adapter.getCellValidation === undefined) {
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
// text は遅延 1 回読み (8 読み目 — align 未設定の自動揃え / CF 条件 / isVisiblyInvalid が同じ
|
|
194
|
+
// thunk を共有)。VirtualGrid からは渡されない (engine は children 用に別途読む)
|
|
195
|
+
const readText = lazyCellText(adapter, row, col)
|
|
196
|
+
const attrs: Record<string, string> = {}
|
|
197
|
+
let style: CellStyle | null = null
|
|
198
|
+
let value: CellValueFormat | undefined
|
|
199
|
+
if (adapter.getCellFormat !== undefined) {
|
|
200
|
+
// 読み 1: 塗り書式 = base ⊕ CF (唯一の合流点 — 索引は root の宣言能力、未指定は null で base のみ = v0.7)
|
|
201
|
+
const painted = resolvePaintedFormat(adapter, cf === null ? null : cf.indexRef.current, cf === null ? NO_CF_MEMO : cf.memoRef.current, row, col, readText)
|
|
202
|
+
// 表示の値書式は塗り書式のもの (= base の value — CF 語彙に value は無い)。children はこれを掛け金で受け取り再読しない
|
|
203
|
+
value = painted === undefined ? undefined : painted.value
|
|
204
|
+
// 唯一の揃え判定 (align 明示なら text を読まない)
|
|
205
|
+
const align = resolveCellAlign(painted, readText)
|
|
206
|
+
// 読み 2 / 3: 罫線所有の畳込み相手は次の**可視**隣接の base 書式 (非表示トラックは線を持たない)。
|
|
207
|
+
// 左端 / 上端 = 軸原点に接するセル (先行トラックが全て非表示なら offset 0)
|
|
208
|
+
const rightCol = nextVisibleIndex(colAxis, col, 1)
|
|
209
|
+
const belowRow = nextVisibleIndex(rowAxis, row, 1)
|
|
210
|
+
const paint = resolveCellPaint(painted, rightCol === null ? undefined : adapter.getCellFormat(row, rightCol), belowRow === null ? undefined : adapter.getCellFormat(belowRow, col), colAxis.offsetOf(col) === 0, rowAxis.offsetOf(row) === 0)
|
|
211
|
+
if (paint !== null) {
|
|
212
|
+
Object.assign(attrs, paint.attrs)
|
|
213
|
+
}
|
|
214
|
+
// 揃え属性は解決済み揃えが唯一の書き手 (paint の明示 align と同値なので上書きは衝突しない)
|
|
215
|
+
attrs["data-aqsp-align"] = align
|
|
216
|
+
style = internCellStyle(paint, align)
|
|
217
|
+
}
|
|
218
|
+
// 読み 4: ノート指標
|
|
219
|
+
if (adapter.getCellNote !== undefined && adapter.getCellNote(row, col) !== undefined) {
|
|
220
|
+
attrs["data-aqsp-note"] = ""
|
|
221
|
+
}
|
|
222
|
+
// 読み 5: コメント指標 (空スレッドは指標なし)
|
|
223
|
+
if (adapter.getCellComments !== undefined) {
|
|
224
|
+
const comments = adapter.getCellComments(row, col)
|
|
225
|
+
if (comments !== undefined && comments.length > 0) {
|
|
226
|
+
attrs["data-aqsp-comment"] = ""
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// 読み 6: リンク指標 — 信頼境界 (ADR-10): 許可外スキームはリンク無し (開通 / copy / fill と同じ判定 —
|
|
230
|
+
// 指標だけリンクに見える不整合を作らない)。URL 解析の費用はリンクを持つセルに限る
|
|
231
|
+
if (adapter.getCellHyperlink !== undefined && normalizeAdapterHyperlink(adapter.getCellHyperlink(row, col)) !== undefined) {
|
|
232
|
+
attrs["data-aqsp-link"] = ""
|
|
233
|
+
}
|
|
234
|
+
// 読み 7 (+ 規則があれば text): 塗り時に判定できる規則だけの不正マーク (custom は評価しない)
|
|
235
|
+
if (adapter.getCellValidation !== undefined) {
|
|
236
|
+
const rule = adapter.getCellValidation(row, col)
|
|
237
|
+
if (rule !== undefined && isVisiblyInvalid(rule, readText())) {
|
|
238
|
+
attrs["data-aqsp-invalid"] = ""
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return { attrs, style, value }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
type GridBodyProps = {
|
|
245
|
+
adapter: SpreadsheetDataAdapter
|
|
246
|
+
rowAxis: AxisModel
|
|
247
|
+
colAxis: AxisModel
|
|
248
|
+
viewStore: ViewStore
|
|
249
|
+
handleRef: RefObject<VirtualGridHandle | null>
|
|
250
|
+
/** Engine hit-test / listener target element (§6.4.3 — owned by Spreadsheet). / 選択エンジンのヒットテスト / リスナー対象 (所有は Spreadsheet)。 */
|
|
251
|
+
bodyRef?: RefObject<HTMLDivElement | null>
|
|
252
|
+
/** The consumer dataVersion prop (external half of renderVersion — §6.9-2). / renderVersion の外部成分。 */
|
|
253
|
+
dataVersion: number | undefined
|
|
254
|
+
/** Fenwick baseValue for the grid's column tree (§7-8 — behaviorOptions.defaultColWidth). / 列幅木の Fenwick baseValue (§7-8)。 */
|
|
255
|
+
defaultColumnWidth: number
|
|
256
|
+
/** Frozen leading columns (v0.5 — VirtualGrid frozenLeadingCols pass-through; engine-validated). / 先頭凍結列数 (v0.5 — エンジン検証の素通し)。 */
|
|
257
|
+
frozenColumns: number
|
|
258
|
+
/** Frozen leading rows (v0.6 — VirtualGrid frozenLeadingRows pass-through; engine-validated). / 先頭凍結行数 (v0.6 — エンジン検証の素通し)。 */
|
|
259
|
+
frozenRows: number
|
|
260
|
+
renderCell?: (ctx: RenderCellContext) => ReactNode
|
|
261
|
+
/** 2D scroll notification from the grid handle (the truth — §7-4). / グリッドハンドル発の 2 軸スクロール通知 (§7-4 の真実側)。 */
|
|
262
|
+
onScroll: (position: { x: number; y: number }) => void
|
|
263
|
+
onRangeChange: (range: VirtualGridRange) => void
|
|
264
|
+
/** Unified focus recovery for the bar bands (§6.8-3 — both scrollbars live inside the grid since v0.4). / バー帯のフォーカス回収 (§6.8-3 — v0.4 から両バーはグリッド内在)。 */
|
|
265
|
+
focusSink: () => void
|
|
266
|
+
/** Editing-session probe for the §6.8-3 commit gate. / §6.8-3 確定ゲート用の編集セッションプローブ。 */
|
|
267
|
+
isEditing: () => boolean
|
|
268
|
+
/** Commit gate — false aborts the bar press keeping the session. / 確定ゲート (false でバー押下中断・セッション維持)。 */
|
|
269
|
+
requestCommit: () => boolean
|
|
270
|
+
testId?: string
|
|
271
|
+
/** Test-only CellView re-render counter seam (§8.2). / テスト専用の再レンダー計数シーム。 */
|
|
272
|
+
__onCellRender?: ProfilerOnRenderCallback
|
|
273
|
+
/**
|
|
274
|
+
* v0.8 capability (with `cfMemoRef`): the root-built conditional-format index, rebuilt per
|
|
275
|
+
* (rules identity, renderVersion) and read fresh at every slice. Absent ⇒ the painted format
|
|
276
|
+
* is the adapter's base format (v0.7). Passing only one of the two refs is a RangeError.
|
|
277
|
+
* v0.8 能力 (`cfMemoRef` と対): root が (規則 identity, renderVersion) 毎に再構築する条件付き書式
|
|
278
|
+
* 索引 — 各スライスで鮮度読み。未指定 ⇒ 塗り書式 = アダプタの base 書式 (v0.7)。片方だけは RangeError。
|
|
279
|
+
*/
|
|
280
|
+
cfIndexRef?: RefObject<CfIndex | null>
|
|
281
|
+
/** Companion memo of `cfIndexRef` (same rebuild key). / `cfIndexRef` と対の memo (同じ再構築キー)。 */
|
|
282
|
+
cfMemoRef?: RefObject<CfMemo>
|
|
283
|
+
/**
|
|
284
|
+
* v0.8 capability: formula-reference pointing probe. While pointing, a bar-band press is
|
|
285
|
+
* consumed (no commit, focus back to the sink — §4.2.11). Absent ⇒ the v0.7 gate.
|
|
286
|
+
* v0.8 能力: 参照ポインティングの判定。ポインティング中のバー帯押下は consume (確定せずシンクへ
|
|
287
|
+
* 焦点回収 — §4.2.11)。未指定 ⇒ v0.7 のゲート。
|
|
288
|
+
*/
|
|
289
|
+
isPointing?: () => boolean
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Owns the VirtualGrid wiring and the cell renderer (memo target). / VirtualGrid 結線とセルレンダラーの所有 (memo 対象)。
|
|
294
|
+
*/
|
|
295
|
+
const GridBodyInner = ({
|
|
296
|
+
adapter,
|
|
297
|
+
rowAxis,
|
|
298
|
+
colAxis,
|
|
299
|
+
viewStore,
|
|
300
|
+
handleRef,
|
|
301
|
+
bodyRef: externalBodyRef,
|
|
302
|
+
dataVersion,
|
|
303
|
+
defaultColumnWidth,
|
|
304
|
+
frozenColumns,
|
|
305
|
+
frozenRows,
|
|
306
|
+
renderCell,
|
|
307
|
+
onScroll,
|
|
308
|
+
onRangeChange,
|
|
309
|
+
focusSink,
|
|
310
|
+
isEditing,
|
|
311
|
+
requestCommit,
|
|
312
|
+
testId,
|
|
313
|
+
__onCellRender,
|
|
314
|
+
cfIndexRef,
|
|
315
|
+
cfMemoRef,
|
|
316
|
+
isPointing,
|
|
317
|
+
}: GridBodyProps): ReactNode => {
|
|
318
|
+
const internalBodyRef = useRef<HTMLDivElement | null>(null)
|
|
319
|
+
// 外部 ref (エンジン結線用) と内部 ref の合成
|
|
320
|
+
const bodyRef = externalBodyRef ?? internalBodyRef
|
|
321
|
+
// 条件付き書式索引は索引 + memo の対でだけ意味を持つ (片方だけの結線は呼び手バグ — fail-fast)
|
|
322
|
+
if ((cfIndexRef === undefined) !== (cfMemoRef === undefined)) {
|
|
323
|
+
throw new RangeError("[spreadsheet] cfIndexRef and cfMemoRef must be passed together")
|
|
324
|
+
}
|
|
325
|
+
// 能力の対を 1 値へ (identity は 2 つの ref に追随 — 通常は不変)
|
|
326
|
+
const cf = useMemo((): CfCapability | null => (cfIndexRef === undefined || cfMemoRef === undefined ? null : { indexRef: cfIndexRef, memoRef: cfMemoRef }), [cfIndexRef, cfMemoRef])
|
|
327
|
+
|
|
328
|
+
// editVersion 購読 (renderVersion 配送 §6.9-2): v0.3 は useColumnWindow キーに乗せていたが、
|
|
329
|
+
// 列窓は VirtualGrid の所有になったため editVersion を直接購読する (再レンダー源はこれ 1 本)
|
|
330
|
+
const getEditSnapshot = useCallback(() => viewStore.get().editVersion, [viewStore])
|
|
331
|
+
const subscribe = useCallback((listener: () => void) => viewStore.subscribeCoalesced(listener), [viewStore])
|
|
332
|
+
const editVersion = useSyncExternalStore(subscribe, getEditSnapshot, getEditSnapshot)
|
|
333
|
+
// ?? 0 は必須 — 省略すると editVersion + undefined = NaN で deps が恒久安定し編集が映らなくなる
|
|
334
|
+
const renderVersion = editVersion + (dataVersion ?? 0)
|
|
335
|
+
|
|
336
|
+
// ---- VirtualGrid 結線 (identity 安定 — §6.9-4) ----
|
|
337
|
+
const getRowHeight = useCallback((row: number) => rowAxis.sizeOf(row), [rowAxis])
|
|
338
|
+
const getColWidth = useCallback((col: number) => colAxis.sizeOf(col), [colAxis])
|
|
339
|
+
const getCell = useCallback((row: number, col: number) => adapter.getCellText(row, col), [adapter])
|
|
340
|
+
// ARIA コンテナ属性 (identity は行列数にのみ追随)。型は行側と同型の狭い contentProps
|
|
341
|
+
const contentProps = useMemo(() => ({ role: "grid", "aria-rowcount": adapter.rowCount, "aria-colcount": adapter.colCount, "aria-multiselectable": true }) as const, [adapter.rowCount, adapter.colCount])
|
|
342
|
+
// 挙動オプション: 行木は identity 変化で再構築 (軸再構築追随)、列幅木も対称 + defaultColWidth
|
|
343
|
+
// (§7-8 — サンプリング省略の Fenwick baseValue)。pointerDragInputs はペン / タッチのみ
|
|
344
|
+
// (マウスドラッグは選択エンジンの所有)
|
|
345
|
+
const behaviorOptions = useMemo(() => ({ enableKeyboardNavigation: false, pointerDragInputs: ["pen", "touch"] as const, resetOnGetItemHeightChange: true, resetOnGetColWidthChange: true, defaultColWidth: defaultColumnWidth }), [defaultColumnWidth])
|
|
346
|
+
const scrollBarOptions = useMemo(() => ({ width: SCROLLBAR_WIDTH }), [])
|
|
347
|
+
// 表示値書式の掛け金 (getCellProps が書き、同じセルの children が直後に消費 — 番兵は届かない座標)
|
|
348
|
+
const displayLatchRef = useRef<DisplayLatch>({ row: -1, col: -1, value: undefined })
|
|
349
|
+
/**
|
|
350
|
+
* Cell attribute surface (§7-3 — the v0.3 CellView div contract moved onto the grid-owned
|
|
351
|
+
* cell node) + the v0.8 rich-cell paint (plan §4.2.11) resolved by `resolveRichCellProps`:
|
|
352
|
+
* null keeps the v0.1 static attributes byte-identical. Latches the resolved display value
|
|
353
|
+
* format for the cell's `children` call (the engine's per-cell call order) so the display
|
|
354
|
+
* pays no adapter read.
|
|
355
|
+
* セル属性面 (§7-3 — v0.3 の CellView div 契約をグリッド所有セルノードへ移植) + v0.8 のリッチセル塗り
|
|
356
|
+
* (計画 §4.2.11、`resolveRichCellProps` が解決): null は v0.1 の静的属性を厳密維持。解決した表示値書式は
|
|
357
|
+
* そのセルの `children` 呼び (エンジンのセル毎呼び順) のために掛け金へ置き、表示にアダプタ読みを払わない。
|
|
358
|
+
*/
|
|
359
|
+
const getCellProps = useCallback(
|
|
360
|
+
(row: number, col: number) => {
|
|
361
|
+
const base = {
|
|
362
|
+
// aria-rowindex は行ラッパーと重複出力 (合法な上位集合) — セル単独が AT に読まれた際の行文脈補助 (§10-1)
|
|
363
|
+
role: "gridcell",
|
|
364
|
+
"aria-rowindex": row + 1,
|
|
365
|
+
"aria-colindex": col + 1,
|
|
366
|
+
"data-aqsp-cell": "",
|
|
367
|
+
"data-row": row,
|
|
368
|
+
"data-col": col,
|
|
369
|
+
className: "aqsp-cell",
|
|
370
|
+
}
|
|
371
|
+
const rich = resolveRichCellProps(adapter, rowAxis, colAxis, cf, row, col)
|
|
372
|
+
// 掛け金は毎セル更新 (v0.1 恒等の null でも座標を進める — children が消費するのは書式能力下だけ)
|
|
373
|
+
const latch = displayLatchRef.current
|
|
374
|
+
latch.row = row
|
|
375
|
+
latch.col = col
|
|
376
|
+
latch.value = rich === null ? undefined : rich.value
|
|
377
|
+
if (rich === null) {
|
|
378
|
+
return base as HTMLAttributes<HTMLDivElement>
|
|
379
|
+
}
|
|
380
|
+
return (rich.style === null ? { ...base, ...rich.attrs } : { ...base, ...rich.attrs, style: rich.style }) as HTMLAttributes<HTMLDivElement>
|
|
381
|
+
},
|
|
382
|
+
[adapter, rowAxis, colAxis, cf],
|
|
383
|
+
)
|
|
384
|
+
/** Row attribute surface — carries the engine event gate (data-aqsp-cells §6.4.3-1). / 行属性面 — エンジンのイベントゲート (data-aqsp-cells) を運ぶ。 */
|
|
385
|
+
const getRowProps = useCallback(
|
|
386
|
+
(row: number) =>
|
|
387
|
+
({
|
|
388
|
+
role: "row",
|
|
389
|
+
"aria-rowindex": row + 1,
|
|
390
|
+
"data-aqsp-row": row,
|
|
391
|
+
// data-aqsp-cells は選択エンジンの標的ゲート (§6.4.3-1) — グリッド内にはスクロール
|
|
392
|
+
// バーも同居しており、セル領域由来のイベントだけを機械へ通す (v0.3 の .aqsp-cells
|
|
393
|
+
// ラッパーの契約を行ラッパーへ移植 — 中間ノード 1 段の削減)
|
|
394
|
+
"data-aqsp-cells": "",
|
|
395
|
+
className: "aqsp-row",
|
|
396
|
+
}) as HTMLAttributes<HTMLDivElement>,
|
|
397
|
+
[],
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
// renderVersion は children identity に乗せる (§6.9-2): identity 変化が VirtualGrid の行
|
|
401
|
+
// レンダラーを作り替え、可視セル全再スライス (getCell 再読み) を駆動する — v0.3 の
|
|
402
|
+
// useColumnWindow キー方式と同じ配送保証
|
|
403
|
+
const children = useCallback(
|
|
404
|
+
(text: string, row: number, col: number): ReactNode => {
|
|
405
|
+
// renderVersion は識別子として参照する (identity-bust — 値そのものは使わない)
|
|
406
|
+
void renderVersion
|
|
407
|
+
// v0.7 表示形式: renderCell 指定時は素の text を素通し (escape hatch・組み込み値書式に優先)、
|
|
408
|
+
// 未指定時のみ値書式を表示文字列へ適用 (編集 / コピーは素の値のまま)。値書式は直前の
|
|
409
|
+
// getCellProps が掛け金へ置いた塗り書式の value (= base の value — CF 語彙に value は無い) で、
|
|
410
|
+
// アダプタは再読しない (書式能力なしは掛け金にも触れない v0.7 恒等)
|
|
411
|
+
let display = text
|
|
412
|
+
if (renderCell === undefined && adapter.getCellFormat !== undefined) {
|
|
413
|
+
display = formatCellValue(text, takeLatchedValue(displayLatchRef.current, row, col))
|
|
414
|
+
}
|
|
415
|
+
return <CellView row={row} col={col} text={display} renderCell={renderCell} __onCellRender={__onCellRender} />
|
|
416
|
+
},
|
|
417
|
+
[renderVersion, renderCell, __onCellRender, adapter],
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return (
|
|
421
|
+
<div
|
|
422
|
+
ref={bodyRef}
|
|
423
|
+
className="aqsp-body"
|
|
424
|
+
data-aqsp-body=""
|
|
425
|
+
data-testid={testId ? `${testId}-body` : undefined}
|
|
426
|
+
onPointerDownCapture={(event) => {
|
|
427
|
+
// バー帯の統合ゲート (§6.8-3 — v0.4 でスクロールバー 2 本ともグリッド内在化):
|
|
428
|
+
// セル領域 (data-aqsp-cells) 由来は選択エンジンの所有 — 素通し。それ以外 (縦バー /
|
|
429
|
+
// 横バー帯 / 余白) は v0.3 HScrollBar と同一の 4 サイト共通ゲートを適用する
|
|
430
|
+
if (event.target instanceof Element && event.target.closest("[data-aqsp-cells]") !== null) {
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
// mouse 限定: タッチはスクロールジェスチャ専用でシンク focus はモバイル OSK を招く
|
|
434
|
+
if (event.pointerType !== "mouse") {
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
// 非主ボタンは押下の既定動作 (フォーカス移動) だけ止めて素通し — ネイティブメニューは
|
|
438
|
+
// preventDefault でも届き、止めないと root group へ逃げたフォーカスでキーボードが全滅する (R2 是正)
|
|
439
|
+
if (event.button !== 0) {
|
|
440
|
+
event.preventDefault()
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
// フォーカス回収の一本化 (§6.8-3): バー操作後も Ctrl+C が発火する状態を保つ。
|
|
444
|
+
// ❗ capture 必須 — ScrollBar のサム / トラック / 矢印ハンドラは合成 stopPropagation
|
|
445
|
+
// で終わるため、バブル側の親ハンドラは機能押下の全てで死にコードになる
|
|
446
|
+
event.preventDefault()
|
|
447
|
+
// 参照ポインティング中 (v0.8 §4.2.11): バー帯押下は確定させず consume してシンクへ焦点を
|
|
448
|
+
// 戻す (セッションと部分数式を保つ — 能力なしは v0.7 のゲートへ)
|
|
449
|
+
if (isPointing?.() === true) {
|
|
450
|
+
event.stopPropagation()
|
|
451
|
+
focusSink()
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
// 編集中の requestCommit ゲート (§6.8-3): バーもフォーカス不動で focusout が発火
|
|
455
|
+
// しない — ここが唯一の確定経路。失敗は押下中断 (capture の stopPropagation で
|
|
456
|
+
// ScrollBar のサム / トラック処理を止め、セッションを維持する)
|
|
457
|
+
if (isEditing() && !requestCommit()) {
|
|
458
|
+
event.stopPropagation()
|
|
459
|
+
focusSink()
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
focusSink()
|
|
463
|
+
}}>
|
|
464
|
+
<VirtualGrid<string>
|
|
465
|
+
ref={handleRef}
|
|
466
|
+
rowCount={adapter.rowCount}
|
|
467
|
+
colCount={adapter.colCount}
|
|
468
|
+
frozenLeadingCols={frozenColumns}
|
|
469
|
+
frozenLeadingRows={frozenRows}
|
|
470
|
+
getRowHeight={getRowHeight}
|
|
471
|
+
getColWidth={getColWidth}
|
|
472
|
+
getCell={getCell}
|
|
473
|
+
getCellProps={getCellProps}
|
|
474
|
+
getRowProps={getRowProps}
|
|
475
|
+
contentProps={contentProps}
|
|
476
|
+
behaviorOptions={behaviorOptions}
|
|
477
|
+
scrollBarOptions={scrollBarOptions}
|
|
478
|
+
callbackThrottleMs={0}
|
|
479
|
+
onScroll={onScroll}
|
|
480
|
+
onRangeChange={onRangeChange}
|
|
481
|
+
testId={testId}>
|
|
482
|
+
{children}
|
|
483
|
+
</VirtualGrid>
|
|
484
|
+
</div>
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Memo boundary isolating the body from shell re-renders (§6.9-3). / shell 再レンダー隔離境界。 */
|
|
489
|
+
export const GridBody = memo(GridBodyInner)
|