@aiquants/spreadsheet 0.8.0 → 0.10.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 +104 -0
- package/README.md +160 -13
- package/dist/index.cjs +13 -13
- package/dist/index.d.cts +156 -7
- package/dist/index.d.ts +156 -7
- package/dist/index.js +4508 -4057
- package/dist/styles/spreadsheet.css +1 -1
- package/dist/styles/spreadsheet.standalone.css +1 -1
- package/package.json +5 -5
- package/src/Spreadsheet.tsx +350 -34
- package/src/clipboard/tiling.ts +106 -16
- package/src/clipboard/useClipboard.ts +120 -7
- package/src/commands/useCommands.ts +226 -42
- package/src/editor/useCellEditor.ts +62 -13
- package/src/grid/GridBody.tsx +45 -3
- package/src/header/ColumnHeader.tsx +84 -12
- package/src/header/RowHeader.tsx +79 -11
- package/src/header/resizeStrips.ts +86 -1
- package/src/header/useHeaderResize.ts +88 -20
- package/src/header/useHeaderSelection.ts +28 -9
- package/src/index.ts +5 -1
- package/src/keyboard/ensureCellVisible.ts +40 -17
- package/src/keyboard/useGridKeyboard.ts +34 -7
- package/src/model/dataAdapter.ts +121 -2
- package/src/model/frozenPartition.ts +183 -59
- package/src/model/labels.ts +8 -2
- package/src/overlay/SelectionOverlay.tsx +599 -116
- package/src/overlay/useFillHandle.ts +100 -13
- package/src/popover/CommentPopover.tsx +14 -2
- package/src/popover/LinkPopover.tsx +15 -3
- package/src/popover/NotePopover.tsx +24 -6
- package/src/popover/usePopovers.ts +128 -18
- package/src/selection/useAutoScroller.ts +58 -15
- package/src/selection/useSelectionEngine.ts +27 -10
- package/src/styles/spreadsheet.css +32 -0
package/src/Spreadsheet.tsx
CHANGED
|
@@ -39,9 +39,10 @@
|
|
|
39
39
|
* openPopoverRef / hideNoteTipRef / pasteRef / pendingPasteOptionsRef / ポップオーバーアンカー) を所有する。
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import type { VirtualGridHandle, VirtualGridRange } from "@aiquants/virtualscroll"
|
|
42
|
+
import type { ScrollBarTapCircleOptions, VirtualGridHandle, VirtualGridRange } from "@aiquants/virtualscroll"
|
|
43
43
|
import type { ProfilerOnRenderCallback } from "react"
|
|
44
44
|
import { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"
|
|
45
|
+
import type { ClipChannel } from "./clipboard/clipPayload.ts"
|
|
45
46
|
import type { PasteOptions } from "./clipboard/pasteSpecial.ts"
|
|
46
47
|
import { type ClipInfo, type PasteOutcome, type PasteSheet, useClipboard } from "./clipboard/useClipboard.ts"
|
|
47
48
|
import { type BorderPreset, type CommandRunner, useCommands } from "./commands/useCommands.ts"
|
|
@@ -62,7 +63,7 @@ import { type CellCoord, type CellRange, clampCoord, makeRange, rangeSize, type
|
|
|
62
63
|
import { type CellBorder, type CellFormat, type CellFormatPatch, formatCellValue } from "./model/cellFormat.ts"
|
|
63
64
|
import { type EditPaint, IDLE_EDIT_PAINT, normalizeColor, resolveEditPaint } from "./model/cellPaint.ts"
|
|
64
65
|
import { buildCfIndex, type CfIndex, type CfMemo, createCfMemo, resolvePaintedFormat } from "./model/conditionalFormat.ts"
|
|
65
|
-
import { type CellEdit, type EditContext, type EditRejection, type EditSource, lazyCellText, resolveCellAlign, type SpreadsheetDataAdapter } from "./model/dataAdapter.ts"
|
|
66
|
+
import { type CellEdit, type EditContext, type EditRejection, type EditSource, isReadOnlyCell, lazyCellText, type ReadOnlyRejection, readOnlyDeclared, resolveCellAlign, type SpreadsheetDataAdapter } from "./model/dataAdapter.ts"
|
|
66
67
|
import { type DataValidationRule, type ValidationVerdict, validateCellText } from "./model/dataValidation.ts"
|
|
67
68
|
import type { ReferenceAxis } from "./model/formulaRefs.ts"
|
|
68
69
|
import { colToViewportX, rowToViewportY } from "./model/frozenPartition.ts"
|
|
@@ -177,6 +178,45 @@ export type SpreadsheetProps = {
|
|
|
177
178
|
* 位置に依らず常時可視で、可視化の縦成分は no-op。飛行中の変更は全ドラッグを取り消す。
|
|
178
179
|
*/
|
|
179
180
|
frozenRows?: number
|
|
181
|
+
/**
|
|
182
|
+
* Freezes the LAST N columns at the right edge (v0.9 — the VirtualGrid frozenTrailingCols
|
|
183
|
+
* pass-through; LIVE prop, default 0). Validation SSOT is the engine's fail-fast contract
|
|
184
|
+
* (integer in [0, 128] — RangeError otherwise; the count clamp is leading-wins:
|
|
185
|
+
* effectiveTrailing = min(T, colCount − effectiveLeading)). Trailing columns stay visible at
|
|
186
|
+
* any horizontal scroll — with ONE registered exception: when the leading + trailing extent
|
|
187
|
+
* exceeds the viewport, occluded trailing cells cannot be scrolled into view and keyboard
|
|
188
|
+
* focus paints no visible frame (ADR-19-1). scrollToCell / ensure-visible treat a trailing
|
|
189
|
+
* target as a horizontal no-op. A mid-flight change cancels every in-flight drag without
|
|
190
|
+
* restore (the frozenColumns cancel seam).
|
|
191
|
+
* 末尾 N 列の右端凍結 (v0.9 — VirtualGrid frozenTrailingCols の素通し。ライブ prop、既定 0)。
|
|
192
|
+
* 検証 SSOT はエンジンの fail-fast 契約 ([0, 128] 整数、それ以外 RangeError。件数クランプは
|
|
193
|
+
* leading 勝ち: min(T, colCount − effectiveLeading))。末尾列は任意の横スクロールで可視 —
|
|
194
|
+
* ただし leading + trailing の extent が viewport を超える overlap では遮蔽された末尾セルを
|
|
195
|
+
* スクロールで可視化できず、キーボードフォーカスは可視フレームを描かない (ADR-19-1 の登記済み
|
|
196
|
+
* 帰結)。scrollToCell / ensure-visible は末尾ターゲットを横 no-op として扱う。飛行中の変更は
|
|
197
|
+
* 全ドラッグを復元なしで取り消す (frozenColumns と同じシーム)。
|
|
198
|
+
*/
|
|
199
|
+
frozenTrailingColumns?: number
|
|
200
|
+
/**
|
|
201
|
+
* Freezes the LAST N rows at the bottom (v0.9 — the VirtualGrid frozenTrailingRows
|
|
202
|
+
* pass-through; LIVE prop, default 0 — the row twin of frozenTrailingColumns, same engine
|
|
203
|
+
* fail-fast contract and cancel seam). The typical use is a pinned totals row.
|
|
204
|
+
* 末尾 N 行の下端凍結 (v0.9 — frozenTrailingRows の素通し。ライブ prop、既定 0 —
|
|
205
|
+
* frozenTrailingColumns の行双子。エンジン契約・取消シームは同文)。典型は固定合計行。
|
|
206
|
+
*/
|
|
207
|
+
frozenTrailingRows?: number
|
|
208
|
+
/**
|
|
209
|
+
* Tap-scroll circle tuning (v0.10 — the VirtualGrid `scrollBarOptions.tapScrollCircleOptions`
|
|
210
|
+
* engine-validated pass-through; LIVE prop): position and enable/disable of the touch
|
|
211
|
+
* tap-scroll circle. The engine's `resolveTapScrollCircleOptions` is the validation SSOT —
|
|
212
|
+
* undefined is the engine's declared absence signal (engine defaults), never a value
|
|
213
|
+
* fallback in this package.
|
|
214
|
+
* タップサークル調整 (v0.10 — VirtualGrid `scrollBarOptions.tapScrollCircleOptions` の
|
|
215
|
+
* エンジン検証素通し。ライブ prop): タッチのタップスクロールサークルの位置・有効 / 無効の
|
|
216
|
+
* 調整口。検証の SSOT はエンジンの `resolveTapScrollCircleOptions` — undefined はエンジン
|
|
217
|
+
* 宣言の不在信号 (エンジン既定) で、本パッケージでの値フォールバックなし。
|
|
218
|
+
*/
|
|
219
|
+
tapScrollCircleOptions?: ScrollBarTapCircleOptions
|
|
180
220
|
/**
|
|
181
221
|
* Identity-stable contract (same as adapter): an inline arrow here defeats the CellView memo
|
|
182
222
|
* and turns every host re-render into a full visible re-slice (~800 cells at 16k columns).
|
|
@@ -228,6 +268,39 @@ export type SpreadsheetProps = {
|
|
|
228
268
|
* Escape は WCAG 2.1.2 のフォーカス脱出)。
|
|
229
269
|
*/
|
|
230
270
|
onEditRejected?: (error: unknown, rejection: EditRejection) => void
|
|
271
|
+
/**
|
|
272
|
+
* Read-only refusal notice (v0.10): fired once per refused gesture — the adapter was never
|
|
273
|
+
* called, so this is neither an EditRejection nor an operation limit. Never fired when the
|
|
274
|
+
* adapter does not declare `isCellReadOnly`; omit for silence.
|
|
275
|
+
* 読み取り専用拒否通知 (v0.10)。拒否されたジェスチャ毎に 1 回発火 — adapter は呼ばれて
|
|
276
|
+
* いないため EditRejection でも操作上限でもない。`isCellReadOnly` 未宣言では発火せず、
|
|
277
|
+
* 省略時は通知なし。
|
|
278
|
+
*/
|
|
279
|
+
onReadOnlyRejected?: (rejection: ReadOnlyRejection) => void
|
|
280
|
+
/**
|
|
281
|
+
* AT announcement text for a read-only refusal (v0.10), routed through the live region;
|
|
282
|
+
* omit for silence (no built-in strings). Inherits the announcement debounce + rAF collapse,
|
|
283
|
+
* which is what keeps autorepeat / IME refusal storms from spamming the live region.
|
|
284
|
+
* 読み取り専用拒否の AT 読み上げ文言 (v0.10) — live region 経由。省略で無音 (組み込み文言
|
|
285
|
+
* なし)。読み上げのデバウンス + rAF 合流を継承し、autorepeat / IME の拒否連打でも live
|
|
286
|
+
* region は溢れない。
|
|
287
|
+
*/
|
|
288
|
+
formatReadOnlyAnnouncement?: (rejection: ReadOnlyRejection) => string
|
|
289
|
+
/**
|
|
290
|
+
* Paste outcome summary (v0.10 plan §3.1): the menu channel mirrors `paste(options)`'s
|
|
291
|
+
* resolve value 1:1 (read refusals and the write-time re-gate included); the event channel
|
|
292
|
+
* fires once per gesture that reaches the paste pipeline — the drag-freeze consumption and a
|
|
293
|
+
* null `clipboardData` stay deliberately silent, and an empty grid attaches no listener at
|
|
294
|
+
* all. Cause-specific callbacks (`onClipboardUnavailable` / `onOperationRejected` /
|
|
295
|
+
* `onReadOnlyRejected`) fire FIRST — `"rejected"` can be the trailing summary of one of
|
|
296
|
+
* them. Omit = never fired.
|
|
297
|
+
* 貼り付け結果の要約 (v0.10 計画 §3.1)。menu チャネルは `paste(options)` の resolve 値の
|
|
298
|
+
* 1:1 鏡 (読み拒否・書込時再ゲート含む)、event チャネルは貼り付けパイプラインに到達した
|
|
299
|
+
* ゲスチャ毎 1 回 — ドラッグ凍結消費と `clipboardData` null は意図的無言、空グリッドは
|
|
300
|
+
* リスナ自体が無い。原因特定系 callback が先に発火し、`"rejected"` はその後追いの要約に
|
|
301
|
+
* なり得る。省略時は発火なし。
|
|
302
|
+
*/
|
|
303
|
+
onPasteResolved?: (outcome: PasteOutcome, channel: ClipChannel) => void
|
|
231
304
|
/** Change notice for out-of-package writes (undo / collaboration); each increment repaints visible cells (§6.9-2). / パッケージ外経路のデータ変更通知。 */
|
|
232
305
|
dataVersion?: number
|
|
233
306
|
/** Operation cell-count ceiling, default 100,000 (ADR-12). / 操作セル数上限。 */
|
|
@@ -254,12 +327,19 @@ export type SpreadsheetProps = {
|
|
|
254
327
|
* AT announcement text for the active cell; omit for silence (no built-in strings). `text`
|
|
255
328
|
* is the RAW canonical getCellText; `displayText` is what a sighted user sees (the
|
|
256
329
|
* getCellFormat-formatted string — equals `text` when the cell has no format), so AT can
|
|
257
|
-
* hear "87.0%" for a cell whose raw value is "0.87".
|
|
330
|
+
* hear "87.0%" for a cell whose raw value is "0.87". `readOnly` (v0.10) is
|
|
331
|
+
* declaration-gated three-valued: the key is ABSENT when the adapter does not declare
|
|
332
|
+
* `isCellReadOnly` (payload byte-identical to v0.9), `false` = declared editable, `true` =
|
|
333
|
+
* declared read-only — the navigation-time read-only channel (DOM focus stays on the sink,
|
|
334
|
+
* so `aria-readonly` is inert during Arrow navigation).
|
|
258
335
|
* AT 向け読み上げ文言 (省略で無音)。`text` は素の正準 getCellText、`displayText` は目視と同じ
|
|
259
336
|
* 表示文字列 (getCellFormat 整形後 — 書式なしセルでは text と同値)。素値 "0.87" の表示
|
|
260
|
-
* "87.0%"
|
|
337
|
+
* "87.0%" をそのまま読み上げられる。`readOnly` (v0.10) は宣言ゲートの 3 値: `isCellReadOnly`
|
|
338
|
+
* 未宣言はキー自体なし (payload は v0.9 とバイト同一)、`false` = 宣言済み編集可、`true` =
|
|
339
|
+
* 宣言済み読み取り専用 — 矢印航行時の読み取り専用チャネル (フォーカスはシンク常駐のため
|
|
340
|
+
* `aria-readonly` は航行中の AT に届かない)。
|
|
261
341
|
*/
|
|
262
|
-
formatCellAnnouncement?: (ctx: { row: number; col: number; text: string; displayText: string; columnName: string }) => string
|
|
342
|
+
formatCellAnnouncement?: (ctx: { row: number; col: number; text: string; displayText: string; columnName: string; readOnly?: boolean }) => string
|
|
263
343
|
/** AT announcement for the selection extent ("A1 to C3, 3 rows 3 cols"); omit for silence. / AT 向け選択範囲読み上げ。 */
|
|
264
344
|
formatSelectionAnnouncement?: (ctx: { range: CellRange; anchor: CellCoord; active: CellCoord; rangeRowCount: number; rangeColCount: number; rangeCount: number }) => string
|
|
265
345
|
/**
|
|
@@ -417,6 +497,17 @@ const stepZoom = (value: number, direction: 1 | -1): number => (direction > 0 ?
|
|
|
417
497
|
type SpreadsheetTestProps = {
|
|
418
498
|
/** Test-only CellView re-render counter; never passed in production. / テスト専用カウンタ。 */
|
|
419
499
|
__onCellRender?: ProfilerOnRenderCallback
|
|
500
|
+
/**
|
|
501
|
+
* Test-only virtual-keyboard raise spy (v0.10 plan §6.4 / RC-G2t): called synchronously by
|
|
502
|
+
* the single `raiseVirtualKeyboard` funnel, so spy count ≡ raise executions — a React-batched
|
|
503
|
+
* raise-then-lower mutates no DOM (`inputmode` observer-blind) yet still counts. Never
|
|
504
|
+
* passed in production (zero calls when unset — the `__onCellRender` conduct).
|
|
505
|
+
* テスト専用の仮想キーボード招待スパイ (v0.10 計画 §6.4 / RC-G2t)。単一漏斗
|
|
506
|
+
* `raiseVirtualKeyboard` が同期で呼ぶため、計数 ≡ raise 実行 — React バッチの
|
|
507
|
+
* raise-then-lower は DOM 無変異 (`inputmode` の観測は盲目) でも計数には映る。production
|
|
508
|
+
* では渡さない (未指定 = 呼出しゼロ — `__onCellRender` と同じ品行)。
|
|
509
|
+
*/
|
|
510
|
+
__onVirtualKeyboardRaise?: () => void
|
|
420
511
|
}
|
|
421
512
|
|
|
422
513
|
const DEFAULT_ROW_HEIGHT = 24
|
|
@@ -525,6 +616,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
525
616
|
defaultColumnWidth = DEFAULT_COLUMN_WIDTH,
|
|
526
617
|
frozenColumns = 0,
|
|
527
618
|
frozenRows = 0,
|
|
619
|
+
frozenTrailingColumns = 0,
|
|
620
|
+
frozenTrailingRows = 0,
|
|
621
|
+
tapScrollCircleOptions,
|
|
528
622
|
renderCell,
|
|
529
623
|
onSelectionChange,
|
|
530
624
|
onCellContextMenu,
|
|
@@ -533,6 +627,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
533
627
|
onCopyMultiRange,
|
|
534
628
|
onOperationRejected,
|
|
535
629
|
onEditRejected,
|
|
630
|
+
onReadOnlyRejected,
|
|
631
|
+
formatReadOnlyAnnouncement,
|
|
536
632
|
dataVersion,
|
|
537
633
|
maxOperationCells = DEFAULT_MAX_OPERATION_CELLS,
|
|
538
634
|
enableFillHandle = true,
|
|
@@ -555,6 +651,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
555
651
|
onValidationRejected,
|
|
556
652
|
onPasteSpecialRequest,
|
|
557
653
|
onClipboardUnavailable,
|
|
654
|
+
onPasteResolved,
|
|
558
655
|
now = Date.now,
|
|
559
656
|
zoom,
|
|
560
657
|
defaultZoom,
|
|
@@ -562,6 +659,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
562
659
|
testId,
|
|
563
660
|
className,
|
|
564
661
|
__onCellRender,
|
|
662
|
+
__onVirtualKeyboardRaise,
|
|
565
663
|
}: SpreadsheetProps & SpreadsheetTestProps,
|
|
566
664
|
ref,
|
|
567
665
|
): ReactNode {
|
|
@@ -747,6 +845,16 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
747
845
|
const windowOriginYRef = useRef(0)
|
|
748
846
|
const colTrackRef = useRef<HTMLDivElement | null>(null)
|
|
749
847
|
const windowOriginXRef = useRef(0)
|
|
848
|
+
// ヘッダー帯要素 (選択機械 / オートスクローラーの結線先 — 従来はオートスクローラー節で宣言)
|
|
849
|
+
const colHeaderRef = useRef<HTMLDivElement | null>(null)
|
|
850
|
+
const rowHeaderRef = useRef<HTMLDivElement | null>(null)
|
|
851
|
+
// ヘッダー分割線セグメントのマウント時キャッシュ (v0.9 §2.4 / §4.9 census #12/#13):
|
|
852
|
+
// ヘッダー再描画は同期横パス上の毎通知呼びのため、querySelector 走査ではなく JSX ref で
|
|
853
|
+
// 装着済みノードを直読みする (存在は件数駆動 render 双子 — アンマウントで React が null へ戻す)
|
|
854
|
+
const dividerXLeadingRef = useRef<HTMLDivElement | null>(null)
|
|
855
|
+
const dividerXTrailingRef = useRef<HTMLDivElement | null>(null)
|
|
856
|
+
const dividerYLeadingRef = useRef<HTMLDivElement | null>(null)
|
|
857
|
+
const dividerYTrailingRef = useRef<HTMLDivElement | null>(null)
|
|
750
858
|
const liveRegionRef = useRef<HTMLDivElement | null>(null)
|
|
751
859
|
|
|
752
860
|
// ---- 選択状態 (React state + 命令読み ref — §6.9-1) ----
|
|
@@ -873,11 +981,42 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
873
981
|
}, [])
|
|
874
982
|
|
|
875
983
|
/**
|
|
876
|
-
*
|
|
984
|
+
* Equality-suppressed display heal of one header divider segment (v0.9 §4.9 census #12/#13):
|
|
985
|
+
* visibility = band TREE px > 0; existence is the header's count-driven render twin and the
|
|
986
|
+
* geometry stays clip-owned, so this display flip is the ONLY divider write the header
|
|
987
|
+
* repaint path ever makes. The segment arrives through the mount-cached JSX ref (never a
|
|
988
|
+
* per-notification querySelector traversal — the synchronous horizontal pass must not pay
|
|
989
|
+
* a selector scan, least of all a guaranteed miss at F = T = 0). A same-value pass writes
|
|
990
|
+
* nothing — the §5.7 zero-write invariant observes both header repaint writers.
|
|
991
|
+
* ヘッダー分割線セグメント 1 つの等値抑制 display heal (v0.9 §4.9 census #12/#13)。可視 =
|
|
992
|
+
* 帯 TREE px > 0。存在はヘッダーの件数駆動 render 双子、幾何はクリップ所有のままで、ヘッダー
|
|
993
|
+
* 再描画が分割線へ行う書込はこの display フリップだけ。セグメントはマウント時キャッシュの
|
|
994
|
+
* JSX ref 経由 (毎通知の querySelector 走査は同期横パスに載せない — F = T = 0 では全走査
|
|
995
|
+
* ミスが保証される)。同値パスは無書込 — §5.7 のゼロ書込不変量が両書き手を観測する。
|
|
996
|
+
*/
|
|
997
|
+
const healHeaderDivider = useCallback((segment: HTMLElement | null, shown: boolean) => {
|
|
998
|
+
// 件数 0 の家族はセグメント未マウント (ref は null) — 無仕事で戻る
|
|
999
|
+
if (segment === null) {
|
|
1000
|
+
return
|
|
1001
|
+
}
|
|
1002
|
+
const next = shown ? "" : "none"
|
|
1003
|
+
// 等値抑制 — 定常スクロール wheel 窓中の分割線 style 書込 = 0 の不変量 (§2.4 / §5.7)
|
|
1004
|
+
if (segment.style.display !== next) {
|
|
1005
|
+
segment.style.display = next
|
|
1006
|
+
}
|
|
1007
|
+
}, [])
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Repaints the row-header track residual transform. Callers: onScroll, resizeRow, and
|
|
877
1011
|
* RowHeader's layout effect. vy comes from the grid handle (the truth — §7-4; the v0.6 −H_F
|
|
878
|
-
* frozen-band term makes this the exact twin of repaintColHeaders).
|
|
879
|
-
*
|
|
880
|
-
*
|
|
1012
|
+
* frozen-band term makes this the exact twin of repaintColHeaders). v0.9 adds the
|
|
1013
|
+
* equality-suppressed display heal of the two y header divider segments (§4.9 census #12 —
|
|
1014
|
+
* NO geometry write: the scroll-clip insets are the registered calc() forms that re-resolve
|
|
1015
|
+
* through 100% by themselves).
|
|
1016
|
+
* 行 track の残差 transform を書く命令再描画。vy はグリッドハンドル (§7-4 の真実) から読む
|
|
1017
|
+
* (v0.6 の −H_F 凍結帯項で repaintColHeaders の完全双子)。v0.9 で y ヘッダー分割線 2 セグメント
|
|
1018
|
+
* の等値抑制 display heal を追加 (§4.9 census #12 — 幾何書込なし: scroll クリップのインセットは
|
|
1019
|
+
* 登記 calc() 形が 100% 再解決で自癒する)。
|
|
881
1020
|
*/
|
|
882
1021
|
const repaintHeaders = useCallback(() => {
|
|
883
1022
|
const track = rowTrackRef.current
|
|
@@ -888,14 +1027,27 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
888
1027
|
const frozenHeight = handleRef.current?.getFrozenSize().height ?? 0
|
|
889
1028
|
track.style.transform = `translateY(${windowOriginYRef.current - vy - frozenHeight}px)`
|
|
890
1029
|
}
|
|
891
|
-
|
|
1030
|
+
// v0.9 (§4.9 census #12): y ヘッダー分割線の可視 heal — 可視 = 帯 TREE px > 0 (H_F / H_T の
|
|
1031
|
+
// ハンドルフィールド)。ハンドル未装着は無書込 (JSX 初期状態のまま = 件数 render が真実)。
|
|
1032
|
+
// 両セグメント未マウント (R = T = 0) は getFrozenSize 読みごと省く — 同期横パスの無仕事保証
|
|
1033
|
+
const handle = handleRef.current
|
|
1034
|
+
if (handle !== null && (dividerYLeadingRef.current !== null || dividerYTrailingRef.current !== null)) {
|
|
1035
|
+
const frozen = handle.getFrozenSize()
|
|
1036
|
+
healHeaderDivider(dividerYLeadingRef.current, frozen.height > 0)
|
|
1037
|
+
healHeaderDivider(dividerYTrailingRef.current, frozen.trailingHeight > 0)
|
|
1038
|
+
}
|
|
1039
|
+
}, [healHeaderDivider])
|
|
892
1040
|
|
|
893
1041
|
/**
|
|
894
|
-
* Repaints
|
|
1042
|
+
* Repaints the column-header track residual transform (§7-5 — the row repaintHeaders
|
|
895
1043
|
* twin; the v0.3 --aqsp-hx CSS variable is retired). hx comes from the grid handle (the
|
|
896
|
-
* truth — §7-4). Callers: onScroll, resize tails, and ColumnHeader's layout effect.
|
|
897
|
-
*
|
|
898
|
-
*
|
|
1044
|
+
* truth — §7-4). Callers: onScroll, resize tails, and ColumnHeader's layout effect. v0.9
|
|
1045
|
+
* adds the equality-suppressed display heal of the two x header divider segments (§4.9
|
|
1046
|
+
* census #13 — NO geometry write, the exact twin of repaintHeaders).
|
|
1047
|
+
* 列 track の残差 transform を書く命令再描画 (§7-5 — 行 repaintHeaders の双子。v0.3 の
|
|
1048
|
+
* --aqsp-hx CSS 変数は退役)。hx はグリッドハンドル (§7-4 の真実) から読む。v0.9 で x ヘッダー
|
|
1049
|
+
* 分割線 2 セグメントの等値抑制 display heal を追加 (§4.9 census #13 — 幾何書込なし、
|
|
1050
|
+
* repaintHeaders の完全双子)。
|
|
899
1051
|
*/
|
|
900
1052
|
const repaintColHeaders = useCallback(() => {
|
|
901
1053
|
const track = colTrackRef.current
|
|
@@ -906,7 +1058,61 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
906
1058
|
const frozenWidth = handleRef.current?.getFrozenSize().width ?? 0
|
|
907
1059
|
track.style.transform = `translateX(${windowOriginXRef.current - hx - frozenWidth}px)`
|
|
908
1060
|
}
|
|
909
|
-
|
|
1061
|
+
// v0.9 (§4.9 census #13): x ヘッダー分割線の可視 heal (W_F / W_T の TREE px 駆動 — 行と同文。
|
|
1062
|
+
// 両セグメント未マウント (F = T = 0) は getFrozenSize 読みごと省く)
|
|
1063
|
+
const handle = handleRef.current
|
|
1064
|
+
if (handle !== null && (dividerXLeadingRef.current !== null || dividerXTrailingRef.current !== null)) {
|
|
1065
|
+
const frozen = handle.getFrozenSize()
|
|
1066
|
+
healHeaderDivider(dividerXLeadingRef.current, frozen.width > 0)
|
|
1067
|
+
healHeaderDivider(dividerXTrailingRef.current, frozen.trailingWidth > 0)
|
|
1068
|
+
}
|
|
1069
|
+
}, [healHeaderDivider])
|
|
1070
|
+
|
|
1071
|
+
// ---- 分割線可視の post-commit heal (v0.9 §2.4 / §2.7) ----
|
|
1072
|
+
const dividerHealRafRef = useRef<number | null>(null)
|
|
1073
|
+
/**
|
|
1074
|
+
* Schedules ONE post-commit divider-visibility heal after a track-resize seam (v0.9 §2.7 —
|
|
1075
|
+
* "T_px = 0 ⇒ no divider" must hold without an interposed scroll). The seam's synchronous
|
|
1076
|
+
* repaint reads the engine's render-captured getFrozenSize (a registered per-render mapping)
|
|
1077
|
+
* BEFORE the engine has re-rendered the new track size, so the divider display would
|
|
1078
|
+
* otherwise lag one geometry transition until the next write pass. One coalesced rAF
|
|
1079
|
+
* re-runs the three imperative writers from the committed getFrozenSize; the equality
|
|
1080
|
+
* suppression keeps this a zero-write pass unless a visibility flip is actually due, and
|
|
1081
|
+
* the F = T = 0 configuration schedules nothing (v0.8 identity — no reservation either).
|
|
1082
|
+
* トラックリサイズシーム後の分割線可視 post-commit heal を 1 回予約 (v0.9 §2.7 —
|
|
1083
|
+
* 「T_px = 0 ⇒ 分割線なし」はスクロール介在なしで成立が要件)。シームの同期 repaint は
|
|
1084
|
+
* エンジンの render 捕捉 getFrozenSize (登記済み毎レンダー写像) をエンジン再レンダー**前**に
|
|
1085
|
+
* 読むため、放置すると分割線 display が次の書込パスまで 1 遷移遅れる。合流 rAF 1 回で
|
|
1086
|
+
* commit 済み getFrozenSize から命令書き手 3 つを再実行 — 等値抑制によりフリップ不要なら
|
|
1087
|
+
* 無書込パス、F = T = 0 構成は予約ごと省く (v0.8 恒等)。
|
|
1088
|
+
*/
|
|
1089
|
+
const scheduleDividerHeal = useCallback(() => {
|
|
1090
|
+
// 分割線セグメントが 1 本も存在しない構成 (F = T = 0) は heal 対象なし — 予約も残さない
|
|
1091
|
+
if (dividerXLeadingRef.current === null && dividerXTrailingRef.current === null && dividerYLeadingRef.current === null && dividerYTrailingRef.current === null) {
|
|
1092
|
+
return
|
|
1093
|
+
}
|
|
1094
|
+
// 合流 (バッチリサイズの多発呼びでも 1 フレーム 1 heal)
|
|
1095
|
+
if (dividerHealRafRef.current !== null) {
|
|
1096
|
+
return
|
|
1097
|
+
}
|
|
1098
|
+
dividerHealRafRef.current = requestAnimationFrame(() => {
|
|
1099
|
+
dividerHealRafRef.current = null
|
|
1100
|
+
// ヘッダー 2 面 + overlay (胴体セグメントは layersFor が同パスで heal する) — commit 済み
|
|
1101
|
+
// getFrozenSize からの再実行。残差 transform / 幾何は同値再書込 (エンジンが既に確定済み)
|
|
1102
|
+
repaintHeaders()
|
|
1103
|
+
repaintColHeaders()
|
|
1104
|
+
repaintOverlay()
|
|
1105
|
+
})
|
|
1106
|
+
}, [repaintHeaders, repaintColHeaders, repaintOverlay])
|
|
1107
|
+
// アンマウント時に予約中の heal rAF を回収 (本パッケージの全タイマ / rAF 回収規律)
|
|
1108
|
+
useEffect(
|
|
1109
|
+
() => () => {
|
|
1110
|
+
if (dividerHealRafRef.current !== null) {
|
|
1111
|
+
cancelAnimationFrame(dividerHealRafRef.current)
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
[],
|
|
1115
|
+
)
|
|
910
1116
|
|
|
911
1117
|
// ensureCellVisible: §6.6.3 の自前 nearest (1 回構築して keyboard / Shift+F10 へ配布 — §6.9-7)。
|
|
912
1118
|
// 軸 identity が変わったら作り直す (クロージャが旧軸を掴む stale を防ぐ)
|
|
@@ -961,6 +1167,28 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
961
1167
|
[],
|
|
962
1168
|
)
|
|
963
1169
|
|
|
1170
|
+
/**
|
|
1171
|
+
* The single read-only refusal funnel (v0.10 plan §6.4 — the SSOT of RC-ANN / RC-B): every
|
|
1172
|
+
* gate (G1..G16) delivers its rejection here, which relays the callback AND the announcement
|
|
1173
|
+
* (`formatReadOnlyAnnouncement` → `announce()` — the debounce is inherited). Both props
|
|
1174
|
+
* omitted = silence = byte-identical; the read-only refusal never rides `onEditRejected` /
|
|
1175
|
+
* `onOperationRejected` (channel purity).
|
|
1176
|
+
* 読み取り専用拒否の単一漏斗 (v0.10 計画 §6.4 — RC-ANN / RC-B の SSOT)。全ゲート (G1..G16)
|
|
1177
|
+
* の拒否はここへ集まり、callback と読み上げ (`formatReadOnlyAnnouncement` → `announce()` —
|
|
1178
|
+
* デバウンス継承) の両方を運ぶ。両 prop 省略 = 無音 = バイト恒等。読み取り専用拒否を
|
|
1179
|
+
* `onEditRejected` / `onOperationRejected` へ流用しない (チャネル純度)。
|
|
1180
|
+
*/
|
|
1181
|
+
const rejectReadOnly = useCallback(
|
|
1182
|
+
(rejection: ReadOnlyRejection) => {
|
|
1183
|
+
onReadOnlyRejected?.(rejection)
|
|
1184
|
+
// 読み上げは prop 宣言時だけ (省略 = 無音 — 組み込み文言なしの家訓)
|
|
1185
|
+
if (formatReadOnlyAnnouncement !== undefined) {
|
|
1186
|
+
announce(formatReadOnlyAnnouncement(rejection))
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
[onReadOnlyRejected, formatReadOnlyAnnouncement, announce],
|
|
1190
|
+
)
|
|
1191
|
+
|
|
964
1192
|
/** The 3-tier announcement rule (§6.9-9, evaluated top-down). / 3 段機械判定 (上から順)。 */
|
|
965
1193
|
const evaluateAnnouncement = useCallback(
|
|
966
1194
|
(prev: SelectionState, next: SelectionState) => {
|
|
@@ -978,7 +1206,18 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
978
1206
|
const raw = adapter.getCellText(a.row, a.col)
|
|
979
1207
|
// displayText は目視と同一の整形後文字列 (塗り書式 = base ⊕ CF — getCellFormat 未宣言は raw と同値)
|
|
980
1208
|
const painted = adapter.getCellFormat === undefined ? undefined : paintedFormatOf(a.row, a.col, () => raw)
|
|
981
|
-
|
|
1209
|
+
// S2 (v0.10 §6.4): readOnly キーは宣言ゲート — isCellReadOnly 未宣言はキー自体を出さず
|
|
1210
|
+
// payload は v0.9 とバイト同一、宣言時は正負両真理のライブ回答 (RC-S2)
|
|
1211
|
+
announce(
|
|
1212
|
+
formatCellAnnouncement({
|
|
1213
|
+
row: a.row,
|
|
1214
|
+
col: a.col,
|
|
1215
|
+
text: raw,
|
|
1216
|
+
displayText: formatCellValue(raw, painted === undefined ? undefined : painted.value),
|
|
1217
|
+
columnName: columnIndexToName(a.col),
|
|
1218
|
+
...(readOnlyDeclared(adapter) ? { readOnly: isReadOnlyCell(adapter, a.row, a.col) } : {}),
|
|
1219
|
+
}),
|
|
1220
|
+
)
|
|
982
1221
|
}
|
|
983
1222
|
}
|
|
984
1223
|
// (1) 単一 1×1 レンジ → セル読み上げ (素のクリック / 矢印移動)
|
|
@@ -1092,14 +1331,37 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1092
1331
|
// OnTextInputTypeChanged で進行中の変換を強制終了する: 未編集 → IME 1 文字 → Enter が
|
|
1093
1332
|
// 落ちた利用者報告の根因 — セッション基準の切替は compositionstart の中で属性を変えていた)
|
|
1094
1333
|
const [virtualKeyboard, setVirtualKeyboard] = useState(false)
|
|
1334
|
+
/**
|
|
1335
|
+
* The ONE invitation-raising funnel (v0.10 plan §6.4 / RC-G2t): `setVirtualKeyboard(true)`
|
|
1336
|
+
* has exactly this production site, so the test seam's spy count ≡ raise executions by
|
|
1337
|
+
* construction — a React-batched raise-then-lower mutates no DOM yet still counts. The two
|
|
1338
|
+
* LOWERING sites (`onRootBlur`, the non-mouse root-capture pointerdown) call
|
|
1339
|
+
* `setVirtualKeyboard(false)` directly. The uniqueness invariant is machine-sealed by the
|
|
1340
|
+
* side-(iv) census of `readOnlyCensus.spec.ts` (a second raise site cannot land silently).
|
|
1341
|
+
* 招待 raise の単一漏斗 (v0.10 計画 §6.4 / RC-G2t)。`setVirtualKeyboard(true)` の production
|
|
1342
|
+
* 出現はここだけ — テストシームのスパイ計数 ≡ raise 実行が構成的に成立し、React バッチの
|
|
1343
|
+
* raise-then-lower も DOM 無変異のまま計数に映る。下ろし 2 サイト (`onRootBlur` / 非 mouse の
|
|
1344
|
+
* root capture pointerdown) は `setVirtualKeyboard(false)` を直呼びのまま。単一性の不変条件は
|
|
1345
|
+
* `readOnlyCensus.spec.ts` の側 (iv) census が機械封印 (第 2 raise サイトは無音で着地できない)。
|
|
1346
|
+
*/
|
|
1347
|
+
const raiseVirtualKeyboard = useCallback(() => {
|
|
1348
|
+
__onVirtualKeyboardRaise?.()
|
|
1349
|
+
setVirtualKeyboard(true)
|
|
1350
|
+
}, [__onVirtualKeyboardRaise])
|
|
1095
1351
|
const onRequestEdit = useCallback(
|
|
1096
|
-
(
|
|
1097
|
-
//
|
|
1098
|
-
//
|
|
1099
|
-
|
|
1352
|
+
(cell: CellCoord, caretIndex?: number) => {
|
|
1353
|
+
// G2 (v0.10 §6.4 / 設計 ADR-29 v3.1 — 招待前置が規範): raise は startEdit より先に走る
|
|
1354
|
+
// ため、読み取り専用セルでは招待自体をスキップする。G1 だけでは OSK が拒否より先に
|
|
1355
|
+
// 上がってしまう (キーボード無し端末のフラッシュ)。skip でも startEdit は呼ぶ —
|
|
1356
|
+
// 拒否発火サイトは G1 の 1 箇所 (SSOT)。能力未宣言は従来どおり無条件 raise (バイト恒等)
|
|
1357
|
+
if (!isReadOnlyCell(adapter, cell.row, cell.col)) {
|
|
1358
|
+
// ポインタで開くエディタだけが仮想キーボードを招待する (キーボード無し端末で開ける唯一の経路)。
|
|
1359
|
+
// 変換は進行中でない (dblclick 押下面が先に requestCommit 済み) — 属性変更は安全
|
|
1360
|
+
raiseVirtualKeyboard()
|
|
1361
|
+
}
|
|
1100
1362
|
startEdit("edit", caretIndex)
|
|
1101
1363
|
},
|
|
1102
|
-
[startEdit],
|
|
1364
|
+
[adapter, raiseVirtualKeyboard, startEdit],
|
|
1103
1365
|
)
|
|
1104
1366
|
const getClipRef = useRef<(() => ClipInfo | null) | null>(null)
|
|
1105
1367
|
const getClip = useCallback(() => getClipRef.current?.() ?? null, [])
|
|
@@ -1265,6 +1527,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1265
1527
|
viewStore.bumpColVersion()
|
|
1266
1528
|
repaintColHeaders()
|
|
1267
1529
|
repaintOverlay()
|
|
1530
|
+
// 同期 repaint はエンジン再レンダー前の stale getFrozenSize — 分割線可視は
|
|
1531
|
+
// post-commit heal が commit 済み値で整合させる (§2.7 — スクロール介在なし)
|
|
1532
|
+
scheduleDividerHeal()
|
|
1268
1533
|
return
|
|
1269
1534
|
}
|
|
1270
1535
|
for (const entry of entries) {
|
|
@@ -1276,8 +1541,10 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1276
1541
|
viewStore.bumpRowGeometry()
|
|
1277
1542
|
repaintHeaders()
|
|
1278
1543
|
repaintOverlay()
|
|
1544
|
+
// 行側も同文 (H_F / H_T の分割線可視は commit 済み値で heal — §2.7)
|
|
1545
|
+
scheduleDividerHeal()
|
|
1279
1546
|
},
|
|
1280
|
-
[rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay],
|
|
1547
|
+
[rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, scheduleDividerHeal],
|
|
1281
1548
|
)
|
|
1282
1549
|
|
|
1283
1550
|
/** Pointer contextmenu relay to onCellContextMenu (§6.4.3-4 sole channel). / pointer 起点の横流し。 */
|
|
@@ -1290,8 +1557,6 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1290
1557
|
|
|
1291
1558
|
// ---- オートスクローラー 3 種 (セル both / 列ヘッダー x / 行ヘッダー y — §6.4.4 / §6.8) ----
|
|
1292
1559
|
const cellAutoScroller = useAutoScroller({ handleRef, rowAxis, colAxis, bodyRef, onDragFrame, axis: "both", zoomRef })
|
|
1293
|
-
const colHeaderRef = useRef<HTMLDivElement | null>(null)
|
|
1294
|
-
const rowHeaderRef = useRef<HTMLDivElement | null>(null)
|
|
1295
1560
|
const colAutoScroller = useAutoScroller({ handleRef, rowAxis, colAxis, bodyRef, onDragFrame, axis: "x", zoomRef })
|
|
1296
1561
|
const rowAutoScroller = useAutoScroller({ handleRef, rowAxis, colAxis, bodyRef, onDragFrame, axis: "y", zoomRef })
|
|
1297
1562
|
// fill 専用の軸別 2 インスタンス (§8.2 — writer も axis も構築時固定。v0.3 のボックス方式では
|
|
@@ -1455,6 +1720,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1455
1720
|
commitEdits,
|
|
1456
1721
|
maxOperationCells,
|
|
1457
1722
|
onOperationRejected,
|
|
1723
|
+
// v0.10: G7 の拒否漏斗 (§6.4)
|
|
1724
|
+
rejectReadOnly,
|
|
1458
1725
|
writeFillPreview: onFillFrame,
|
|
1459
1726
|
repaintOverlay,
|
|
1460
1727
|
xAutoScroller: fillXAutoScroller,
|
|
@@ -1545,6 +1812,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1545
1812
|
rearmSink,
|
|
1546
1813
|
discardClip,
|
|
1547
1814
|
isDragging: isAnyDragging,
|
|
1815
|
+
// v0.10: G1 / G16 の拒否漏斗 (callback + 読み上げの SSOT — §6.4)
|
|
1816
|
+
rejectReadOnly,
|
|
1548
1817
|
// Excel 同様、編集中もセルの水平揃えを保つ (数値セルは右揃えのまま編集)。getCellFormat 未宣言は
|
|
1549
1818
|
// null = v0.1 恒等 (textarea の text-align を触らない)。宣言時は塗り書式 (base ⊕ CF) で判定し
|
|
1550
1819
|
// text は遅延 1 回読み (v0.8 §4.2.13)
|
|
@@ -1606,6 +1875,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1606
1875
|
onClipChange,
|
|
1607
1876
|
onClipboardUnavailable,
|
|
1608
1877
|
onEditRejected,
|
|
1878
|
+
// v0.10: G4 / G5 / G6a / G6b の拒否漏斗 (§6.4) + 貼り付け結果の要約 (§3.1)
|
|
1879
|
+
rejectReadOnly,
|
|
1880
|
+
onPasteResolved,
|
|
1609
1881
|
})
|
|
1610
1882
|
getClipRef.current = clipboard.getClip
|
|
1611
1883
|
discardClipRef.current = clipboard.discardClip
|
|
@@ -1635,25 +1907,31 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1635
1907
|
onHyperlinkOpen,
|
|
1636
1908
|
anchors: popoverAnchorsRef,
|
|
1637
1909
|
repaintOverlay,
|
|
1910
|
+
// v0.10: G14a / G14b / G14c の拒否漏斗 (§6.4)
|
|
1911
|
+
rejectReadOnly,
|
|
1638
1912
|
})
|
|
1639
1913
|
popoversRef.current = popovers
|
|
1640
1914
|
openPopoverRef.current = popovers.open
|
|
1641
1915
|
hideNoteTipRef.current = popovers.hideTip
|
|
1642
1916
|
|
|
1643
1917
|
// ---- コマンド実行 (v0.8 §4.1.13 / ADR-17) ----
|
|
1644
|
-
/** The active cell's viewport rect in visual px — the keyboard-origin cellRect recipe
|
|
1918
|
+
/** The active cell's viewport rect in visual px via the three-way forward map (v0.9 §4.9 census #11 — the keyboard-origin cellRect recipe, onCellContextMenu twin) for onPasteSpecialRequest. / active セルの視覚 px 矩形 — 3 分岐順写像 (v0.9 §4.9 census #11。keyboard 起点 cellRect のレシピ、onCellContextMenu と同式)。 */
|
|
1645
1919
|
const keyboardCellRect = useCallback(() => {
|
|
1646
1920
|
const active = selectionRef.current.active
|
|
1647
1921
|
// 画面外座標へメニューを出さない (§6.6.1) — 可視化してからハンドルの同期読み (§7-4)
|
|
1648
1922
|
ensureCellVisible(active)
|
|
1649
1923
|
const handle = handleRef.current
|
|
1650
1924
|
const position = handle === null ? { x: 0, y: 0 } : handle.getScrollPosition()
|
|
1651
|
-
|
|
1925
|
+
// 帯件数と extent も同じ判断で読む (v0.9 計画 §4.9、census #11 — AxisForwardCtx はこの場で
|
|
1926
|
+
// インライン構築、§2.1 規則 1)。detached ハンドルは件数 0 / extent 0 の縮退 (末尾分岐不活性 = v0.8 恒等)
|
|
1927
|
+
const frozen = handle === null ? { rows: 0, cols: 0, trailingRows: 0, trailingCols: 0 } : handle.getFrozenSize()
|
|
1928
|
+
const viewport = handle === null ? { width: 0, height: 0 } : handle.getViewportSize()
|
|
1652
1929
|
// 両軸とも番兵クランプ (detached の {-1, -1} を座標へ流さない)、ズーム境界で視覚 px へ
|
|
1653
1930
|
const z = zoomRef.current
|
|
1654
1931
|
return {
|
|
1655
|
-
|
|
1656
|
-
|
|
1932
|
+
// 行 / 列とも凍結パーティション 3 分岐順写像 (末尾は extent アンカー — v0.9 §2.1)。×zoom は写像後 (契約不変)
|
|
1933
|
+
top: rowToViewportY(rowAxis, active.row, { scroll: Math.max(0, position.y), frozenCount: frozen.rows, trailingCount: frozen.trailingRows, extent: viewport.height }) * z,
|
|
1934
|
+
left: colToViewportX(colAxis, active.col, { scroll: Math.max(0, position.x), frozenCount: frozen.cols, trailingCount: frozen.trailingCols, extent: viewport.width }) * z,
|
|
1657
1935
|
width: colAxis.sizeOf(active.col) * z,
|
|
1658
1936
|
height: rowAxis.sizeOf(active.row) * z,
|
|
1659
1937
|
}
|
|
@@ -1690,6 +1968,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1690
1968
|
announce,
|
|
1691
1969
|
formatCommandAnnouncement,
|
|
1692
1970
|
keyboardCellRect,
|
|
1971
|
+
// v0.10: G8–G13 / G15 の拒否漏斗 (§6.4)
|
|
1972
|
+
rejectReadOnly,
|
|
1693
1973
|
})
|
|
1694
1974
|
commandsRef.current = commands
|
|
1695
1975
|
executeCommandRef.current = commands.executeCommand
|
|
@@ -1724,6 +2004,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
1724
2004
|
commitEdits,
|
|
1725
2005
|
maxOperationCells,
|
|
1726
2006
|
onOperationRejected,
|
|
2007
|
+
// v0.10: G3 (Delete / Backspace) の拒否漏斗 (§6.4)
|
|
2008
|
+
rejectReadOnly,
|
|
1727
2009
|
getClip,
|
|
1728
2010
|
discardClip,
|
|
1729
2011
|
isDragging: isDraggingAny,
|
|
@@ -2072,12 +2354,18 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2072
2354
|
// 同じ stale 座標遮断だが、座標系は同一グリッドのため選択と編集セッションは保持する
|
|
2073
2355
|
const prevFrozenColsRef = useRef(frozenColumns)
|
|
2074
2356
|
const prevFrozenRowsRef = useRef(frozenRows)
|
|
2357
|
+
// v0.9 (§4.9 census #21 — FZ46 教訓): 末尾 2 props も同じ取消 OR に参加する (4 props の
|
|
2358
|
+
// どれかの変化 = 旧 3 分岐パーティションの押下時アンカーが全て陳腐化する)
|
|
2359
|
+
const prevTrailingColsRef = useRef(frozenTrailingColumns)
|
|
2360
|
+
const prevTrailingRowsRef = useRef(frozenTrailingRows)
|
|
2075
2361
|
useLayoutEffect(() => {
|
|
2076
|
-
// 列 /
|
|
2077
|
-
// 押下時アンカーは旧パーティション座標系。混成ヒットで誤 fill 確定を防ぐ)
|
|
2078
|
-
if (prevFrozenColsRef.current !== frozenColumns || prevFrozenRowsRef.current !== frozenRows) {
|
|
2362
|
+
// 列 / 行・先頭 / 末尾いずれの凍結変更も飛行中ドラッグを復元なしで取り消す (R2 是正の縦拡張 +
|
|
2363
|
+
// v0.9 の末尾拡張 — 押下時アンカーは旧パーティション座標系。混成ヒットで誤 fill 確定を防ぐ)
|
|
2364
|
+
if (prevFrozenColsRef.current !== frozenColumns || prevFrozenRowsRef.current !== frozenRows || prevTrailingColsRef.current !== frozenTrailingColumns || prevTrailingRowsRef.current !== frozenTrailingRows) {
|
|
2079
2365
|
prevFrozenColsRef.current = frozenColumns
|
|
2080
2366
|
prevFrozenRowsRef.current = frozenRows
|
|
2367
|
+
prevTrailingColsRef.current = frozenTrailingColumns
|
|
2368
|
+
prevTrailingRowsRef.current = frozenTrailingRows
|
|
2081
2369
|
engine.cancelDrag()
|
|
2082
2370
|
colHeaderMachine.cancelDrag()
|
|
2083
2371
|
rowHeaderMachine.cancelDrag()
|
|
@@ -2088,7 +2376,19 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2088
2376
|
// (cancelDrag は abortDrag と違いシンク断面を復元しない)
|
|
2089
2377
|
pointDrag.cancelDrag()
|
|
2090
2378
|
}
|
|
2091
|
-
}, [frozenColumns, frozenRows, engine, colHeaderMachine, rowHeaderMachine, headerResizeMachine, fillMachine, pointDrag])
|
|
2379
|
+
}, [frozenColumns, frozenRows, frozenTrailingColumns, frozenTrailingRows, engine, colHeaderMachine, rowHeaderMachine, headerResizeMachine, fillMachine, pointDrag])
|
|
2380
|
+
|
|
2381
|
+
// ---- 凍結 props フリップの分割線可視整合 (v0.9 §2.4 / §2.7) ----
|
|
2382
|
+
// フリップの同一 commit 内でエンジンは新 props で render 済み (毎レンダー写像の getFrozenSize は
|
|
2383
|
+
// layout effect 時点で確定値)。非表示トラックを含む帯へのフリップ (例: 全 track 幅 0 の列を F = 1 で
|
|
2384
|
+
// 凍結) では新規マウントの分割線セグメントが JSX 初期 display "" のまま px = 0 になるため、
|
|
2385
|
+
// paint 前にここで heal する — スクロール介在なしで「px = 0 ⇒ 分割線なし」を成立させる (§2.7)。
|
|
2386
|
+
// F = T = 0 では両ヘッダー書き手が getFrozenSize 読みごと省き、残差 transform の同値再書込のみ
|
|
2387
|
+
useLayoutEffect(() => {
|
|
2388
|
+
repaintHeaders()
|
|
2389
|
+
repaintColHeaders()
|
|
2390
|
+
repaintOverlay()
|
|
2391
|
+
}, [frozenColumns, frozenRows, frozenTrailingColumns, frozenTrailingRows, repaintHeaders, repaintColHeaders, repaintOverlay])
|
|
2092
2392
|
|
|
2093
2393
|
// ---- dataVersion (外部書込通知 §6.9-2 (b) — Undo / 協調編集) もクリップの破棄条件 ----
|
|
2094
2394
|
// adapter 外で値が書き戻ると cut 時行列のまま照合が通り、古い指紋の move が外部値を "" で
|
|
@@ -2129,6 +2429,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2129
2429
|
// §6.8-2 の順序契約の設計内猶予。自己修復するため実害なし)
|
|
2130
2430
|
repaintHeaders()
|
|
2131
2431
|
repaintOverlay()
|
|
2432
|
+
// 分割線可視は commit 済み getFrozenSize の post-commit heal で整合 (§2.7)
|
|
2433
|
+
scheduleDividerHeal()
|
|
2132
2434
|
},
|
|
2133
2435
|
resizeColumn: (col: number, widthPx: number) => {
|
|
2134
2436
|
// 対呼び出し (§3.8): colAxis (SSOT) を先に更新し、グリッドの幅木へ通知する。
|
|
@@ -2141,6 +2443,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2141
2443
|
viewStore.bumpColVersion()
|
|
2142
2444
|
repaintColHeaders()
|
|
2143
2445
|
repaintOverlay()
|
|
2446
|
+
// 分割線可視は commit 済み getFrozenSize の post-commit heal で整合 (§2.7)
|
|
2447
|
+
scheduleDividerHeal()
|
|
2144
2448
|
},
|
|
2145
2449
|
scrollToCell: (cell: CellCoord, options?: ScrollToCellOptions) => {
|
|
2146
2450
|
// エンジンのセル狙いジャンプへ素通し (§7-4 の真実。選択は変えない — 純 viewport 移動)。
|
|
@@ -2175,7 +2479,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2175
2479
|
openPopoverRef.current(kind)
|
|
2176
2480
|
},
|
|
2177
2481
|
}),
|
|
2178
|
-
[rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, focusSink, isEditing, rearmSink, adapter, paintedFormatOf, startEdit, requireCommands],
|
|
2482
|
+
[rowAxis, colAxis, viewStore, repaintColHeaders, repaintHeaders, repaintOverlay, scheduleDividerHeal, focusSink, isEditing, rearmSink, adapter, paintedFormatOf, startEdit, requireCommands],
|
|
2179
2483
|
)
|
|
2180
2484
|
|
|
2181
2485
|
// ---- 空グリッド分岐 (§6.9-1: フックの後・結線の前) ----
|
|
@@ -2184,7 +2488,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2184
2488
|
}
|
|
2185
2489
|
|
|
2186
2490
|
// live region は読み上げ prop のどれか 1 つでも宣言されたときだけ描く (コマンド通知も同じ領域へ)
|
|
2187
|
-
|
|
2491
|
+
// v0.10: formatReadOnlyAnnouncement も live region の存在条件 (拒否読み上げは announce() 経由 — §6.4)
|
|
2492
|
+
const hasAnnouncement = formatCellAnnouncement !== undefined || formatSelectionAnnouncement !== undefined || formatCommandAnnouncement !== undefined || formatReadOnlyAnnouncement !== undefined
|
|
2188
2493
|
|
|
2189
2494
|
return (
|
|
2190
2495
|
// biome-ignore lint/a11y/useSemanticElements: fieldset はフォーム意味論であり不適。role="group" は Escape 脱出の着地点に現在地名を与えるための選択 (§6.9-7 / §10-4)
|
|
@@ -2231,6 +2536,7 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2231
2536
|
viewStore={viewStore}
|
|
2232
2537
|
colAxis={colAxis}
|
|
2233
2538
|
frozenCols={frozenColumns}
|
|
2539
|
+
frozenTrailingCols={frozenTrailingColumns}
|
|
2234
2540
|
handleRef={handleRef}
|
|
2235
2541
|
bandRef={colHeaderRef}
|
|
2236
2542
|
colTrackRef={colTrackRef}
|
|
@@ -2238,6 +2544,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2238
2544
|
repaintColHeaders={repaintColHeaders}
|
|
2239
2545
|
selectionRanges={selection.ranges}
|
|
2240
2546
|
enableResizeUI={enableResizeUI}
|
|
2547
|
+
dividerLeadingRef={dividerXLeadingRef}
|
|
2548
|
+
dividerTrailingRef={dividerXTrailingRef}
|
|
2241
2549
|
/>
|
|
2242
2550
|
<RowHeader
|
|
2243
2551
|
viewStore={viewStore}
|
|
@@ -2250,6 +2558,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2250
2558
|
repaintHeaders={repaintHeaders}
|
|
2251
2559
|
enableResizeUI={enableResizeUI}
|
|
2252
2560
|
frozenRows={frozenRows}
|
|
2561
|
+
frozenTrailingRows={frozenTrailingRows}
|
|
2562
|
+
dividerLeadingRef={dividerYLeadingRef}
|
|
2563
|
+
dividerTrailingRef={dividerYTrailingRef}
|
|
2253
2564
|
/>
|
|
2254
2565
|
<div className="aqsp-body-stack" data-aqsp-body-stack="">
|
|
2255
2566
|
<GridBody
|
|
@@ -2263,6 +2574,9 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2263
2574
|
defaultColumnWidth={defaultColumnWidth}
|
|
2264
2575
|
frozenColumns={frozenColumns}
|
|
2265
2576
|
frozenRows={frozenRows}
|
|
2577
|
+
frozenTrailingColumns={frozenTrailingColumns}
|
|
2578
|
+
frozenTrailingRows={frozenTrailingRows}
|
|
2579
|
+
tapScrollCircleOptions={tapScrollCircleOptions}
|
|
2266
2580
|
renderCell={renderCell}
|
|
2267
2581
|
onScroll={onScroll}
|
|
2268
2582
|
onRangeChange={onRangeChange}
|
|
@@ -2294,6 +2608,8 @@ export const Spreadsheet = forwardRef<SpreadsheetHandle, SpreadsheetProps & Spre
|
|
|
2294
2608
|
enableFillHandle={enableFillHandle}
|
|
2295
2609
|
frozenCols={frozenColumns}
|
|
2296
2610
|
frozenRows={frozenRows}
|
|
2611
|
+
trailingCols={frozenTrailingColumns}
|
|
2612
|
+
trailingRows={frozenTrailingRows}
|
|
2297
2613
|
sinkAriaLabel={resolvedLabels.sinkAriaLabel}
|
|
2298
2614
|
onSinkMouseDown={onSinkMouseDown}
|
|
2299
2615
|
editPaintRef={editPaintRef}
|