@aiquants/virtualscroll 3.4.0 → 3.5.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 +39 -0
- package/README.md +23 -3
- package/dist/TapScrollCircle.d.cts +1 -1
- package/dist/TapScrollCircle.d.ts +1 -1
- package/dist/TapScrollCircle.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +105 -3
- package/dist/VirtualGrid.d.ts +105 -3
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2067 -1984
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/package.json +1 -1
- package/src/TapScrollCircle.tsx +1 -1
- package/src/VirtualGrid.tsx +408 -72
- package/src/index.ts +14 -1
- package/src/styles/virtualscroll.css +59 -1
package/src/VirtualGrid.tsx
CHANGED
|
@@ -89,6 +89,36 @@ export const MAX_FROZEN_LEADING_COLS = 128
|
|
|
89
89
|
*/
|
|
90
90
|
export const MAX_FROZEN_LEADING_ROWS = 128
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Hard cap for `frozenTrailingCols` (v3.5.0 trailing-freeze plan §3). The trailing band is NOT
|
|
94
|
+
* windowed — every trailing column materializes in every rendered row and is EXEMPT from the
|
|
95
|
+
* MAX_RENDERED_CELLS truncation (the scroll-band budget is reduced by the trailing count
|
|
96
|
+
* instead) — so the cap is what bounds the band: worst case 128 x MAX_RENDERED_ITEMS rows, and
|
|
97
|
+
* W_T <= 128 x MAX_TRACK_SIZE = 2^25 (the measured layout wall; practical W_T <= viewport <<
|
|
98
|
+
* 2^24). Violations fail fast with a RangeError.
|
|
99
|
+
*
|
|
100
|
+
* `frozenTrailingCols` の上限 (3.5.0 末尾凍結プラン §3)。末尾帯は窓化されず全描画行で具現化し、
|
|
101
|
+
* かつ MAX_RENDERED_CELLS の打切り対象外 (代わりにスクロール帯予算を末尾数ぶん控除) のため、
|
|
102
|
+
* 帯を有界にするのはこの上限そのもの: 最悪 128 × MAX_RENDERED_ITEMS 行、W_T ≤ 128 ×
|
|
103
|
+
* MAX_TRACK_SIZE = 2^25 (実測壁ちょうど — 実運用は W_T ≤ viewport ≪ 2^24)。違反は fail-fast の
|
|
104
|
+
* RangeError。
|
|
105
|
+
*/
|
|
106
|
+
export const MAX_FROZEN_TRAILING_COLS = 128
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Hard cap for `frozenTrailingRows` (v3.5.0 trailing-freeze plan §3). The trailing-row band is
|
|
110
|
+
* grid-owned and OUT of the embedded VirtualScroll (the SECOND out-of-pane band, below the
|
|
111
|
+
* pane): every band row materializes every rendered column and is exempt from
|
|
112
|
+
* MAX_RENDERED_CELLS (the budget denominator counts band rows instead), so the cap bounds the
|
|
113
|
+
* band itself. Violations fail fast with a RangeError.
|
|
114
|
+
*
|
|
115
|
+
* `frozenTrailingRows` の上限 (3.5.0 末尾凍結プラン §3)。末尾行帯はグリッド所有で埋め込み
|
|
116
|
+
* VirtualScroll の外 (ペインの**下**の第 2 帯外バンド)。帯行は描画列を全て具現化し
|
|
117
|
+
* MAX_RENDERED_CELLS の打切り対象外 (予算分母に帯行数を算入) のため、帯を有界にするのは
|
|
118
|
+
* この上限そのもの。違反は fail-fast の RangeError。
|
|
119
|
+
*/
|
|
120
|
+
export const MAX_FROZEN_TRAILING_ROWS = 128
|
|
121
|
+
|
|
92
122
|
/**
|
|
93
123
|
* Maximum number of CONSECUTIVE NON-CONVERGENT self-heal flushes. A stable accessor converges in
|
|
94
124
|
* ONE flush (tree == accessor afterwards), so the next collect pass is clean and RESETS this
|
|
@@ -303,6 +333,40 @@ export type VirtualGridProps<T> = {
|
|
|
303
333
|
* 通知しゼロ描画)。
|
|
304
334
|
*/
|
|
305
335
|
frozenLeadingRows?: number
|
|
336
|
+
/**
|
|
337
|
+
* Number of trailing columns frozen at the right edge (v3.5.0 trailing-freeze plan §3;
|
|
338
|
+
* default 0). Integer in [0, MAX_FROZEN_TRAILING_COLS] — RangeError otherwise (fail-fast).
|
|
339
|
+
* Dynamic clamp (documented contract): min(T, colCount − effectiveFrozenCols) — the LEADING
|
|
340
|
+
* band wins the count space. Trailing cells render in a right-anchored clip outside the
|
|
341
|
+
* anchor machinery (band-local lefts); `--aqvs-grid-trailing-width` is written iff T > 0.
|
|
342
|
+
* `scrollToCell` / `initialScrollAnchor` targeting a trailing column are horizontal no-ops
|
|
343
|
+
* (always visible). T = 0 is structurally identical to the pre-trailing DOM.
|
|
344
|
+
* 右端に凍結する末尾列数 (3.5.0 末尾凍結プラン §3。既定 0)。[0, MAX_FROZEN_TRAILING_COLS]
|
|
345
|
+
* の整数 — 違反は RangeError (fail-fast)。動的クランプ (文書化契約): min(T, colCount −
|
|
346
|
+
* effectiveFrozenCols) — カウント空間は**先頭が勝つ**。末尾セルはアンカー機構の外の
|
|
347
|
+
* 右アンカークリップに帯ローカル left で描画し、`--aqvs-grid-trailing-width` は T > 0 の
|
|
348
|
+
* ときのみ書かれる。`scrollToCell` / `initialScrollAnchor` の末尾列狙いは横 no-op
|
|
349
|
+
* (常時可視)。T = 0 は DOM 構造まで従来と恒等。
|
|
350
|
+
*/
|
|
351
|
+
frozenTrailingCols?: number
|
|
352
|
+
/**
|
|
353
|
+
* Number of trailing rows pinned at the bottom (v3.5.0 trailing-freeze plan §3; default 0).
|
|
354
|
+
* Integer in [0, MAX_FROZEN_TRAILING_ROWS] — RangeError otherwise (fail-fast). Dynamic
|
|
355
|
+
* clamp (documented contract): min(T, rowCount − effectiveFrozenRows) — leading wins. The
|
|
356
|
+
* band is grid-owned BELOW the pane; the embedded scroll rows shrink at the TAIL only
|
|
357
|
+
* (itemCount = rowCount − R − T, getItem = i + R unchanged), so a runtime T change does NOT
|
|
358
|
+
* remount the embedded VirtualScroll (ADR-23). `scrollToCell` / `initialScrollAnchor`
|
|
359
|
+
* targeting a trailing row are vertical no-ops (always visible). T = 0 is structurally
|
|
360
|
+
* identical to the pre-trailing DOM.
|
|
361
|
+
* 下端に凍結する末尾行数 (3.5.0 末尾凍結プラン §3。既定 0)。[0, MAX_FROZEN_TRAILING_ROWS]
|
|
362
|
+
* の整数 — 違反は RangeError (fail-fast)。動的クランプ (文書化契約): min(T, rowCount −
|
|
363
|
+
* effectiveFrozenRows) — 先頭が勝つ。帯はペインの**下**のグリッド所有バンドで、埋め込み
|
|
364
|
+
* スクロール行は**末尾**だけが伸縮する (itemCount = rowCount − R − T、getItem = i + R
|
|
365
|
+
* 不変) — T の実行時変更は埋め込み VirtualScroll を再マウントしない (ADR-23)。
|
|
366
|
+
* `scrollToCell` / `initialScrollAnchor` の末尾行狙いは縦 no-op (常時可視)。T = 0 は
|
|
367
|
+
* DOM 構造まで従来と恒等。
|
|
368
|
+
*/
|
|
369
|
+
frozenTrailingRows?: number
|
|
306
370
|
/** Row-axis overscan (default 3 — a declared, intentional deviation from the bare VirtualScroll default 15; see the bounding contract). / 行軸 overscan (既定 3 — 素の既定 15 からの宣言済み意図的乖離)。 */
|
|
307
371
|
overscanRows?: number
|
|
308
372
|
/** Column-axis overscan (default 3). / 列軸 overscan (既定 3)。 */
|
|
@@ -338,7 +402,24 @@ export type VirtualGridHandle = {
|
|
|
338
402
|
scrollTo(pos: { x?: number; y?: number }): { x: number; y: number }
|
|
339
403
|
/** Relative float-accumulating path (wheel semantics; returns the applied clamped position — drag loops consume it synchronously). / 相対 float 累積経路 (ホイール同義。クランプ後位置を同期 return — ドラッグループが消費)。 */
|
|
340
404
|
scrollBy(delta: { x?: number; y?: number }): { x: number; y: number }
|
|
341
|
-
/**
|
|
405
|
+
/**
|
|
406
|
+
* Cell-targeted jump. alignY uses the row vocabulary (top/bottom/center), alignX its
|
|
407
|
+
* horizontal twin (start/end/center). No "nearest". Targeting a LEADING frozen or TRAILING
|
|
408
|
+
* frozen column is a horizontal no-op that leaves any previously armed column anchor in
|
|
409
|
+
* place (the visual status quo is preserved; manual horizontal input clears anchors as
|
|
410
|
+
* usual); targeting a leading/trailing frozen row is a vertical no-op. Band membership is
|
|
411
|
+
* judged at CALL time — a T change AFTER arming follows the ADR-23 clamp semantics: a
|
|
412
|
+
* pending row anchor whose target row lands inside the trailing band after a T increase
|
|
413
|
+
* clamps to the last scroll row. Overlap registration (ADR-19-1): when the leading +
|
|
414
|
+
* trailing band extents exceed the viewport, occluded trailing cells cannot be scrolled
|
|
415
|
+
* into view — the no-op is exact there too.
|
|
416
|
+
* セル狙いジャンプ。"nearest" は無し。先頭 / 末尾凍結列狙いの横成分は no-op で、既存の
|
|
417
|
+
* 列アンカーはそのまま残る (視覚的現状維持 — 手動横入力が従来どおり解除する)。先頭 /
|
|
418
|
+
* 末尾凍結行狙いの縦成分も no-op。帯所属の判定は**呼出し時** — 張った後の T 変更は
|
|
419
|
+
* ADR-23 のクランプ意味論 (T 増加で末尾帯入りする行を狙った保留行アンカーは最終
|
|
420
|
+
* スクロール行へクランプ) に従う。重複登記 (ADR-19-1): 先頭 + 末尾の帯寸がビューポートを
|
|
421
|
+
* 超えるとき、隠れた末尾セルはどのスクロール位置でも可視化できない — no-op はそこでも正確。
|
|
422
|
+
*/
|
|
342
423
|
scrollToCell(row: number, col: number, options?: { alignY?: "top" | "bottom" | "center"; alignX?: "start" | "end" | "center"; offsetX?: number; offsetY?: number }): void
|
|
343
424
|
/** Synchronous-fresh position read (internal refs — safe mid-event; {-1,-1} pre-attach). / 同期・最新の位置読み (内部 ref — イベント中も安全。未接続 {-1,-1})。 */
|
|
344
425
|
getScrollPosition(): { x: number; y: number }
|
|
@@ -347,7 +428,7 @@ export type VirtualGridHandle = {
|
|
|
347
428
|
getViewportSize(): { width: number; height: number }
|
|
348
429
|
getContentSize(): { width: number; height: number }
|
|
349
430
|
getRange(): VirtualGridRange | null
|
|
350
|
-
/** Pair-call seam: getRowHeight stays the truth;
|
|
431
|
+
/** Pair-call seam: getRowHeight stays the truth. 3-way split — leading band rows bump the frozen-row epoch, trailing band rows bump the trailing-row epoch (accessor re-read; the bands hold no tree), scroll rows delegate to the row updateItemSize at −R. / 対呼び出しシーム (getRowHeight が正)。3 分岐 — 先頭帯行は凍結行 epoch、末尾帯行は末尾行 epoch の繰上げ (アクセサ再読 — 帯に木は無い)、スクロール行は −R で行 updateItemSize へ委譲。 */
|
|
351
432
|
updateRowSize(row: number, px: number): void
|
|
352
433
|
/**
|
|
353
434
|
* Column twin: validates (MAX_TRACK_SIZE fail-fast), updates the width tree, and bumps the
|
|
@@ -360,8 +441,25 @@ export type VirtualGridHandle = {
|
|
|
360
441
|
* バッチングで 1 コミットに合流 (設計の rAF 合流意図のコミット粒度での実現)。
|
|
361
442
|
*/
|
|
362
443
|
updateColSize(col: number, px: number): void
|
|
363
|
-
/**
|
|
364
|
-
|
|
444
|
+
/**
|
|
445
|
+
* Effective frozen-band sizes on both axes and both ends (v3.5.0 — a non-breaking field
|
|
446
|
+
* extension of the {cols, width} → {cols, width, rows, height} precedent). cols/rows/
|
|
447
|
+
* trailingCols/trailingRows = dynamic post-clamp counts; width/height/trailingWidth/
|
|
448
|
+
* trailingHeight = TREE px (the trailing sizes are tree SUFFIX sums); trailingVisibleWidth/
|
|
449
|
+
* trailingVisibleHeight = viewport-clipped `min(tree, max(0, viewport − leading))` — THE
|
|
450
|
+
* single source for W_T_vis / H_T_vis that every consumer judgment/write path cites (never
|
|
451
|
+
* re-derive the formula). Pre-attach the grid viewport reads {0, 0}, so the vis fields are
|
|
452
|
+
* 0 (an initial measured state, not a sentinel) while the tree fields stay tree px.
|
|
453
|
+
* 実効凍結帯寸法 — 両軸・両端 (v3.5.0。{cols, width} → {cols, width, rows, height} の
|
|
454
|
+
* 前例に続く非破壊のフィールド追加)。cols / rows / trailingCols / trailingRows は動的
|
|
455
|
+
* クランプ後の実効カウント、width / height / trailingWidth / trailingHeight は**木** px
|
|
456
|
+
* (末尾寸は木の接尾辞和)、trailingVisibleWidth / trailingVisibleHeight はビューポート
|
|
457
|
+
* クリップ `min(木, max(0, viewport − 先頭))` — W_T_vis / H_T_vis の**単一情報源**で、
|
|
458
|
+
* 消費側の判定 / 書込み経路は全てこのフィールドを名指しで読む (式の再導出禁止)。
|
|
459
|
+
* プレアタッチはビューポート {0, 0} のため vis フィールドは 0 (番兵ではなく初期実測状態)、
|
|
460
|
+
* 木フィールドは常に木 px。
|
|
461
|
+
*/
|
|
462
|
+
getFrozenSize(): { cols: number; width: number; rows: number; height: number; trailingCols: number; trailingRows: number; trailingWidth: number; trailingHeight: number; trailingVisibleWidth: number; trailingVisibleHeight: number }
|
|
365
463
|
/** 2-axis wheel bridge entry: vertical → embedded VirtualScroll, horizontal → hx. Returns whether the event was consumed (WheelBridgeTarget contract). / 2 軸ホイールブリッジ口。消費有無を返す (ブリッジ契約)。 */
|
|
366
464
|
applyWheel(event: WheelEvent): boolean
|
|
367
465
|
/** Focuses the row wrapper at `row` (scroll rows via the −R-shifted row-side focusItemAtIndex — enableKeyboardNavigation applies; frozen band rows are focused directly and need no option). / 行ラッパーへフォーカス (スクロール行は −R シフトの行側 focusItemAtIndex — enableKeyboardNavigation が前提。凍結帯行は直接フォーカスでオプション不要)。 */
|
|
@@ -525,6 +623,58 @@ const collectFrozenColumns = (frozenCols: number, getColWidth: (col: number) =>
|
|
|
525
623
|
return { columns, widthUpdates, width: runningLeft }
|
|
526
624
|
}
|
|
527
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Enumerates the trailing frozen band (v3.5.0 trailing-freeze plan §6.1-5 — the suffix twin of
|
|
628
|
+
* collectFrozenColumns): columns [colCount − trailingCols, colCount) with BAND-LOCAL lefts (a
|
|
629
|
+
* running prefix that starts at 0 — the right-anchored inner owns the placement, so trailing
|
|
630
|
+
* cells never take the anchor subtraction; W_T is bounded by 128 x MAX_TRACK_SIZE = 2^25),
|
|
631
|
+
* zero-width columns skipped (no DOM — the shared hidden-track discipline), and width
|
|
632
|
+
* self-heal deltas collected under the same tree-is-position / accessor-is-width split as
|
|
633
|
+
* collectVisibleColumns. Returns width = W_T, the TREE suffix width of the band.
|
|
634
|
+
*
|
|
635
|
+
* 末尾凍結帯の列挙 (3.5.0 末尾凍結プラン §6.1-5 — collectFrozenColumns の接尾辞双子)。列
|
|
636
|
+
* [colCount − trailingCols, colCount) を**帯ローカル** left (0 始まりの running 接頭辞 —
|
|
637
|
+
* 配置は右アンカー inner が担い、末尾セルはアンカー減算を決して取らない。W_T は 128 × 2^18 =
|
|
638
|
+
* 2^25 で有界) で並べ、幅 0 列は DOM 非生成 (非表示トラックの共有規律)、幅 self-heal 差分は
|
|
639
|
+
* collectVisibleColumns と同じ「位置は木・幅はアクセサ」分担で収集する。返す width = W_T
|
|
640
|
+
* (帯の**木**接尾辞幅)。
|
|
641
|
+
*/
|
|
642
|
+
const collectTrailingColumns = (colCount: number, trailingCols: number, getColWidth: (col: number) => number, colTree: ReturnType<typeof useFenwickMapTree>): { columns: PlacedColumn[]; widthUpdates: Array<{ index: number; value: number }>; width: number } => {
|
|
643
|
+
const columns: PlacedColumn[] = []
|
|
644
|
+
const widthUpdates: Array<{ index: number; value: number }> = []
|
|
645
|
+
let runningLeft = 0
|
|
646
|
+
// T ≤ MAX_FROZEN_TRAILING_COLS のため素朴な O(T) 走査で十分 (幅 0 ジャンプ機構は不要)
|
|
647
|
+
for (let col = colCount - trailingCols; col < colCount; col += 1) {
|
|
648
|
+
const width = getColWidth(col)
|
|
649
|
+
const cached = colTree.get(col)
|
|
650
|
+
if (width !== cached) {
|
|
651
|
+
widthUpdates.push({ index: col, value: width })
|
|
652
|
+
}
|
|
653
|
+
if (width > 0) {
|
|
654
|
+
columns.push({ col, left: runningLeft, width })
|
|
655
|
+
}
|
|
656
|
+
runningLeft += cached
|
|
657
|
+
}
|
|
658
|
+
return { columns, widthUpdates, width: runningLeft }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* THE single JS source for W_T_vis / H_T_vis (v3.5.0 trailing-freeze plan §2.1-4): clips a
|
|
663
|
+
* trailing band's TREE extent to what the viewport can show after the leading band took its
|
|
664
|
+
* share — `min(trailing, max(0, viewport − leading))`. Consumers are exactly three: the
|
|
665
|
+
* trailing-rows band template height, the hbar paddingRight, and the getFrozenSize
|
|
666
|
+
* trailingVisibleWidth/Height export. Never re-derive the formula elsewhere (the CSS max()
|
|
667
|
+
* left edge of `.aqvs-grid-row-trailing` is the registered calc() equivalent — the ONLY
|
|
668
|
+
* exception, annotated in the stylesheet).
|
|
669
|
+
*
|
|
670
|
+
* W_T_vis / H_T_vis の**単一 JS 源** (3.5.0 末尾凍結プラン §2.1-4)。末尾帯の**木**寸を
|
|
671
|
+
* 「先頭帯が取った残り」へクリップする `min(末尾, max(0, viewport − 先頭))`。消費先は
|
|
672
|
+
* 末尾行帯テンプレート高 / hbar paddingRight / getFrozenSize の vis export の 3 点ちょうど —
|
|
673
|
+
* 式を他所へ複製しない (`.aqvs-grid-row-trailing` の CSS max() 左端だけが登記済みの calc()
|
|
674
|
+
* 等価形で、スタイルシート側にインライン登記済み)。
|
|
675
|
+
*/
|
|
676
|
+
const trailingVisibleSize = (trailingSize: number, frozenSize: number, viewportSize: number): number => Math.min(trailingSize, Math.max(0, viewportSize - frozenSize))
|
|
677
|
+
|
|
528
678
|
/** Internal committed column-window state (anchor rides with the window — the M10 coherence unit). / コミット済み列窓状態 (アンカーは窓と一体 — M10 整合の単位)。 */
|
|
529
679
|
type ColumnWindowState = {
|
|
530
680
|
renderingColStart: number
|
|
@@ -575,6 +725,8 @@ const VirtualGridInner = <T,>(
|
|
|
575
725
|
onRowFocus,
|
|
576
726
|
frozenLeadingCols = 0,
|
|
577
727
|
frozenLeadingRows = 0,
|
|
728
|
+
frozenTrailingCols = 0,
|
|
729
|
+
frozenTrailingRows = 0,
|
|
578
730
|
horizontalKeyInputs,
|
|
579
731
|
horizontalKeyStep,
|
|
580
732
|
onScroll,
|
|
@@ -650,17 +802,34 @@ const VirtualGridInner = <T,>(
|
|
|
650
802
|
/** Fresh W_F for drag loops / clamps (mirrored from the frozen memo each render). / ドラッグループ / クランプ用の鮮度 W_F (凍結 memo から毎レンダー写像)。 */
|
|
651
803
|
const frozenWidthRef = useRef(0)
|
|
652
804
|
|
|
805
|
+
// ---- 末尾凍結列帯 (3.5.0 プラン §6.1-3) — 先頭側と同文の fail-fast。動的クランプは
|
|
806
|
+
// min(T, colCount − 先頭実効) で**先頭がカウント空間で勝つ** (ADR-19-1 の文書化契約) ----
|
|
807
|
+
if (!(Number.isInteger(frozenTrailingCols) && frozenTrailingCols >= 0 && frozenTrailingCols <= MAX_FROZEN_TRAILING_COLS)) {
|
|
808
|
+
throw new RangeError(`[VirtualGrid] frozenTrailingCols must be an integer in [0, ${MAX_FROZEN_TRAILING_COLS}], received ${frozenTrailingCols}.`)
|
|
809
|
+
}
|
|
810
|
+
const effectiveTrailingCols = Math.min(frozenTrailingCols, colCount - effectiveFrozenCols)
|
|
811
|
+
/** Fresh W_T for drag loops / clamps (mirrored from the trailing memo each render). / ドラッグループ / クランプ用の鮮度 W_T (末尾 memo から毎レンダー写像)。 */
|
|
812
|
+
const trailingWidthRef = useRef(0)
|
|
813
|
+
|
|
653
814
|
// ---- 凍結行帯 (行凍結設計 §4-1) — 列と同文の fail-fast + 動的クランプ ----
|
|
654
815
|
if (!(Number.isInteger(frozenLeadingRows) && frozenLeadingRows >= 0 && frozenLeadingRows <= MAX_FROZEN_LEADING_ROWS)) {
|
|
655
816
|
throw new RangeError(`[VirtualGrid] frozenLeadingRows must be an integer in [0, ${MAX_FROZEN_LEADING_ROWS}], received ${frozenLeadingRows}.`)
|
|
656
817
|
}
|
|
657
818
|
const effectiveFrozenRows = Math.min(frozenLeadingRows, rowCount)
|
|
658
|
-
|
|
659
|
-
|
|
819
|
+
|
|
820
|
+
// ---- 末尾凍結行帯 (3.5.0 プラン §6.1-3) — 列側と同文の fail-fast + 先頭勝ちクランプ ----
|
|
821
|
+
if (!(Number.isInteger(frozenTrailingRows) && frozenTrailingRows >= 0 && frozenTrailingRows <= MAX_FROZEN_TRAILING_ROWS)) {
|
|
822
|
+
throw new RangeError(`[VirtualGrid] frozenTrailingRows must be an integer in [0, ${MAX_FROZEN_TRAILING_ROWS}], received ${frozenTrailingRows}.`)
|
|
823
|
+
}
|
|
824
|
+
const effectiveTrailingRows = Math.min(frozenTrailingRows, rowCount - effectiveFrozenRows)
|
|
825
|
+
/** Scroll-row count in the SHIFTED space (design §2; the tail loses T — the embedded VirtualScroll's itemCount). / シフト空間のスクロール行数 (末尾は −T — 埋め込み VirtualScroll の itemCount)。 */
|
|
826
|
+
const scrollRowCount = Math.max(0, rowCount - effectiveFrozenRows - effectiveTrailingRows)
|
|
660
827
|
/** Degenerate-band flag ref (H_F >= measured viewport — §3.3 の縮退帯と同じ正準空窓へ). / 縮退帯フラグ ref (H_F ≥ 実測ビューポート — §3.3 縮退帯と同じ正準空窓)。 */
|
|
661
828
|
const degenerateBandRef = useRef(false)
|
|
662
829
|
/** Fresh H_F for handle reads (mirrored from the band memo each render). / ハンドル読み用の鮮度 H_F (帯 memo から毎レンダー写像)。 */
|
|
663
830
|
const frozenHeightRef = useRef(0)
|
|
831
|
+
/** Fresh H_T for handle reads (mirrored from the trailing band memo each render). / ハンドル読み用の鮮度 H_T (末尾帯 memo から毎レンダー写像)。 */
|
|
832
|
+
const trailingHeightRef = useRef(0)
|
|
664
833
|
|
|
665
834
|
// ---- 凍結行帯の列挙 (行凍結設計 §4-2) — グリッド所有の帯外バンド。行高の真実は
|
|
666
835
|
// アクセサ直読 (列帯と違い第 2 の格納庫 = 木 が無いためヒール機構は不要 — 更新経路は
|
|
@@ -683,6 +852,28 @@ const VirtualGridInner = <T,>(
|
|
|
683
852
|
return { frozenRows: rows, frozenHeight: running }
|
|
684
853
|
}, [effectiveFrozenRows, frozenRowEpoch, validatedGetRowHeight])
|
|
685
854
|
frozenHeightRef.current = frozenHeight
|
|
855
|
+
|
|
856
|
+
// ---- 末尾行帯の列挙 (3.5.0 プラン §6.1-11) — 先頭行帯と同形の帯外バンド。木もヒール機構も
|
|
857
|
+
// 持たない (アクセサが唯一の真実 — 先頭行帯の宣言済み非対称と同文)。top は帯ローカル
|
|
858
|
+
// 接頭辞和 (inner の bottom 定着が右アンカーの行版を担う)。高さ 0 行は DOM 非生成 ----
|
|
859
|
+
const [trailingRowEpoch, setTrailingRowEpoch] = useState(0)
|
|
860
|
+
const { trailingRows, trailingHeight } = useMemo(() => {
|
|
861
|
+
void trailingRowEpoch
|
|
862
|
+
if (effectiveTrailingRows === 0) {
|
|
863
|
+
return { trailingRows: [] as Array<{ row: number; top: number; height: number }>, trailingHeight: 0 }
|
|
864
|
+
}
|
|
865
|
+
const rows: Array<{ row: number; top: number; height: number }> = []
|
|
866
|
+
let running = 0
|
|
867
|
+
for (let row = rowCount - effectiveTrailingRows; row < rowCount; row += 1) {
|
|
868
|
+
const height = validatedGetRowHeight(row)
|
|
869
|
+
if (height > 0) {
|
|
870
|
+
rows.push({ row, top: running, height })
|
|
871
|
+
}
|
|
872
|
+
running += height
|
|
873
|
+
}
|
|
874
|
+
return { trailingRows: rows, trailingHeight: running }
|
|
875
|
+
}, [effectiveTrailingRows, rowCount, trailingRowEpoch, validatedGetRowHeight])
|
|
876
|
+
trailingHeightRef.current = trailingHeight
|
|
686
877
|
/** Shifted row-height accessor for the embedded VirtualScroll (design §2). / 埋め込み VirtualScroll 用のシフト済み行高アクセサ (設計 §2)。 */
|
|
687
878
|
const scrollGetRowHeight = useCallback((index: number) => validatedGetRowHeight(index + effectiveFrozenRows), [validatedGetRowHeight, effectiveFrozenRows])
|
|
688
879
|
// 行フォーカス通知はフル行空間へ +R 翻訳する (シフト漏れの是正 — 素通しは R > 0 でフル行 r の
|
|
@@ -696,6 +887,8 @@ const VirtualGridInner = <T,>(
|
|
|
696
887
|
const shiftedRowFocus = useMemo(() => (hasRowFocus ? (index: number) => onRowFocusRef.current?.(index + effectiveFrozenRows) : undefined), [effectiveFrozenRows, hasRowFocus])
|
|
697
888
|
/** Reactive key for H_F consumers (memo edge — the ref alone would not invalidate). / H_F 消費 memo の反応辺キー (ref 直読は失効しない)。 */
|
|
698
889
|
const frozenHeightEpochKey = frozenHeight
|
|
890
|
+
/** Reactive key for H_T consumers (the trailing twin). / H_T 消費 memo の反応辺キー (末尾双子)。 */
|
|
891
|
+
const trailingHeightEpochKey = trailingHeight
|
|
699
892
|
|
|
700
893
|
// ---- R の実行時変更 (R1 是正): シフト空間の再ペアリングは埋め込み木にとって「中間削除」で、
|
|
701
894
|
// 木の changeSize (末尾意味論) では歪む (実測 +228px の総高汚染)。key 再マウントで木を
|
|
@@ -756,8 +949,8 @@ const VirtualGridInner = <T,>(
|
|
|
756
949
|
const viewport = explicitViewport ?? measuredViewport
|
|
757
950
|
const viewportRef = useRef(viewport)
|
|
758
951
|
viewportRef.current = viewport
|
|
759
|
-
/** Degenerate band: H_F fills the measured viewport — scroll rows go canonical-EMPTY (§3.3
|
|
760
|
-
const degenerateBand = viewport.height > 0 && viewport.height - frozenHeight <= 0
|
|
952
|
+
/** Degenerate band: H_F + H_T fills the measured viewport — scroll rows go canonical-EMPTY (§3.3 縮退帯の縦対称; 3.5.0 で −H_T を一般化). / 縮退帯: H_F + H_T が実測ビューポートを食い尽くす — スクロール行は正準空窓 (§3.3 の縦対称。3.5.0 で −H_T を一般化)。 */
|
|
953
|
+
const degenerateBand = viewport.height > 0 && viewport.height - frozenHeight - trailingHeight <= 0
|
|
761
954
|
degenerateBandRef.current = degenerateBand
|
|
762
955
|
const hasExplicitViewport = explicitViewport !== null
|
|
763
956
|
useLayoutEffect(() => {
|
|
@@ -793,10 +986,21 @@ const VirtualGridInner = <T,>(
|
|
|
793
986
|
root.style.setProperty("--aqvs-grid-hx-residual", `${committedAnchorRef.current - hxRef.current}px`)
|
|
794
987
|
}, [])
|
|
795
988
|
|
|
989
|
+
/**
|
|
990
|
+
* Fresh scroll-band width from refs (v3.5.0 trailing-freeze plan §6.1-6 — the ONE ref-space
|
|
991
|
+
* source): viewport minus BOTH band extents. Replaces the three former inline
|
|
992
|
+
* `max(0, viewport − W_F)` sites (applyHx / refreshColumns / resolveColumnAnchorHx) so a
|
|
993
|
+
* W_T change can never go stale in just one of them. T = 0 reduces to the pre-trailing form.
|
|
994
|
+
* ref 空間のスクロール帯幅 (3.5.0 末尾凍結プラン §6.1-6 — ref 面の単一源)。viewport から
|
|
995
|
+
* 両端の帯寸を控除する。旧 3 箇所のインライン `max(0, viewport − W_F)` を置換し、W_T 変化が
|
|
996
|
+
* 1 箇所だけ stale 化する余地を構造的に断つ。T = 0 は従来式へ恒等縮退。
|
|
997
|
+
*/
|
|
998
|
+
const scrollBandWidthRef = useCallback(() => Math.max(0, viewportRef.current.width - frozenWidthRef.current - trailingWidthRef.current), [])
|
|
999
|
+
|
|
796
1000
|
// ---- 列窓の再計算 (setState は窓 / アンカー変化時のみ — key 比較で bail) ----
|
|
797
1001
|
const refreshColumns = useCallback(() => {
|
|
798
|
-
// 凍結帯 (§8-a): 窓探索は木座標 W_F + hx 始まり・帯幅 = viewport − W_F
|
|
799
|
-
const bandWidth =
|
|
1002
|
+
// 凍結帯 (§8-a): 窓探索は木座標 W_F + hx 始まり・帯幅 = viewport − W_F − W_T (3.5.0 一般化)
|
|
1003
|
+
const bandWidth = scrollBandWidthRef()
|
|
800
1004
|
const placement = computeColumnPlacement(frozenWidthRef.current + hxRef.current, bandWidth, overscanCols, colCount, validatedGetColWidth, colTree, totalWidthRef.current, columnWindowRef.current.colAnchor)
|
|
801
1005
|
// 幅 0 の凍結列があると indexAt(W_F + hx) が F 未満へ届く — スクロール窓に凍結列を
|
|
802
1006
|
// 再出現させない床クランプ (§8-c: 凍結帯の描画は凍結 subtree の専有)。床は F そのもの:
|
|
@@ -806,21 +1010,31 @@ const VirtualGridInner = <T,>(
|
|
|
806
1010
|
if (effectiveFrozenCols > 0) {
|
|
807
1011
|
placement.renderingColStart = Math.max(placement.renderingColStart, effectiveFrozenCols)
|
|
808
1012
|
placement.visibleColStart = Math.max(placement.visibleColStart, effectiveFrozenCols)
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1013
|
+
}
|
|
1014
|
+
// 末尾帯の ceiling clamp (3.5.0 プラン §6.1-6 — F 床クランプの鏡像・anti-#4454):
|
|
1015
|
+
// stale-bandWidth や幅 0 末尾列で窓末端が末尾帯へ届いても、スクロール窓に末尾列を
|
|
1016
|
+
// 再出現させない。天井は colCount − T_eff − 1 そのもの
|
|
1017
|
+
if (effectiveTrailingCols > 0) {
|
|
1018
|
+
const ceiling = colCount - effectiveTrailingCols - 1
|
|
1019
|
+
placement.renderingColEnd = Math.min(placement.renderingColEnd, ceiling)
|
|
1020
|
+
placement.visibleColEnd = Math.min(placement.visibleColEnd, ceiling)
|
|
1021
|
+
}
|
|
1022
|
+
// 空窓ガードの一般化 (3.5.0 プラン §6.1-6): F > 0 または T > 0 のとき、縮退帯
|
|
1023
|
+
// (帯幅 ≤ 0 — W_F + W_T ≥ viewport) と clamp の結果 start > end になった窓は
|
|
1024
|
+
// 全列凍結と同じ空窓正準形へ整形する (非空の可視窓を報告するとヘッダー同期 /
|
|
1025
|
+
// liveRegion 消費側がセル無き列を描く — R3 実証の非対称の解消。T のみ縮退の
|
|
1026
|
+
// phantom 窓もここで封じる)
|
|
1027
|
+
if ((effectiveFrozenCols > 0 || effectiveTrailingCols > 0) && (bandWidth <= 0 || placement.renderingColStart > placement.renderingColEnd)) {
|
|
1028
|
+
placement.renderingColStart = colCount
|
|
1029
|
+
placement.renderingColEnd = colCount - 1
|
|
1030
|
+
placement.visibleColStart = colCount
|
|
1031
|
+
placement.visibleColEnd = colCount - 1
|
|
818
1032
|
}
|
|
819
1033
|
const key = `${placement.renderingColStart}:${placement.renderingColEnd}:${placement.colAnchor}`
|
|
820
1034
|
if (key !== columnWindowRef.current.key) {
|
|
821
1035
|
setColumnWindow({ ...placement, key })
|
|
822
1036
|
}
|
|
823
|
-
}, [overscanCols, colCount, validatedGetColWidth, colTree, effectiveFrozenCols])
|
|
1037
|
+
}, [overscanCols, colCount, validatedGetColWidth, colTree, effectiveFrozenCols, effectiveTrailingCols, scrollBandWidthRef])
|
|
824
1038
|
|
|
825
1039
|
// ---- 横スクロールバーの追従 (rAF 合流の軽量 state — React 毎フレーム再レンダー回避) ----
|
|
826
1040
|
const [barHx, setBarHx] = useState(0)
|
|
@@ -866,9 +1080,10 @@ const VirtualGridInner = <T,>(
|
|
|
866
1080
|
// 見積り基点は現在の行窓先頭 (到達済みなら) — マウント前は初期アンカー行。
|
|
867
1081
|
// リサイズ時に初期アンカー基点のままだと現在位置と別領域の行高で見積もってしまう
|
|
868
1082
|
const basisRow = rowRangeRef.current?.renderingStartIndex ?? initialAnchorRow
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
1083
|
+
// 凍結行帯 (先頭 + 末尾) はスクロール行の見積り高から控除する (帯外バンド — 設計 §4-3、
|
|
1084
|
+
// 3.5.0 プラン §6.1-12: −H_T 欠落は高い末尾帯のマウントコミットで偽打切りを発火させる)
|
|
1085
|
+
return estimateRenderedRowCount(Math.max(0, viewport.height - frozenHeightRef.current - trailingHeightRef.current), validatedGetRowHeight, rowCount, basisRow, overscanRows)
|
|
1086
|
+
}, [viewport.height, validatedGetRowHeight, rowCount, initialAnchorRow, overscanRows, frozenHeightEpochKey, trailingHeightEpochKey])
|
|
872
1087
|
const effectiveRenderedRows = notifiedRowCount ?? estimatedRowCount
|
|
873
1088
|
|
|
874
1089
|
const buildRange = useCallback((): VirtualGridRange | null => {
|
|
@@ -889,8 +1104,9 @@ const VirtualGridInner = <T,>(
|
|
|
889
1104
|
scrollX: hxRef.current,
|
|
890
1105
|
scrollY: vyRef.current,
|
|
891
1106
|
totalWidth: totalWidthRef.current,
|
|
892
|
-
// 帯高は鮮度加算 (格納はスクロール木のみ — 帯行リサイズ後も総高が正しい。R1
|
|
893
|
-
|
|
1107
|
+
// 帯高は鮮度加算 (格納はスクロール木のみ — 帯行リサイズ後も総高が正しい。R1 是正。
|
|
1108
|
+
// 3.5.0: 末尾帯高 H_T も同じ鮮度規律で加算)
|
|
1109
|
+
totalHeight: rows.totalHeight + frozenHeightRef.current + trailingHeightRef.current,
|
|
894
1110
|
}
|
|
895
1111
|
}, [])
|
|
896
1112
|
|
|
@@ -904,10 +1120,11 @@ const VirtualGridInner = <T,>(
|
|
|
904
1120
|
// ---- applyHx — 横論理位置の唯一の書き口 (clamp → ref → 残差 var → 列窓 → 通知 → クランプ後値 return) ----
|
|
905
1121
|
const applyHx = useCallback(
|
|
906
1122
|
(next: number): number => {
|
|
907
|
-
// 凍結帯 (§8): スクロール帯幅 = viewport − W_F
|
|
908
|
-
// W_F < viewport のとき従来式 (総幅 − viewport)
|
|
909
|
-
|
|
910
|
-
const
|
|
1123
|
+
// 凍結帯 (§8): スクロール帯幅 = viewport − W_F − W_T (3.5.0 一般化)。maxHx =
|
|
1124
|
+
// (総幅 − W_F − W_T) − 帯幅で、W_F + W_T < viewport のとき従来式 (総幅 − viewport)
|
|
1125
|
+
// と一致する (F = T = 0 で恒等)
|
|
1126
|
+
const bandWidth = scrollBandWidthRef()
|
|
1127
|
+
const maxHx = Math.max(0, totalWidthRef.current - frozenWidthRef.current - trailingWidthRef.current - bandWidth)
|
|
911
1128
|
const clamped = minmax(next, 0, maxHx)
|
|
912
1129
|
if (clamped !== hxRef.current) {
|
|
913
1130
|
hxRef.current = clamped
|
|
@@ -918,7 +1135,7 @@ const VirtualGridInner = <T,>(
|
|
|
918
1135
|
}
|
|
919
1136
|
return clamped
|
|
920
1137
|
},
|
|
921
|
-
[writeResidual, refreshColumns, scheduleBarSync],
|
|
1138
|
+
[writeResidual, refreshColumns, scheduleBarSync, scrollBandWidthRef],
|
|
922
1139
|
)
|
|
923
1140
|
const applyHxRef = useRef(applyHx)
|
|
924
1141
|
applyHxRef.current = applyHx
|
|
@@ -936,7 +1153,7 @@ const VirtualGridInner = <T,>(
|
|
|
936
1153
|
if (rowRangeRef.current !== null) {
|
|
937
1154
|
emitRangeChange()
|
|
938
1155
|
}
|
|
939
|
-
}, [frozenHeight, emitRangeChange])
|
|
1156
|
+
}, [frozenHeight, trailingHeight, emitRangeChange])
|
|
940
1157
|
|
|
941
1158
|
// ---- 保留列アンカー (行側 scrollToIndex のアンカー張りの列対): initialScrollAnchor 復元と
|
|
942
1159
|
// scrollToCell が張り、self-heal / 総幅同期 / リサイズで列左が動くたび paint 前に再ピン留め
|
|
@@ -951,13 +1168,14 @@ const VirtualGridInner = <T,>(
|
|
|
951
1168
|
const prefix = colTree.prefixSum(safeCol, { materializeOption: { materialize: false } })
|
|
952
1169
|
const colLeft = prefix.cumulative - prefix.currentValue
|
|
953
1170
|
const colWidth = prefix.currentValue
|
|
954
|
-
// 凍結帯 (§8): 整列先はスクロール帯 [W_F, viewport) — 列座標から W_F を引き、
|
|
955
|
-
//
|
|
956
|
-
|
|
1171
|
+
// 凍結帯 (§8): 整列先はスクロール帯 [W_F, viewport − W_T) — 列座標から W_F を引き、
|
|
1172
|
+
// 幅もスクロール帯幅 (3.5.0 一般化: −W_T 済み) で測る (F = T = 0 では従来式と恒等。
|
|
1173
|
+
// "end" は目標右端を viewport − W_T へ着地させる — §8 の VALUE 行がピンする形)
|
|
1174
|
+
const bandWidth = scrollBandWidthRef()
|
|
957
1175
|
const base = anchor.alignX === "end" ? colLeft + colWidth - frozenWidthRef.current - bandWidth : anchor.alignX === "center" ? colLeft + colWidth / 2 - frozenWidthRef.current - bandWidth / 2 : colLeft - frozenWidthRef.current
|
|
958
1176
|
return base + anchor.offsetX
|
|
959
1177
|
},
|
|
960
|
-
[colTree, colCount],
|
|
1178
|
+
[colTree, colCount, scrollBandWidthRef],
|
|
961
1179
|
)
|
|
962
1180
|
|
|
963
1181
|
// ---- 幅 self-heal (窓内の getColWidth と木の乖離をバッチ適用 — 行 memo の toUpdateHeights 双子)。
|
|
@@ -1023,6 +1241,54 @@ const VirtualGridInner = <T,>(
|
|
|
1023
1241
|
}
|
|
1024
1242
|
}, [frozenWidth, effectiveFrozenCols])
|
|
1025
1243
|
|
|
1244
|
+
// ---- 末尾列帯の列挙 (3.5.0 プラン §6.1-7 — 先頭帯 memo の接尾辞双子)。self-heal 差分は
|
|
1245
|
+
// 共有バッチへ append 合流し、暴走ガードは帯専用の独立カウンタ (T = 0 でエピソードを
|
|
1246
|
+
// 閉じる — 先頭帯の「越境リーク遮断」と同文) ----
|
|
1247
|
+
const trailingHealBurstRef = useRef({ count: 0, warned: false })
|
|
1248
|
+
const { trailingColumns, trailingWidth } = useMemo(() => {
|
|
1249
|
+
// widthEpoch は木内容バージョンの反応辺 (先頭帯 memo と同じ規律)
|
|
1250
|
+
void widthEpoch
|
|
1251
|
+
if (effectiveTrailingCols === 0) {
|
|
1252
|
+
// 末尾凍結解除は帯ガードのエピソードも閉じる (先頭帯の R3 実証と同文の遮断)
|
|
1253
|
+
trailingHealBurstRef.current.count = 0
|
|
1254
|
+
trailingHealBurstRef.current.warned = false
|
|
1255
|
+
return { trailingColumns: EMPTY_PLACED_COLUMNS, trailingWidth: 0 }
|
|
1256
|
+
}
|
|
1257
|
+
const { columns, widthUpdates, width } = collectTrailingColumns(colCount, effectiveTrailingCols, validatedGetColWidth, colTree)
|
|
1258
|
+
if (widthUpdates.length > 0) {
|
|
1259
|
+
const burst = trailingHealBurstRef.current
|
|
1260
|
+
if (burst.count >= WIDTH_HEAL_BURST_LIMIT) {
|
|
1261
|
+
if (!burst.warned) {
|
|
1262
|
+
burst.warned = true
|
|
1263
|
+
Logger.warn("[VirtualGrid] getColWidth keeps disagreeing with its own previous values inside the trailing band; width self-heal is suspended until a converged pass re-arms it (unstable accessor).", {
|
|
1264
|
+
trailingCols: effectiveTrailingCols,
|
|
1265
|
+
pendingUpdates: widthUpdates.length,
|
|
1266
|
+
})
|
|
1267
|
+
}
|
|
1268
|
+
} else {
|
|
1269
|
+
burst.count += 1
|
|
1270
|
+
// 共有バッチへ append 合流 (同 tick の先頭帯 / スクロール窓の治癒を上書きで落とさない)
|
|
1271
|
+
pendingWidthUpdatesRef.current = [...(pendingWidthUpdatesRef.current ?? []), ...widthUpdates]
|
|
1272
|
+
Promise.resolve().then(flushWidthUpdates)
|
|
1273
|
+
}
|
|
1274
|
+
} else {
|
|
1275
|
+
trailingHealBurstRef.current.count = 0
|
|
1276
|
+
trailingHealBurstRef.current.warned = false
|
|
1277
|
+
}
|
|
1278
|
+
return { trailingColumns: columns, trailingWidth: width }
|
|
1279
|
+
}, [effectiveTrailingCols, colCount, widthEpoch, validatedGetColWidth, colTree, flushWidthUpdates])
|
|
1280
|
+
trailingWidthRef.current = trailingWidth
|
|
1281
|
+
// 末尾幅 var の書き手 (3.5.0 プラン §6.1-8 — **単一書き手規律**: この effect だけが書く)。
|
|
1282
|
+
// 消費者はクリップ右端 (.aqvs-grid-row-scroll の right)・末尾クリップの max() 左端と inner 幅。
|
|
1283
|
+
// T = 0 は除去 (T = 0 恒等 — CSS 側 0px フォールバックで安全)
|
|
1284
|
+
useLayoutEffect(() => {
|
|
1285
|
+
if (effectiveTrailingCols > 0) {
|
|
1286
|
+
rootRef.current?.style.setProperty("--aqvs-grid-trailing-width", `${trailingWidth}px`)
|
|
1287
|
+
} else {
|
|
1288
|
+
rootRef.current?.style.removeProperty("--aqvs-grid-trailing-width")
|
|
1289
|
+
}
|
|
1290
|
+
}, [trailingWidth, effectiveTrailingCols])
|
|
1291
|
+
|
|
1026
1292
|
// ---- ビューポート / 総幅 / 幅エポック変化での再クランプ + 列窓追随。保留列アンカーが
|
|
1027
1293
|
// あれば位置はアンカーから再導出 (単なる再クランプでは推定空間の hx が残留する) ----
|
|
1028
1294
|
useLayoutEffect(() => {
|
|
@@ -1033,7 +1299,7 @@ const VirtualGridInner = <T,>(
|
|
|
1033
1299
|
applyHxRef.current(resolveColumnAnchorHx(pending))
|
|
1034
1300
|
}
|
|
1035
1301
|
refreshColumns()
|
|
1036
|
-
}, [viewport.width, totalWidth, widthEpoch, frozenWidth, refreshColumns, resolveColumnAnchorHx])
|
|
1302
|
+
}, [viewport.width, totalWidth, widthEpoch, frozenWidth, trailingWidth, refreshColumns, resolveColumnAnchorHx])
|
|
1037
1303
|
|
|
1038
1304
|
// ---- 初期アンカー復元 (横) — 生 px でなく {col, offsetX} (行と同一契約)。マウント時 1 回。
|
|
1039
1305
|
// 保留列アンカーとして張り、以後の self-heal による列左の実測化へ追随させる ----
|
|
@@ -1048,10 +1314,15 @@ const VirtualGridInner = <T,>(
|
|
|
1048
1314
|
if (targetCol < effectiveFrozenCols) {
|
|
1049
1315
|
return
|
|
1050
1316
|
}
|
|
1317
|
+
// 末尾凍結列狙いも同文の no-op (3.5.0 プラン §6.1-16 — appliedRef を先に立てる現行順序は
|
|
1318
|
+
// 仕様 §7 の登記済み未検証防御と同じ姿勢で維持)
|
|
1319
|
+
if (targetCol >= colCount - effectiveTrailingCols) {
|
|
1320
|
+
return
|
|
1321
|
+
}
|
|
1051
1322
|
const anchor = { col: targetCol, alignX: "start" as const, offsetX: Math.max(0, initialScrollAnchor.offsetX) }
|
|
1052
1323
|
pendingColAnchorRef.current = anchor
|
|
1053
1324
|
applyHxRef.current(resolveColumnAnchorHx(anchor))
|
|
1054
|
-
}, [initialScrollAnchor, colCount, resolveColumnAnchorHx, effectiveFrozenCols])
|
|
1325
|
+
}, [initialScrollAnchor, colCount, resolveColumnAnchorHx, effectiveFrozenCols, effectiveTrailingCols])
|
|
1055
1326
|
|
|
1056
1327
|
// ---- 縦軸の結線 ----
|
|
1057
1328
|
const handleVerticalScroll = useCallback((position: number) => {
|
|
@@ -1097,8 +1368,9 @@ const VirtualGridInner = <T,>(
|
|
|
1097
1368
|
}, [])
|
|
1098
1369
|
|
|
1099
1370
|
// ---- 描画列の列挙 (配置ごとに 1 回 — 行間で共有) + セル数上限 ----
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1371
|
+
// 凍結行帯 (先頭 + 末尾) は全帯行 × 全描画列を具現化する — 予算分母に帯行数を算入
|
|
1372
|
+
// (行凍結設計 §4-7、3.5.0 プラン §6.1-13)
|
|
1373
|
+
const maxColsPerRow = resolveMaxColsPerRow(__maxRenderedCells, effectiveRenderedRows + frozenRows.length + trailingRows.length)
|
|
1102
1374
|
// self-heal の暴走ガード: 乖離→治癒→再乖離の非収束が連続する = getColWidth が自身の
|
|
1103
1375
|
// 過去値と不一致を繰り返す契約違反 (振動アクセサ)。放置すると治癒→エポック→再走査の
|
|
1104
1376
|
// マイクロタスクループが無限旋回するため、上限で治癒を停止し 1 回だけ警告する。
|
|
@@ -1108,9 +1380,9 @@ const VirtualGridInner = <T,>(
|
|
|
1108
1380
|
const { placedColumns, truncatedCols } = useMemo(() => {
|
|
1109
1381
|
// widthEpoch は木の内容バージョンの反応辺 (木は identity 安定のため値経由では失効しない)
|
|
1110
1382
|
void widthEpoch
|
|
1111
|
-
// 凍結帯 (§8): スクロール帯幅 ≤ 0 (W_F ≥ viewport の縮退)
|
|
1112
|
-
//
|
|
1113
|
-
const bandWidth = Math.max(0, viewport.width - frozenWidth)
|
|
1383
|
+
// 凍結帯 (§8): スクロール帯幅 ≤ 0 (W_F + W_T ≥ viewport の縮退 — 3.5.0 一般化) は
|
|
1384
|
+
// スクロール列を描かない — 凍結帯 / 末尾帯自体は独立 memo が描き続ける定義済み縮退
|
|
1385
|
+
const bandWidth = Math.max(0, viewport.width - frozenWidth - trailingWidth)
|
|
1114
1386
|
if (colCount === 0 || bandWidth <= 0) {
|
|
1115
1387
|
return { placedColumns: [] as PlacedColumn[], truncatedCols: 0 }
|
|
1116
1388
|
}
|
|
@@ -1118,8 +1390,8 @@ const VirtualGridInner = <T,>(
|
|
|
1118
1390
|
if (columnWindow.renderingColStart > columnWindow.renderingColEnd) {
|
|
1119
1391
|
return { placedColumns: [] as PlacedColumn[], truncatedCols: 0 }
|
|
1120
1392
|
}
|
|
1121
|
-
//
|
|
1122
|
-
const scrollColsBudget = Math.max(1, maxColsPerRow - frozenColumns.length)
|
|
1393
|
+
// セル予算は凍結列 + 末尾列ぶんを控除 (両帯セルも行ごとに具現化するため — 上限は行の総セル数)
|
|
1394
|
+
const scrollColsBudget = Math.max(1, maxColsPerRow - frozenColumns.length - trailingColumns.length)
|
|
1123
1395
|
const { columns, widthUpdates, truncated } = collectVisibleColumns(minmax(columnWindow.renderingColStart, 0, colCount - 1), minmax(columnWindow.renderingColEnd, 0, colCount - 1), validatedGetColWidth, colTree, scrollColsBudget)
|
|
1124
1396
|
if (widthUpdates.length > 0) {
|
|
1125
1397
|
const burst = healBurstRef.current
|
|
@@ -1147,7 +1419,10 @@ const VirtualGridInner = <T,>(
|
|
|
1147
1419
|
healBurstRef.current.warned = false
|
|
1148
1420
|
}
|
|
1149
1421
|
return { placedColumns: columns, truncatedCols: truncated }
|
|
1150
|
-
|
|
1422
|
+
// deps へ trailingWidth / trailingColumns.length を明示追加 (3.5.0 プラン §6.1-6 / F4):
|
|
1423
|
+
// bandWidth 式とスクロール列予算が同 memo 内で両値を読む — 追加を怠ると列窓キー不変の
|
|
1424
|
+
// T トグルで memo が stale 化し、予算が trailing 分だけ過大に残る
|
|
1425
|
+
}, [columnWindow, colCount, viewport.width, frozenWidth, trailingWidth, frozenColumns.length, trailingColumns.length, widthEpoch, validatedGetColWidth, colTree, maxColsPerRow, flushWidthUpdates])
|
|
1151
1426
|
|
|
1152
1427
|
// 打切りは無言にしない (設計 §3.2 — 行側の病的ガードと違い正当構成で到達し得る)
|
|
1153
1428
|
const lastTruncationKeyRef = useRef("")
|
|
@@ -1156,10 +1431,11 @@ const VirtualGridInner = <T,>(
|
|
|
1156
1431
|
const key = `${columnWindow.key}:${truncatedCols}`
|
|
1157
1432
|
if (key !== lastTruncationKeyRef.current) {
|
|
1158
1433
|
lastTruncationKeyRef.current = key
|
|
1159
|
-
|
|
1434
|
+
// 打切り分子は両帯セル / 両帯行込みの exactness を維持する (3.5.0 プラン §6.1-13)
|
|
1435
|
+
onRenderTruncatedRef.current?.({ renderedCells: (placedColumns.length + frozenColumns.length + trailingColumns.length) * (effectiveRenderedRows + frozenRows.length + trailingRows.length), droppedCols: truncatedCols })
|
|
1160
1436
|
}
|
|
1161
1437
|
}
|
|
1162
|
-
}, [truncatedCols, columnWindow.key, placedColumns.length, frozenColumns.length, effectiveRenderedRows, frozenRows.length])
|
|
1438
|
+
}, [truncatedCols, columnWindow.key, placedColumns.length, frozenColumns.length, trailingColumns.length, effectiveRenderedRows, frozenRows.length, trailingRows.length])
|
|
1163
1439
|
|
|
1164
1440
|
// ---- 行レンダラー (VirtualScroll children) — セル left は「絶対 − コミット対象アンカー」 ----
|
|
1165
1441
|
const colAnchor = columnWindow.colAnchor
|
|
@@ -1181,20 +1457,23 @@ const VirtualGridInner = <T,>(
|
|
|
1181
1457
|
</div>
|
|
1182
1458
|
)
|
|
1183
1459
|
})
|
|
1184
|
-
//
|
|
1185
|
-
if (effectiveFrozenCols === 0) {
|
|
1460
|
+
// 横帯なし (既定) は従来 DOM と恒等 — 行が残差 translate を運ぶ (中身はバイト恒等)
|
|
1461
|
+
if (effectiveFrozenCols === 0 && effectiveTrailingCols === 0) {
|
|
1186
1462
|
return (
|
|
1187
1463
|
<div {...rowProps} className={twMerge("aqvs-grid-row", rowProps?.className)} style={rowProps?.style}>
|
|
1188
1464
|
{scrollCells}
|
|
1189
1465
|
</div>
|
|
1190
1466
|
)
|
|
1191
1467
|
}
|
|
1192
|
-
//
|
|
1193
|
-
// −W_F 原点シフト付き残差 translate
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1468
|
+
// 横帯モード (§8-c + 3.5.0 プラン §6.1-15): 行は静的化し、スクロールセルは左端 W_F ・
|
|
1469
|
+
// 右端 W_T の静的クリップ + −W_F 原点シフト付き残差 translate の内側へ。凍結セルは
|
|
1470
|
+
// 行直下の独立 subtree で木絶対 left の直接描画 — DOM は**前置** (論理列順 = 読み上げ /
|
|
1471
|
+
// フォーカス順)。末尾セルは右アンカー inner の帯ローカル left で**後置** (同じく論理列順)。
|
|
1472
|
+
// class はモードマーカー (描画トレイト) のまま、data-aqvs-frozen-row は F_eff > 0 のみ
|
|
1473
|
+
// (ADR-25 — 末尾のみのグリッドが凍結列ゼロで属性を出すフック面の嘘の封じ)。
|
|
1474
|
+
// 描画順は自由: クリップが帯への侵入を構造的に遮断するため重なり自体が無い
|
|
1196
1475
|
return (
|
|
1197
|
-
<div {...rowProps} data-aqvs-frozen-row="" className={twMerge("aqvs-grid-row aqvs-grid-row-frozen-host", rowProps?.className)} style={rowProps?.style}>
|
|
1476
|
+
<div {...rowProps} data-aqvs-frozen-row={effectiveFrozenCols > 0 ? "" : undefined} className={twMerge("aqvs-grid-row aqvs-grid-row-frozen-host", rowProps?.className)} style={rowProps?.style}>
|
|
1198
1477
|
{hidden
|
|
1199
1478
|
? null
|
|
1200
1479
|
: frozenColumns.map(({ col, left, width }) => {
|
|
@@ -1211,10 +1490,25 @@ const VirtualGridInner = <T,>(
|
|
|
1211
1490
|
<div className="aqvs-grid-row-scroll-inner">{scrollCells}</div>
|
|
1212
1491
|
</div>
|
|
1213
1492
|
)}
|
|
1493
|
+
{hidden || effectiveTrailingCols === 0 ? null : (
|
|
1494
|
+
<div className="aqvs-grid-row-trailing">
|
|
1495
|
+
<div className="aqvs-grid-row-trailing-inner">
|
|
1496
|
+
{trailingColumns.map(({ col, left, width }) => {
|
|
1497
|
+
const cellProps = getCellProps?.(rowIndex, col)
|
|
1498
|
+
const style: CSSProperties = { ...cellProps?.style, left, width }
|
|
1499
|
+
return (
|
|
1500
|
+
<div {...cellProps} data-aqvs-trailing-cell="" key={getCellKey ? getCellKey(rowIndex, col) : col} className={twMerge("aqvs-grid-cell", cellProps?.className)} style={style}>
|
|
1501
|
+
{children(getCell(rowIndex, col), rowIndex, col)}
|
|
1502
|
+
</div>
|
|
1503
|
+
)
|
|
1504
|
+
})}
|
|
1505
|
+
</div>
|
|
1506
|
+
</div>
|
|
1507
|
+
)}
|
|
1214
1508
|
</div>
|
|
1215
1509
|
)
|
|
1216
1510
|
},
|
|
1217
|
-
[placedColumns, frozenColumns, effectiveFrozenCols, colAnchor, getRowProps, getCellProps, getCellKey, getCell, children, validatedGetRowHeight],
|
|
1511
|
+
[placedColumns, frozenColumns, trailingColumns, effectiveFrozenCols, effectiveTrailingCols, colAnchor, getRowProps, getCellProps, getCellKey, getCell, children, validatedGetRowHeight],
|
|
1218
1512
|
)
|
|
1219
1513
|
|
|
1220
1514
|
// ---- liveRegion (2D — 文言は消費側所有、静定デバウンス) ----
|
|
@@ -1313,15 +1607,17 @@ const VirtualGridInner = <T,>(
|
|
|
1313
1607
|
if (inner !== null && rowCount > 0) {
|
|
1314
1608
|
const targetRow = minmax(Math.trunc(row), 0, rowCount - 1)
|
|
1315
1609
|
// 凍結行狙いの y は no-op (行凍結設計 §3 — 常時可視。列の §8 no-op と同文)。
|
|
1610
|
+
// 末尾帯行狙いも同文の縦 no-op (帯所属は**呼出し時**判定 — 3.5.0 プラン §3)。
|
|
1316
1611
|
// スクロール行はシフト空間へ −R 翻訳して委譲する
|
|
1317
|
-
if (targetRow >= effectiveFrozenRows) {
|
|
1612
|
+
if (targetRow >= effectiveFrozenRows && targetRow < rowCount - effectiveTrailingRows) {
|
|
1318
1613
|
inner.scrollToIndex(targetRow - effectiveFrozenRows, { align: options?.alignY ?? "top", offset: options?.offsetY })
|
|
1319
1614
|
}
|
|
1320
1615
|
}
|
|
1321
1616
|
if (colCount > 0) {
|
|
1322
1617
|
const targetCol = minmax(Math.trunc(col), 0, colCount - 1)
|
|
1323
|
-
// 凍結列狙いの x は no-op (§8 —
|
|
1324
|
-
|
|
1618
|
+
// 凍結列狙いの x は no-op (§8 — 常時可視)。末尾列狙いも横 no-op かつ既存の
|
|
1619
|
+
// 列アンカー残置 (分岐スキップのみ — pendingColAnchorRef は触らない。3.5.0 §3)
|
|
1620
|
+
if (targetCol >= effectiveFrozenCols && targetCol < colCount - effectiveTrailingCols) {
|
|
1325
1621
|
// 列アンカーを張る (行 scrollToIndex のアンカー張りと同義): 以後の self-heal で
|
|
1326
1622
|
// 列左が実測化されても狙った列に留まり続ける。手動横入力で解除
|
|
1327
1623
|
const anchor = { col: targetCol, alignX: options?.alignX ?? "start", offsetX: options?.offsetX ?? 0 }
|
|
@@ -1357,15 +1653,19 @@ const VirtualGridInner = <T,>(
|
|
|
1357
1653
|
return { row: rowAnchor.index + effectiveFrozenRows, col: found.index, offsetX: Math.max(0, bandStart - colLeft), offsetY: rowAnchor.offsetPx }
|
|
1358
1654
|
},
|
|
1359
1655
|
getViewportSize: () => ({ width: viewportRef.current.width, height: viewportRef.current.height }),
|
|
1360
|
-
// 縦はインセット込みの行側 getContentSize を単一情報源にする (range.totalHeight は木総高のみ)
|
|
1361
|
-
|
|
1656
|
+
// 縦はインセット込みの行側 getContentSize を単一情報源にする (range.totalHeight は木総高のみ)。
|
|
1657
|
+
// 3.5.0: height = H_F + 埋め込み + H_T
|
|
1658
|
+
getContentSize: () => ({ width: totalWidthRef.current, height: scrollHandleRef.current === null ? 0 : frozenHeightRef.current + trailingHeightRef.current + Math.max(0, scrollHandleRef.current.getContentSize()) }),
|
|
1362
1659
|
getRange: buildRange,
|
|
1363
1660
|
updateRowSize: (row, px) => {
|
|
1364
1661
|
validateTrackSize(px, "row", row)
|
|
1365
1662
|
// 凍結行はグリッド所有バンド — epoch 繰上げでアクセサ再読 (帯に第 2 の格納庫は
|
|
1366
|
-
// 無いため木更新は不要)
|
|
1663
|
+
// 無いため木更新は不要)。末尾帯行も同文の epoch 繰上げ (3.5.0 — 3 分岐。T = 0 は
|
|
1664
|
+
// 分岐自体が不成立で従来挙動と恒等)。スクロール行はシフト空間へ −R 翻訳して委譲する
|
|
1367
1665
|
if (Math.trunc(row) < effectiveFrozenRows) {
|
|
1368
1666
|
setFrozenRowEpoch((epoch) => epoch + 1)
|
|
1667
|
+
} else if (effectiveTrailingRows > 0 && Math.trunc(row) >= rowCount - effectiveTrailingRows) {
|
|
1668
|
+
setTrailingRowEpoch((epoch) => epoch + 1)
|
|
1369
1669
|
} else {
|
|
1370
1670
|
scrollHandleRef.current?.updateItemSize(Math.trunc(row) - effectiveFrozenRows, px)
|
|
1371
1671
|
}
|
|
@@ -1377,23 +1677,38 @@ const VirtualGridInner = <T,>(
|
|
|
1377
1677
|
// 窓キー不変の幅変更でもセル配置を失効させる (バッチループは React 自動バッチングで 1 コミット)
|
|
1378
1678
|
setWidthEpoch((epoch) => epoch + 1)
|
|
1379
1679
|
},
|
|
1380
|
-
getFrozenSize: () => ({
|
|
1680
|
+
getFrozenSize: () => ({
|
|
1681
|
+
cols: effectiveFrozenCols,
|
|
1682
|
+
width: frozenWidthRef.current,
|
|
1683
|
+
rows: effectiveFrozenRows,
|
|
1684
|
+
height: frozenHeightRef.current,
|
|
1685
|
+
trailingCols: effectiveTrailingCols,
|
|
1686
|
+
trailingRows: effectiveTrailingRows,
|
|
1687
|
+
trailingWidth: trailingWidthRef.current,
|
|
1688
|
+
trailingHeight: trailingHeightRef.current,
|
|
1689
|
+
// vis は単一 JS 源ヘルパーの呼び出しのみ (式の再導出禁止 — 3.5.0 プラン §2.1-4)
|
|
1690
|
+
trailingVisibleWidth: trailingVisibleSize(trailingWidthRef.current, frozenWidthRef.current, viewportRef.current.width),
|
|
1691
|
+
trailingVisibleHeight: trailingVisibleSize(trailingHeightRef.current, frozenHeightRef.current, viewportRef.current.height),
|
|
1692
|
+
}),
|
|
1381
1693
|
applyWheel: (event) => scrollHandleRef.current?.applyWheel(event) ?? false,
|
|
1382
1694
|
focusRowAtIndex: (row, options) => {
|
|
1383
|
-
// 凍結行はバンド行 DOM
|
|
1695
|
+
// 凍結行はバンド行 DOM へ直接フォーカス、末尾帯行も帯行 DOM へ直接フォーカス
|
|
1696
|
+
// (3.5.0 — 3 分岐。T = 0 は分岐不成立で従来挙動と恒等)、スクロール行は −R 翻訳して委譲
|
|
1384
1697
|
const targetRow = Math.trunc(row)
|
|
1385
1698
|
if (targetRow < effectiveFrozenRows) {
|
|
1386
1699
|
rootRef.current?.querySelector<HTMLElement>(`[data-aqvs-frozen-band-row="${targetRow}"]`)?.focus()
|
|
1700
|
+
} else if (effectiveTrailingRows > 0 && targetRow >= rowCount - effectiveTrailingRows) {
|
|
1701
|
+
rootRef.current?.querySelector<HTMLElement>(`[data-aqvs-trailing-band-row="${targetRow}"]`)?.focus()
|
|
1387
1702
|
} else {
|
|
1388
1703
|
scrollHandleRef.current?.focusItemAtIndex(targetRow - effectiveFrozenRows, options)
|
|
1389
1704
|
}
|
|
1390
1705
|
},
|
|
1391
1706
|
}),
|
|
1392
|
-
[rowCount, colCount, colTree, buildRange, commitTotalWidth, resolveColumnAnchorHx, effectiveFrozenCols, effectiveFrozenRows],
|
|
1707
|
+
[rowCount, colCount, colTree, buildRange, commitTotalWidth, resolveColumnAnchorHx, effectiveFrozenCols, effectiveFrozenRows, effectiveTrailingCols, effectiveTrailingRows],
|
|
1393
1708
|
)
|
|
1394
1709
|
|
|
1395
1710
|
return (
|
|
1396
|
-
<div ref={rootRef} className={twMerge("aqvs-grid", effectiveFrozenRows > 0 ? "aqvs-grid-has-frozen-rows" : "", className)} data-testid={testId}>
|
|
1711
|
+
<div ref={rootRef} className={twMerge("aqvs-grid", effectiveFrozenRows > 0 ? "aqvs-grid-has-frozen-rows" : "", effectiveTrailingRows > 0 ? "aqvs-grid-has-trailing-rows" : "", className)} data-testid={testId}>
|
|
1397
1712
|
{effectiveFrozenRows > 0 ? (
|
|
1398
1713
|
// 凍結行帯 (行凍結設計 §4-2): グリッド所有の帯外バンド — 行本体は renderRow を
|
|
1399
1714
|
// そのまま再利用する (F > 0 の凍結セル / クリップ / hx 残差 var は継承で無償追随
|
|
@@ -1421,7 +1736,7 @@ const VirtualGridInner = <T,>(
|
|
|
1421
1736
|
getItemKey={(i) => i + effectiveFrozenRows}
|
|
1422
1737
|
getItemHeight={scrollGetRowHeight}
|
|
1423
1738
|
overscanCount={overscanRows}
|
|
1424
|
-
viewportSize={viewport.height > 0 ? Math.max(0, viewport.height - frozenHeight) : undefined}
|
|
1739
|
+
viewportSize={viewport.height > 0 ? Math.max(0, viewport.height - frozenHeight - trailingHeight) : undefined}
|
|
1425
1740
|
callbackThrottleMs={callbackThrottleMs}
|
|
1426
1741
|
behaviorOptions={behaviorOptions}
|
|
1427
1742
|
scrollBarOptions={scrollBarOptions}
|
|
@@ -1439,7 +1754,7 @@ const VirtualGridInner = <T,>(
|
|
|
1439
1754
|
? toggleAnchorRef.current !== null && toggleAnchorRef.current.row >= effectiveFrozenRows && scrollRowCount > 0
|
|
1440
1755
|
? { index: toggleAnchorRef.current.row - effectiveFrozenRows, offsetPx: toggleAnchorRef.current.offsetPx }
|
|
1441
1756
|
: undefined
|
|
1442
|
-
: initialScrollAnchor !== undefined && scrollRowCount > 0 && Math.trunc(initialScrollAnchor.row) >= effectiveFrozenRows
|
|
1757
|
+
: initialScrollAnchor !== undefined && scrollRowCount > 0 && Math.trunc(initialScrollAnchor.row) >= effectiveFrozenRows && Math.trunc(initialScrollAnchor.row) < rowCount - effectiveTrailingRows
|
|
1443
1758
|
? { index: Math.trunc(initialScrollAnchor.row) - effectiveFrozenRows, offsetPx: initialScrollAnchor.offsetY }
|
|
1444
1759
|
: undefined
|
|
1445
1760
|
}
|
|
@@ -1451,16 +1766,37 @@ const VirtualGridInner = <T,>(
|
|
|
1451
1766
|
{renderRow}
|
|
1452
1767
|
</VirtualScroll>
|
|
1453
1768
|
</div>
|
|
1454
|
-
|
|
1769
|
+
{effectiveTrailingRows > 0 ? (
|
|
1770
|
+
// 末尾行帯 (3.5.0 プラン §6.1-17): .aqvs-grid-main の**後**・hbar の**前**の兄弟
|
|
1771
|
+
// (AT には「最終行が最後」の DOM 順正しさ。縦バーは帯の上で自然に終端)。帯クリップ高は
|
|
1772
|
+
// インライン H_T_vis (単一 JS 源 — 帯寸の CSS 変数は幅 2 つのみで高さ変数は導入しない)、
|
|
1773
|
+
// inner はインライン木高 H_T の bottom 定着 — 帯上端 = extent − H_T_vis が全構成で
|
|
1774
|
+
// 成立し、重複縮退では帯の**末尾**行が可視に残る。行本体は renderRow 逐語再利用
|
|
1775
|
+
// (先頭列 / 末尾列の帯構造を継承し 4 象限コーナー全種が無償で成立)
|
|
1776
|
+
<div className="aqvs-grid-trailing-rows" style={{ height: trailingVisibleSize(trailingHeight, frozenHeight, viewport.height) }} data-testid={testId ? `${testId}-trailing-rows` : undefined}>
|
|
1777
|
+
<div className="aqvs-grid-trailing-rows-inner" style={{ height: trailingHeight }}>
|
|
1778
|
+
{trailingRows.map(({ row, top, height }) => (
|
|
1779
|
+
// 帯行は帯ローカル top + right: scrollBarWidth (縦バーコーナーの予約 — 先頭帯行と
|
|
1780
|
+
// 同文) + tabIndex −1 + onFocus フル行 index (focusRowAtIndex ↔ onRowFocus 往復)。
|
|
1781
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: role は消費側の複合ウィジェット契約の所有物 (先頭帯行と同文)
|
|
1782
|
+
<div key={row} data-aqvs-trailing-band-row={row} className="aqvs-grid-trailing-band-row" style={{ top, height, right: scrollBarWidth }} tabIndex={-1} onFocus={() => onRowFocus?.(row)}>
|
|
1783
|
+
{renderRow(row)}
|
|
1784
|
+
</div>
|
|
1785
|
+
))}
|
|
1786
|
+
</div>
|
|
1787
|
+
</div>
|
|
1788
|
+
) : null}
|
|
1789
|
+
<div className="aqvs-grid-hbar-strip" style={{ height: scrollBarWidth, paddingRight: effectiveTrailingCols > 0 ? scrollBarWidth + trailingVisibleSize(trailingWidth, frozenWidth, viewport.width) : scrollBarWidth }} data-testid={testId ? `${testId}-hbar` : undefined}>
|
|
1455
1790
|
<ScrollBar
|
|
1456
1791
|
horizontal
|
|
1457
1792
|
enableHorizontalTapCircle
|
|
1458
|
-
contentSize={Math.max(0, totalWidth - frozenWidth)}
|
|
1459
|
-
viewportSize={Math.max(0, viewport.width - frozenWidth)}
|
|
1793
|
+
contentSize={Math.max(0, totalWidth - frozenWidth - trailingWidth)}
|
|
1794
|
+
viewportSize={Math.max(0, viewport.width - frozenWidth - trailingWidth)}
|
|
1460
1795
|
scrollPosition={barHx}
|
|
1461
1796
|
// タップ速度の log10(colCount) 則 (設計 §3.3 — B7 の対策そのもの)。未配線だと
|
|
1462
|
-
// 兆列でも基礎倍率 2.2x
|
|
1463
|
-
|
|
1797
|
+
// 兆列でも基礎倍率 2.2x に縮退し、サム粒度の到達性が死ぬ。−F / −T 写像は
|
|
1798
|
+
// aria 面に露出せずピン不能 (仕様 §7 非ピン面 (3) の登記済み面)
|
|
1799
|
+
itemCount={Math.max(0, colCount - effectiveFrozenCols - effectiveTrailingCols)}
|
|
1464
1800
|
tapScrollCircleOptions={scrollBarOptions?.tapScrollCircleOptions}
|
|
1465
1801
|
onScroll={(request, previous) => {
|
|
1466
1802
|
// バー操作は手動横入力 — 列アンカーの張りを解除する
|