@aiquants/virtualscroll 3.9.1 → 3.9.2
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 +24 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1937 -1934
- package/package.json +1 -1
- package/src/VirtualScroll.tsx +84 -24
package/package.json
CHANGED
package/src/VirtualScroll.tsx
CHANGED
|
@@ -678,6 +678,24 @@ const edgeOfAlignment = (align: "top" | "bottom" | "center" | undefined): Aligne
|
|
|
678
678
|
return align === "bottom" ? "end" : "start"
|
|
679
679
|
}
|
|
680
680
|
|
|
681
|
+
/**
|
|
682
|
+
* Returns the canonical form of an alignment to remember: `null` for one that can never change the edge the items
|
|
683
|
+
* wrapper keeps, the alignment itself otherwise. An alignment at pane position 0 or below holds only at positions within
|
|
684
|
+
* `EDGE_POSITION_TOLERANCE` of position 0, where `resolveItemsWrapperSnapEdge` answers `"start"` from the position
|
|
685
|
+
* alone, so it means the same as remembering nothing. Storing only the canonical form keeps one state for one meaning:
|
|
686
|
+
* a list the user scrolled back to the top and a list `scrollToIndex` aligned there hold the same state, so aligning it
|
|
687
|
+
* to the top again writes nothing.
|
|
688
|
+
*
|
|
689
|
+
* 覚える揃えの正規形を返す処理。行ラッパーが守る端を決して変えない揃えは `null`、それ以外は揃えそのもの。ペイン位置 0 以下の
|
|
690
|
+
* 揃えが成り立つのは位置 0 から `EDGE_POSITION_TOLERANCE` 以内の位置だけで、そこでは `resolveItemsWrapperSnapEdge` が位置
|
|
691
|
+
* だけから `"start"` を返すため、何も覚えないのと同じ意味。正規形だけを持つことで 1 つの意味に 1 つの状態となり、利用者が
|
|
692
|
+
* 先頭へ戻した一覧と `scrollToIndex` が先頭へ揃えた一覧が同じ状態を持ち、先頭へ揃え直しても何も書かない。
|
|
693
|
+
*
|
|
694
|
+
* @param alignment - The alignment, or `null` / 揃え (無ければ `null`)
|
|
695
|
+
* @returns The canonical alignment, or `null` / 正規形の揃え (無ければ `null`)
|
|
696
|
+
*/
|
|
697
|
+
const canonicalAlignedEdge = (alignment: AlignedEdge | null): AlignedEdge | null => (alignment !== null && alignment.panePosition <= 0 ? null : alignment)
|
|
698
|
+
|
|
681
699
|
/**
|
|
682
700
|
* Chooses the edge the items-wrapper translate keeps when it is snapped to the device-pixel grid
|
|
683
701
|
* (`snapToDevicePixelGrid`), so that aligned content never loses part of its edge gutter to the snap:
|
|
@@ -1428,26 +1446,48 @@ const VirtualScrollInner = <T,>(
|
|
|
1428
1446
|
|
|
1429
1447
|
const [scrollPosition, setScrollPosition] = useState(initialValues.position)
|
|
1430
1448
|
const [contentSize, setContentSize] = useState<number>(initialValues.total)
|
|
1431
|
-
// 初期位置の index とアンカーは上端揃えの scrollToIndex
|
|
1432
|
-
//
|
|
1433
|
-
|
|
1434
|
-
const [alignedEdge, setAlignedEdge] = useState<AlignedEdge | null>(() => ((initialScrollAnchor && itemCount > 0) || typeof initialScrollIndex === "number" || initialValues.position <= EDGE_POSITION_TOLERANCE ? { edge: "start", panePosition: initialValues.position } : null))
|
|
1449
|
+
// 初期位置の index とアンカーは上端揃えの scrollToIndex と同じ位置なので、その位置の上端揃えとして覚えておく。覚えずに
|
|
1450
|
+
// 始めると、同じ行へ揃え直すだけの最初の scrollToIndex が状態を書き、確定を 1 回足す。位置 0 では正規形が null になる
|
|
1451
|
+
const [alignedEdge, setAlignedEdge] = useState<AlignedEdge | null>(() => ((initialScrollAnchor && itemCount > 0) || typeof initialScrollIndex === "number" ? canonicalAlignedEdge({ edge: "start", panePosition: initialValues.position }) : null))
|
|
1435
1452
|
// 状態の同期の写し。手動スクロールのたびに状態へ null を書くと、同じ値でも React が描画を予約し得るため、写しで要否を決める
|
|
1436
1453
|
const alignedEdgeRef = useRef(alignedEdge)
|
|
1454
|
+
// contentSize の同期の写し。確定の直後は同じ値の setState でも React が描画を 1 回予約するため、書くかどうかを写しで決める
|
|
1455
|
+
const contentSizeRef = useRef(contentSize)
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Writes the content size the pane and the scroll bar are sized by, and its synchronous copy, only when the value
|
|
1459
|
+
* differs from the one last written (every writer of the content size goes through here, so the copy is the value the
|
|
1460
|
+
* state settles on).
|
|
1461
|
+
*
|
|
1462
|
+
* ペインとスクロールバーの寸法の元になる中身の寸法と、その同期の写しへ、最後に書いた値と違うときだけ書く処理 (中身の寸法の
|
|
1463
|
+
* 書き手はすべてここを通るため、写しは状態が落ち着く値)。
|
|
1464
|
+
*
|
|
1465
|
+
* @param next - The content size (px) / 中身の寸法 (px)
|
|
1466
|
+
*/
|
|
1467
|
+
const writeContentSize = useCallback((next: number): void => {
|
|
1468
|
+
if (contentSizeRef.current === next) {
|
|
1469
|
+
return
|
|
1470
|
+
}
|
|
1471
|
+
contentSizeRef.current = next
|
|
1472
|
+
setContentSize(next)
|
|
1473
|
+
}, [])
|
|
1437
1474
|
|
|
1438
1475
|
/**
|
|
1439
|
-
* Remembers the alignment that holds at a pane position, or forgets it with `null`:
|
|
1440
|
-
*
|
|
1476
|
+
* Remembers the alignment that holds at a pane position, or forgets it with `null`: reduces it to its canonical form
|
|
1477
|
+
* (`canonicalAlignedEdge`, so an alignment at position 0 is the same as none), then writes the state the items wrapper
|
|
1478
|
+
* snaps with and its synchronous copy, only when the canonical form differs from the remembered one in edge or pane
|
|
1479
|
+
* position.
|
|
1441
1480
|
*
|
|
1442
|
-
* あるペイン位置で成り立つ揃えを覚える処理 (`null` で忘れる)
|
|
1443
|
-
*
|
|
1481
|
+
* あるペイン位置で成り立つ揃えを覚える処理 (`null` で忘れる)。揃えを正規形にし (`canonicalAlignedEdge`。位置 0 の揃えは
|
|
1482
|
+
* 揃え無しと同じ)、行ラッパーが揃えに使う状態と、その同期の写しへ、正規形が覚えた揃えと辺かペイン位置で違うときだけ書く。
|
|
1444
1483
|
*
|
|
1445
|
-
* @param
|
|
1484
|
+
* @param alignment - The alignment, or `null` / 揃え (無ければ `null`)
|
|
1446
1485
|
*/
|
|
1447
|
-
const rememberAlignedEdge = useCallback((
|
|
1486
|
+
const rememberAlignedEdge = useCallback((alignment: AlignedEdge | null): void => {
|
|
1448
1487
|
const current = alignedEdgeRef.current
|
|
1488
|
+
const next = canonicalAlignedEdge(alignment)
|
|
1449
1489
|
// 同じ揃えでも新しいオブジェクトを書けば React は描画を予約する。同じ位置への scrollToIndex (ホストが打鍵ごとに
|
|
1450
|
-
//
|
|
1490
|
+
// 揃え直すなど) のたびに確定が 1 回増えるため、辺と位置が同じなら書かない
|
|
1451
1491
|
if (current === next || (current !== null && next !== null && current.edge === next.edge && current.panePosition === next.panePosition)) {
|
|
1452
1492
|
return
|
|
1453
1493
|
}
|
|
@@ -1457,10 +1497,15 @@ const VirtualScrollInner = <T,>(
|
|
|
1457
1497
|
|
|
1458
1498
|
/**
|
|
1459
1499
|
* Moves the remembered alignment along with a position change that keeps the aligned row in place (layout-shift
|
|
1460
|
-
* compensation): when the alignment held at `fromPanePosition`, it now holds at `toPanePosition`.
|
|
1500
|
+
* compensation): when the alignment held at `fromPanePosition`, it now holds at `toPanePosition`. Only a remembered
|
|
1501
|
+
* alignment moves: the top edge a list keeps at position 0 comes from the position (`resolveItemsWrapperSnapEdge`), not
|
|
1502
|
+
* from the state (`canonicalAlignedEdge`), so a move away from position 0 carries no edge, and an alignment carried to
|
|
1503
|
+
* position 0 or below is dropped.
|
|
1461
1504
|
*
|
|
1462
1505
|
* 揃えた行をその場に留める位置の変化 (レイアウトシフトの補正) に合わせて、覚えた揃えを動かす処理。揃えが
|
|
1463
|
-
* `fromPanePosition` で成り立っていたなら、`toPanePosition`
|
|
1506
|
+
* `fromPanePosition` で成り立っていたなら、`toPanePosition` で成り立つ。動くのは覚えた揃えだけ。位置 0 の一覧が守る上端は
|
|
1507
|
+
* 状態ではなく位置から決まる (`resolveItemsWrapperSnapEdge`・`canonicalAlignedEdge`) ため、位置 0 から離れる変化は端を
|
|
1508
|
+
* 運ばず、位置 0 以下へ運んだ揃えは消える。
|
|
1464
1509
|
*
|
|
1465
1510
|
* @param fromPanePosition - Pane position before the change / 変化の前のペイン位置
|
|
1466
1511
|
* @param toPanePosition - Pane position after the change / 変化の後のペイン位置
|
|
@@ -1861,7 +1906,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1861
1906
|
|
|
1862
1907
|
const totalHeight = fenwickTree.getTotal()
|
|
1863
1908
|
if (contentSize !== totalHeight) {
|
|
1864
|
-
|
|
1909
|
+
writeContentSize(totalHeight)
|
|
1865
1910
|
return
|
|
1866
1911
|
}
|
|
1867
1912
|
|
|
@@ -1933,7 +1978,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1933
1978
|
}
|
|
1934
1979
|
}
|
|
1935
1980
|
isResizingRef.current = false
|
|
1936
|
-
}, [contentSize, fenwickTree, itemCount, resolvedInsets.top, applySelfAdjustment, rememberAlignedEdge, updateScrollPositionImmediate, viewportSize, resolvedInsets.bottom])
|
|
1981
|
+
}, [contentSize, fenwickTree, itemCount, resolvedInsets.top, applySelfAdjustment, rememberAlignedEdge, updateScrollPositionImmediate, viewportSize, resolvedInsets.bottom, writeContentSize])
|
|
1937
1982
|
|
|
1938
1983
|
useEffect(() => {
|
|
1939
1984
|
// 目的: 上部インセットが動的に変更された場合、論理スクロール位置を維持したまま、ペインのスクロール位置を再計算してずらす。
|
|
@@ -1985,7 +2030,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1985
2030
|
|
|
1986
2031
|
const total = fenwickTree.update(safeIndex, size)
|
|
1987
2032
|
if (total !== undefined) {
|
|
1988
|
-
|
|
2033
|
+
writeContentSize(total)
|
|
1989
2034
|
}
|
|
1990
2035
|
Logger.debug("[VirtualScroll] Updated item size manually", { index: safeIndex, size, total })
|
|
1991
2036
|
|
|
@@ -2028,16 +2073,23 @@ const VirtualScrollInner = <T,>(
|
|
|
2028
2073
|
Logger.debug("[VirtualScroll] Adjusted scroll for layout shift (manual update)", { from: currentPanePosition, to: newPosition, causedByIndex: safeIndex, delta, activeVisibleStartIndex })
|
|
2029
2074
|
}
|
|
2030
2075
|
},
|
|
2031
|
-
[fenwickTree, itemCount, applySelfAdjustment, carryAlignedEdge, issueCompensationScroll, updateScrollPositionImmediate, contentInsets],
|
|
2076
|
+
[fenwickTree, itemCount, applySelfAdjustment, carryAlignedEdge, issueCompensationScroll, updateScrollPositionImmediate, contentInsets, writeContentSize],
|
|
2032
2077
|
)
|
|
2033
2078
|
|
|
2034
2079
|
/**
|
|
2035
2080
|
* Scrolls to the requested logical index: lands the pane exactly on the aligned position (clamped to the content),
|
|
2036
2081
|
* pins it as the pending alignment for drift correction, and remembers the alignment's edge at that position for
|
|
2037
|
-
* the device-pixel snap of the items wrapper (`resolveItemsWrapperSnapEdge
|
|
2082
|
+
* the device-pixel snap of the items wrapper (`resolveItemsWrapperSnapEdge`, in its canonical form). The content size
|
|
2083
|
+
* and the scroll position are written only when they differ from their synchronous copies (`writeContentSize`; for the
|
|
2084
|
+
* position, `latestScrollPositionRef` while the render loop is stopped, read before the pane moves), so a call that
|
|
2085
|
+
* leaves the list where it is (the same row, alignment and content) writes no state, schedules no render and reports
|
|
2086
|
+
* no position to `onScroll`, like `scrollTo` to the current position.
|
|
2038
2087
|
*
|
|
2039
2088
|
* 指定インデックスへのスクロールを実行する処理。ペインを揃えた位置 (中身の範囲へクランプ) へ厳密に着地させ、ドリフト補正の
|
|
2040
|
-
*
|
|
2089
|
+
* 保留中の揃えとして留め、行ラッパーの装置の画素への揃えのためにその位置で揃えの端を正規形で覚える
|
|
2090
|
+
* (`resolveItemsWrapperSnapEdge`)。中身の寸法とスクロール位置は同期の写し (`writeContentSize`。位置は描画ループが止まって
|
|
2091
|
+
* いる間の `latestScrollPositionRef` を、ペインを動かす前に読む) と違うときだけ書くため、一覧をその場に留める呼び出し (同じ行・揃え・中身) は状態を書かず、描画を予約せず、`onScroll` へ位置を
|
|
2092
|
+
* 知らせない (現在位置への `scrollTo` と同じ)。
|
|
2041
2093
|
*
|
|
2042
2094
|
* @param index - Item index / アイテムのインデックス
|
|
2043
2095
|
* @param options - Alignment (`"top"` by default) and offset / 揃え方 (既定は `"top"`) と offset
|
|
@@ -2099,6 +2151,10 @@ const VirtualScrollInner = <T,>(
|
|
|
2099
2151
|
const edge = edgeOfAlignment(options?.align)
|
|
2100
2152
|
rememberAlignedEdge(edge === null ? null : { edge, panePosition: clampedPaneOffset })
|
|
2101
2153
|
|
|
2154
|
+
// ❗ 判定はペインを動かす前に行う。ペインの scrollTo はスクロールの処理を同期で呼び、位置の写しを着地位置へ進めて
|
|
2155
|
+
// 描画ループを起こすが、状態はそのループの次のフレームまで古いまま。描画ループが止まっている間だけ、状態 (予約済みの
|
|
2156
|
+
// 値を含む) が位置の写しと一致する
|
|
2157
|
+
const stateHoldsLanding = !renderLoopRef.current.loopActive && latestScrollPositionRef.current === clampedPaneOffset
|
|
2102
2158
|
const currentPanePosition = scrollPaneRef.current?.getScrollPosition() ?? -1
|
|
2103
2159
|
// ❗ 揃えは厳密に着地させる。半ピクセル未満のずれを残すと、揃えた行の端の余白をそのずれが削り、行ラッパーを
|
|
2104
2160
|
// 揃えた端の側へ丸めても取り戻せない (覚えた揃えもペインの位置と一致せず効かない)
|
|
@@ -2120,12 +2176,15 @@ const VirtualScrollInner = <T,>(
|
|
|
2120
2176
|
scrollPaneRef.current?.scrollTo(clampedPaneOffset)
|
|
2121
2177
|
}
|
|
2122
2178
|
|
|
2123
|
-
|
|
2124
|
-
|
|
2179
|
+
writeContentSize(total)
|
|
2180
|
+
// 確定の直後は同じ値の setState でも React が描画を 1 回予約するため、状態が既に着地位置なら書かない
|
|
2181
|
+
if (!stateHoldsLanding) {
|
|
2182
|
+
updateScrollPositionImmediate(clampedPaneOffset, { immediate: true })
|
|
2183
|
+
}
|
|
2125
2184
|
|
|
2126
2185
|
Logger.debug("[VirtualScroll] Setting scroll position to:", clampedPaneOffset, { original: paneOffset, max: maxScrollPosition })
|
|
2127
2186
|
},
|
|
2128
|
-
[fenwickTree, overscanCount, itemCount, resolvedInsets.top, resolvedInsets.bottom, viewportSize, rememberAlignedEdge, updateScrollPositionImmediate],
|
|
2187
|
+
[fenwickTree, overscanCount, itemCount, resolvedInsets.top, resolvedInsets.bottom, viewportSize, rememberAlignedEdge, updateScrollPositionImmediate, writeContentSize],
|
|
2129
2188
|
)
|
|
2130
2189
|
|
|
2131
2190
|
// アンカー付きマウントが itemCount 0 で始まった場合の遅延適用 (一度きり)。
|
|
@@ -2652,7 +2711,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2652
2711
|
// contentSize は memo 本体では直接使わないが「反応辺」として依存に含める:
|
|
2653
2712
|
// fenwickTree は安定参照のため、非同期高さ更新 (updateItemSize / 高さ照合マイクロタスクの
|
|
2654
2713
|
// fenwickTree.updates) で木の prefix 和が変わってもこの memo は自動では失効しない。
|
|
2655
|
-
//
|
|
2714
|
+
// 両経路とも中身の寸法の書き込み (writeContentSize) を伴うため、contentSize を依存へ含めることで
|
|
2656
2715
|
// 行 top を最新の prefix 和で確実に再計算させる (報告座標と視覚描画の desync 防止)。
|
|
2657
2716
|
void contentSize
|
|
2658
2717
|
|
|
@@ -2763,7 +2822,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2763
2822
|
if (isUnmountedRef.current || typeof total !== "number") {
|
|
2764
2823
|
return
|
|
2765
2824
|
}
|
|
2766
|
-
|
|
2825
|
+
writeContentSize(total)
|
|
2767
2826
|
Logger.debug("[VirtualScroll] Updated heights for items", toUpdateHeights, "New total height:", total)
|
|
2768
2827
|
const panePosition = scrollPaneRef.current?.getScrollPosition() ?? latestScrollPositionRef.current
|
|
2769
2828
|
|
|
@@ -2807,6 +2866,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2807
2866
|
resolvedLabels,
|
|
2808
2867
|
updateScrollPositionImmediate,
|
|
2809
2868
|
visibleStartIndex,
|
|
2869
|
+
writeContentSize,
|
|
2810
2870
|
])
|
|
2811
2871
|
|
|
2812
2872
|
/**
|