@aiquants/virtualscroll 3.9.0 → 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 +37 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1907 -1903
- package/package.json +1 -1
- package/src/VirtualScroll.tsx +89 -20
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,30 +1446,66 @@ 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
|
-
|
|
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))
|
|
1433
1452
|
// 状態の同期の写し。手動スクロールのたびに状態へ null を書くと、同じ値でも React が描画を予約し得るため、写しで要否を決める
|
|
1434
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
|
+
}, [])
|
|
1435
1474
|
|
|
1436
1475
|
/**
|
|
1437
|
-
* Remembers the alignment that holds at a pane position, or forgets it with `null`:
|
|
1438
|
-
*
|
|
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.
|
|
1439
1480
|
*
|
|
1440
|
-
* あるペイン位置で成り立つ揃えを覚える処理 (`null` で忘れる)
|
|
1481
|
+
* あるペイン位置で成り立つ揃えを覚える処理 (`null` で忘れる)。揃えを正規形にし (`canonicalAlignedEdge`。位置 0 の揃えは
|
|
1482
|
+
* 揃え無しと同じ)、行ラッパーが揃えに使う状態と、その同期の写しへ、正規形が覚えた揃えと辺かペイン位置で違うときだけ書く。
|
|
1441
1483
|
*
|
|
1442
|
-
* @param
|
|
1484
|
+
* @param alignment - The alignment, or `null` / 揃え (無ければ `null`)
|
|
1443
1485
|
*/
|
|
1444
|
-
const rememberAlignedEdge = useCallback((
|
|
1486
|
+
const rememberAlignedEdge = useCallback((alignment: AlignedEdge | null): void => {
|
|
1487
|
+
const current = alignedEdgeRef.current
|
|
1488
|
+
const next = canonicalAlignedEdge(alignment)
|
|
1489
|
+
// 同じ揃えでも新しいオブジェクトを書けば React は描画を予約する。同じ位置への scrollToIndex (ホストが打鍵ごとに
|
|
1490
|
+
// 揃え直すなど) のたびに確定が 1 回増えるため、辺と位置が同じなら書かない
|
|
1491
|
+
if (current === next || (current !== null && next !== null && current.edge === next.edge && current.panePosition === next.panePosition)) {
|
|
1492
|
+
return
|
|
1493
|
+
}
|
|
1445
1494
|
alignedEdgeRef.current = next
|
|
1446
1495
|
setAlignedEdge(next)
|
|
1447
1496
|
}, [])
|
|
1448
1497
|
|
|
1449
1498
|
/**
|
|
1450
1499
|
* Moves the remembered alignment along with a position change that keeps the aligned row in place (layout-shift
|
|
1451
|
-
* 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.
|
|
1452
1504
|
*
|
|
1453
1505
|
* 揃えた行をその場に留める位置の変化 (レイアウトシフトの補正) に合わせて、覚えた揃えを動かす処理。揃えが
|
|
1454
|
-
* `fromPanePosition` で成り立っていたなら、`toPanePosition`
|
|
1506
|
+
* `fromPanePosition` で成り立っていたなら、`toPanePosition` で成り立つ。動くのは覚えた揃えだけ。位置 0 の一覧が守る上端は
|
|
1507
|
+
* 状態ではなく位置から決まる (`resolveItemsWrapperSnapEdge`・`canonicalAlignedEdge`) ため、位置 0 から離れる変化は端を
|
|
1508
|
+
* 運ばず、位置 0 以下へ運んだ揃えは消える。
|
|
1455
1509
|
*
|
|
1456
1510
|
* @param fromPanePosition - Pane position before the change / 変化の前のペイン位置
|
|
1457
1511
|
* @param toPanePosition - Pane position after the change / 変化の後のペイン位置
|
|
@@ -1852,7 +1906,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1852
1906
|
|
|
1853
1907
|
const totalHeight = fenwickTree.getTotal()
|
|
1854
1908
|
if (contentSize !== totalHeight) {
|
|
1855
|
-
|
|
1909
|
+
writeContentSize(totalHeight)
|
|
1856
1910
|
return
|
|
1857
1911
|
}
|
|
1858
1912
|
|
|
@@ -1924,7 +1978,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1924
1978
|
}
|
|
1925
1979
|
}
|
|
1926
1980
|
isResizingRef.current = false
|
|
1927
|
-
}, [contentSize, fenwickTree, itemCount, resolvedInsets.top, applySelfAdjustment, rememberAlignedEdge, updateScrollPositionImmediate, viewportSize, resolvedInsets.bottom])
|
|
1981
|
+
}, [contentSize, fenwickTree, itemCount, resolvedInsets.top, applySelfAdjustment, rememberAlignedEdge, updateScrollPositionImmediate, viewportSize, resolvedInsets.bottom, writeContentSize])
|
|
1928
1982
|
|
|
1929
1983
|
useEffect(() => {
|
|
1930
1984
|
// 目的: 上部インセットが動的に変更された場合、論理スクロール位置を維持したまま、ペインのスクロール位置を再計算してずらす。
|
|
@@ -1976,7 +2030,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1976
2030
|
|
|
1977
2031
|
const total = fenwickTree.update(safeIndex, size)
|
|
1978
2032
|
if (total !== undefined) {
|
|
1979
|
-
|
|
2033
|
+
writeContentSize(total)
|
|
1980
2034
|
}
|
|
1981
2035
|
Logger.debug("[VirtualScroll] Updated item size manually", { index: safeIndex, size, total })
|
|
1982
2036
|
|
|
@@ -2019,16 +2073,23 @@ const VirtualScrollInner = <T,>(
|
|
|
2019
2073
|
Logger.debug("[VirtualScroll] Adjusted scroll for layout shift (manual update)", { from: currentPanePosition, to: newPosition, causedByIndex: safeIndex, delta, activeVisibleStartIndex })
|
|
2020
2074
|
}
|
|
2021
2075
|
},
|
|
2022
|
-
[fenwickTree, itemCount, applySelfAdjustment, carryAlignedEdge, issueCompensationScroll, updateScrollPositionImmediate, contentInsets],
|
|
2076
|
+
[fenwickTree, itemCount, applySelfAdjustment, carryAlignedEdge, issueCompensationScroll, updateScrollPositionImmediate, contentInsets, writeContentSize],
|
|
2023
2077
|
)
|
|
2024
2078
|
|
|
2025
2079
|
/**
|
|
2026
2080
|
* Scrolls to the requested logical index: lands the pane exactly on the aligned position (clamped to the content),
|
|
2027
2081
|
* pins it as the pending alignment for drift correction, and remembers the alignment's edge at that position for
|
|
2028
|
-
* 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.
|
|
2029
2087
|
*
|
|
2030
2088
|
* 指定インデックスへのスクロールを実行する処理。ペインを揃えた位置 (中身の範囲へクランプ) へ厳密に着地させ、ドリフト補正の
|
|
2031
|
-
*
|
|
2089
|
+
* 保留中の揃えとして留め、行ラッパーの装置の画素への揃えのためにその位置で揃えの端を正規形で覚える
|
|
2090
|
+
* (`resolveItemsWrapperSnapEdge`)。中身の寸法とスクロール位置は同期の写し (`writeContentSize`。位置は描画ループが止まって
|
|
2091
|
+
* いる間の `latestScrollPositionRef` を、ペインを動かす前に読む) と違うときだけ書くため、一覧をその場に留める呼び出し (同じ行・揃え・中身) は状態を書かず、描画を予約せず、`onScroll` へ位置を
|
|
2092
|
+
* 知らせない (現在位置への `scrollTo` と同じ)。
|
|
2032
2093
|
*
|
|
2033
2094
|
* @param index - Item index / アイテムのインデックス
|
|
2034
2095
|
* @param options - Alignment (`"top"` by default) and offset / 揃え方 (既定は `"top"`) と offset
|
|
@@ -2090,6 +2151,10 @@ const VirtualScrollInner = <T,>(
|
|
|
2090
2151
|
const edge = edgeOfAlignment(options?.align)
|
|
2091
2152
|
rememberAlignedEdge(edge === null ? null : { edge, panePosition: clampedPaneOffset })
|
|
2092
2153
|
|
|
2154
|
+
// ❗ 判定はペインを動かす前に行う。ペインの scrollTo はスクロールの処理を同期で呼び、位置の写しを着地位置へ進めて
|
|
2155
|
+
// 描画ループを起こすが、状態はそのループの次のフレームまで古いまま。描画ループが止まっている間だけ、状態 (予約済みの
|
|
2156
|
+
// 値を含む) が位置の写しと一致する
|
|
2157
|
+
const stateHoldsLanding = !renderLoopRef.current.loopActive && latestScrollPositionRef.current === clampedPaneOffset
|
|
2093
2158
|
const currentPanePosition = scrollPaneRef.current?.getScrollPosition() ?? -1
|
|
2094
2159
|
// ❗ 揃えは厳密に着地させる。半ピクセル未満のずれを残すと、揃えた行の端の余白をそのずれが削り、行ラッパーを
|
|
2095
2160
|
// 揃えた端の側へ丸めても取り戻せない (覚えた揃えもペインの位置と一致せず効かない)
|
|
@@ -2111,12 +2176,15 @@ const VirtualScrollInner = <T,>(
|
|
|
2111
2176
|
scrollPaneRef.current?.scrollTo(clampedPaneOffset)
|
|
2112
2177
|
}
|
|
2113
2178
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2179
|
+
writeContentSize(total)
|
|
2180
|
+
// 確定の直後は同じ値の setState でも React が描画を 1 回予約するため、状態が既に着地位置なら書かない
|
|
2181
|
+
if (!stateHoldsLanding) {
|
|
2182
|
+
updateScrollPositionImmediate(clampedPaneOffset, { immediate: true })
|
|
2183
|
+
}
|
|
2116
2184
|
|
|
2117
2185
|
Logger.debug("[VirtualScroll] Setting scroll position to:", clampedPaneOffset, { original: paneOffset, max: maxScrollPosition })
|
|
2118
2186
|
},
|
|
2119
|
-
[fenwickTree, overscanCount, itemCount, resolvedInsets.top, resolvedInsets.bottom, viewportSize, rememberAlignedEdge, updateScrollPositionImmediate],
|
|
2187
|
+
[fenwickTree, overscanCount, itemCount, resolvedInsets.top, resolvedInsets.bottom, viewportSize, rememberAlignedEdge, updateScrollPositionImmediate, writeContentSize],
|
|
2120
2188
|
)
|
|
2121
2189
|
|
|
2122
2190
|
// アンカー付きマウントが itemCount 0 で始まった場合の遅延適用 (一度きり)。
|
|
@@ -2643,7 +2711,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2643
2711
|
// contentSize は memo 本体では直接使わないが「反応辺」として依存に含める:
|
|
2644
2712
|
// fenwickTree は安定参照のため、非同期高さ更新 (updateItemSize / 高さ照合マイクロタスクの
|
|
2645
2713
|
// fenwickTree.updates) で木の prefix 和が変わってもこの memo は自動では失効しない。
|
|
2646
|
-
//
|
|
2714
|
+
// 両経路とも中身の寸法の書き込み (writeContentSize) を伴うため、contentSize を依存へ含めることで
|
|
2647
2715
|
// 行 top を最新の prefix 和で確実に再計算させる (報告座標と視覚描画の desync 防止)。
|
|
2648
2716
|
void contentSize
|
|
2649
2717
|
|
|
@@ -2754,7 +2822,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2754
2822
|
if (isUnmountedRef.current || typeof total !== "number") {
|
|
2755
2823
|
return
|
|
2756
2824
|
}
|
|
2757
|
-
|
|
2825
|
+
writeContentSize(total)
|
|
2758
2826
|
Logger.debug("[VirtualScroll] Updated heights for items", toUpdateHeights, "New total height:", total)
|
|
2759
2827
|
const panePosition = scrollPaneRef.current?.getScrollPosition() ?? latestScrollPositionRef.current
|
|
2760
2828
|
|
|
@@ -2798,6 +2866,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2798
2866
|
resolvedLabels,
|
|
2799
2867
|
updateScrollPositionImmediate,
|
|
2800
2868
|
visibleStartIndex,
|
|
2869
|
+
writeContentSize,
|
|
2801
2870
|
])
|
|
2802
2871
|
|
|
2803
2872
|
/**
|