@aiquants/virtualscroll 1.24.0 → 2.0.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 +73 -1
- package/README.md +17 -5
- package/dist/VirtualScroll.d.cts +53 -4
- package/dist/VirtualScroll.d.ts +53 -4
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1327 -1299
- package/package.json +1 -1
- package/src/VirtualScroll.tsx +149 -13
package/package.json
CHANGED
package/src/VirtualScroll.tsx
CHANGED
|
@@ -25,11 +25,35 @@ export type VirtualScrollRange = {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Imperative handle of VirtualScroll. Every position it accepts or returns is in the
|
|
29
|
+
* LOGICAL coordinate space (content px, insets excluded) — the same space as onScroll /
|
|
30
|
+
* onRangeChange / initialScrollOffset / getScrollAnchor.
|
|
31
|
+
* VirtualScroll の命令ハンドル。受け取る位置・返す位置は**すべて論理座標** (inset 抜きの
|
|
32
|
+
* コンテンツ px) で、onScroll / onRangeChange / initialScrollOffset / getScrollAnchor と同一空間。
|
|
29
33
|
*
|
|
30
|
-
*
|
|
34
|
+
* ❗ ペイン座標 (inset 込み) は内部実装詳細であり、このハンドルからは一切露出しない。
|
|
35
|
+
* 2.0.0 以前は getScrollPosition と scrollTo の返値だけがペイン座標という非対称があり、
|
|
36
|
+
* 消費者側の換算コードが 2 度の実害バグ (daily-report 初期 6px ずれ / directory-tree の
|
|
37
|
+
* inset 二重適用) を生んだ。座標系の混在を消費者から構造的に不能にするための統一である。
|
|
38
|
+
* ScrollPane 自身のハンドル (ScrollPaneHandle) はペインそのものなのでペイン座標のままであり、
|
|
39
|
+
* その境界はこの型が引き受ける。
|
|
31
40
|
*/
|
|
32
|
-
export type VirtualScrollHandle =
|
|
41
|
+
export type VirtualScrollHandle = {
|
|
42
|
+
/**
|
|
43
|
+
* Scrolls to a LOGICAL position (updater form receives the current logical position).
|
|
44
|
+
* Returns the applied (clamped) LOGICAL position.
|
|
45
|
+
* 論理位置へスクロール (updater は現在の論理位置を受け取る)。適用後 (クランプ後) の論理位置を返す。
|
|
46
|
+
*/
|
|
47
|
+
scrollTo: (position: number | ((prev: number) => number)) => number
|
|
48
|
+
/**
|
|
49
|
+
* Current LOGICAL scroll position. -1 when the pane is not connected.
|
|
50
|
+
* 現在の論理スクロール位置。ペイン未接続時は -1。
|
|
51
|
+
*/
|
|
52
|
+
getScrollPosition: () => number
|
|
53
|
+
/** Total pane content size including insets (a size, not a position) / インセット込みのコンテンツ総高さ (位置ではなくサイズ)。未接続時は -1 */
|
|
54
|
+
getContentSize: () => number
|
|
55
|
+
/** Viewport size / ビューポートの高さ。未接続時は -1 */
|
|
56
|
+
getViewportSize: () => number
|
|
33
57
|
/** Scrolls to a specific item index / 指定したアイテムインデックスへスクロール */
|
|
34
58
|
scrollToIndex: (index: number, options?: { align?: "top" | "bottom" | "center"; offset?: number }) => void
|
|
35
59
|
/** Gets the total height managed by the Fenwick Tree / Fenwick Tree で管理されている総高さを取得 */
|
|
@@ -40,6 +64,12 @@ export type VirtualScrollHandle = ScrollPaneHandle & {
|
|
|
40
64
|
focusItemAtIndex: (index: number, options?: { ensureVisible?: boolean }) => void
|
|
41
65
|
/** Gets the current scroll range information / 現在のスクロール範囲情報を取得 */
|
|
42
66
|
getRange: () => VirtualScrollRange
|
|
67
|
+
/**
|
|
68
|
+
* Captures the current top-row anchor ({index, offsetPx}) for exact restore via initialScrollAnchor.
|
|
69
|
+
* initialScrollAnchor での厳密復元用に、現在の先頭可視行アンカー ({index, offsetPx}) を取得。
|
|
70
|
+
* itemCount 0 のときは null。offsetPx は先頭可視行の上端がビューポート上端より上に隠れている px (論理座標)。
|
|
71
|
+
*/
|
|
72
|
+
getScrollAnchor: () => { index: number; offsetPx: number } | null
|
|
43
73
|
/**
|
|
44
74
|
* Manually updates the size of a specific item. Contract: after calling this,
|
|
45
75
|
* `getItemHeight(index)` must return the same `size`; `getItemHeight` is the source of truth,
|
|
@@ -132,6 +162,19 @@ export type VirtualScrollProps<T> = {
|
|
|
132
162
|
children: (item: T, index: number) => ReactNode
|
|
133
163
|
initialScrollIndex?: number
|
|
134
164
|
initialScrollOffset?: number
|
|
165
|
+
/**
|
|
166
|
+
* Anchor-based initial position: starts with row `index` scrolled `offsetPx` px past the viewport top.
|
|
167
|
+
* アンカー基準の初期位置。行 index の上端がビューポート上端より offsetPx px 分だけ上に隠れた位置で開始する。
|
|
168
|
+
*
|
|
169
|
+
* 可変高さ一覧の「位置キープ」はこれを使うこと。initialScrollOffset の生 px 復元は、実測高さが
|
|
170
|
+
* 具現化されていない再マウント空間では px→行変換が推定値で行われ、別の行に着地する
|
|
171
|
+
* (実測: 深い復元で 21 行漂着)。アンカーは行の同一性で位置決めするため空間差に不変。
|
|
172
|
+
* 保存側は VirtualScrollHandle.getScrollAnchor() で取得した値をそのまま渡す。
|
|
173
|
+
* 優先順位: initialScrollAnchor > initialScrollIndex > initialScrollOffset。
|
|
174
|
+
* マウント時に itemCount > 0 なら初回コミットから厳密。itemCount 0 でマウントした場合は
|
|
175
|
+
* 最初のデータ到着 (0 → N) 時に scrollToIndex 経由で遅延適用される (近似 → 自己修復)。
|
|
176
|
+
*/
|
|
177
|
+
initialScrollAnchor?: { index: number; offsetPx?: number }
|
|
135
178
|
callbackThrottleMs?: number
|
|
136
179
|
contentInsets?: ScrollPaneProps["contentInsets"]
|
|
137
180
|
onItemFocus?: (index: number) => void
|
|
@@ -703,6 +746,7 @@ const VirtualScrollInner = <T,>(
|
|
|
703
746
|
background,
|
|
704
747
|
initialScrollIndex,
|
|
705
748
|
initialScrollOffset,
|
|
749
|
+
initialScrollAnchor,
|
|
706
750
|
callbackThrottleMs = 5,
|
|
707
751
|
contentInsets,
|
|
708
752
|
onItemFocus,
|
|
@@ -717,7 +761,9 @@ const VirtualScrollInner = <T,>(
|
|
|
717
761
|
|
|
718
762
|
const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, wheelSpeedMultiplier, inertiaOptions, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
|
|
719
763
|
|
|
720
|
-
|
|
764
|
+
// ❗ 内部ペイン ref の型は ScrollPaneHandle (ペイン座標)。VirtualScrollHandle と誤記すると
|
|
765
|
+
// 2.0.0 以降は型ドキュメントが「論理座標を返す」と嘘をつく (構造的互換で型検査は通ってしまう)
|
|
766
|
+
const scrollPaneRef = useRef<ScrollPaneHandle>(null)
|
|
721
767
|
const currentRangeRef = useRef<VirtualScrollRange>({
|
|
722
768
|
renderingStartIndex: 0,
|
|
723
769
|
renderingEndIndex: 0,
|
|
@@ -726,18 +772,32 @@ const VirtualScrollInner = <T,>(
|
|
|
726
772
|
scrollPosition: 0,
|
|
727
773
|
totalHeight: 0,
|
|
728
774
|
})
|
|
775
|
+
// アンカー付きマウントは保留アンカーを最初から種まきする: マウント後の実測反映 (contentSize
|
|
776
|
+
// コミット) のたびにドリフト補正 effect が同じ行へ再ピン留めし、自己修復する。ユーザーの
|
|
777
|
+
// 手動スクロールで解除される (handleScroll 参照)。offset は scrollToIndex と同じ「減算」規約の
|
|
778
|
+
// ため符号を反転して持つ (offsetPx = 行上端がビューポート上端より上に隠れる px、正の値)
|
|
729
779
|
const pendingVisibleStartIndexRef = useRef<{
|
|
730
780
|
index: number
|
|
731
781
|
align?: "top" | "bottom" | "center"
|
|
732
782
|
offset?: number
|
|
733
|
-
} | null>(null)
|
|
783
|
+
} | null>(initialScrollAnchor && itemCount > 0 ? { index: minmax(Math.trunc(initialScrollAnchor.index), 0, itemCount - 1), align: "top", offset: -Math.max(0, initialScrollAnchor.offsetPx ?? 0) } : null)
|
|
734
784
|
// 「アンマウント済みか」を追跡する (初期 false)。「マウント済みか」の正ガードだと、初回レンダー中に
|
|
735
785
|
// 予約された高さ照合マイクロタスクが passive effect (マウントフラグ設定) より先に実行されて破棄される。
|
|
736
786
|
const isUnmountedRef = useRef(false)
|
|
737
787
|
|
|
788
|
+
// 初回実行スキップ用 (マウント時に走る effect 本体が、直前で種まきした初期アンカーを消さないため)
|
|
789
|
+
const isFirstGetItemHeightRunRef = useRef(true)
|
|
738
790
|
useEffect(() => {
|
|
739
791
|
// 目的: アイテムの高さ取得ロジックがリセットされた場合に、古いインデックスへのアンカーを解除する。
|
|
740
792
|
// 依存関係: resetOnGetItemHeightChange, getItemHeight
|
|
793
|
+
// ❗ 初回 (マウント) は解除しない: このフラグの意味は「getItemHeight の**変更**時に解除」で
|
|
794
|
+
// あり、マウント時の実行で解除すると initialScrollAnchor が種まきした保留アンカー
|
|
795
|
+
// (実測反映ごとの自己修復再ピン留め) が resetOnGetItemHeightChange: true の消費者で
|
|
796
|
+
// 無言に無効化されてしまう
|
|
797
|
+
if (isFirstGetItemHeightRunRef.current) {
|
|
798
|
+
isFirstGetItemHeightRunRef.current = false
|
|
799
|
+
return
|
|
800
|
+
}
|
|
741
801
|
if (resetOnGetItemHeightChange) {
|
|
742
802
|
// When configured to reset on logic change, we drop the scroll anchor
|
|
743
803
|
// to avoid sticking to an index that may no longer be relevant contextually.
|
|
@@ -756,7 +816,9 @@ const VirtualScrollInner = <T,>(
|
|
|
756
816
|
// (sampleRangeChanged) が破壊的な tree.reset を発火し、実測済みの全行高さが破棄されてしまう。
|
|
757
817
|
const [initialSampleRange] = useState(() => {
|
|
758
818
|
const SAMPLE_HALF_WINDOW = 50
|
|
759
|
-
|
|
819
|
+
// アンカー復元はアンカー行を、index 復元はその行を窓の中心にする (どちらも「最初に見る領域」)
|
|
820
|
+
const anchorSource = initialScrollAnchor?.index ?? initialScrollIndex
|
|
821
|
+
const anchor = typeof anchorSource === "number" && Number.isFinite(anchorSource) ? Math.max(0, Math.trunc(anchorSource)) : 0
|
|
760
822
|
return { from: Math.max(0, anchor - SAMPLE_HALF_WINDOW), to: anchor + SAMPLE_HALF_WINDOW }
|
|
761
823
|
})
|
|
762
824
|
|
|
@@ -777,7 +839,20 @@ const VirtualScrollInner = <T,>(
|
|
|
777
839
|
const [initialValues] = useState(() => {
|
|
778
840
|
let position = 0
|
|
779
841
|
let total = 0
|
|
780
|
-
if (
|
|
842
|
+
if (initialScrollAnchor && itemCount > 0) {
|
|
843
|
+
// アンカー復元: アンカー行の周辺を具現化し、その行の上端 + offsetPx を初期位置にする。
|
|
844
|
+
// 初回描画のレンジ計算も同じツリー状態から導かれるため、推定誤差の量に関係なく
|
|
845
|
+
// 「アンカー行がちょうど offsetPx 分だけ上に隠れた」画で最初のフレームから出る
|
|
846
|
+
const safeIndex = minmax(Math.trunc(initialScrollAnchor.index), 0, itemCount - 1)
|
|
847
|
+
const anchorOffsetPx = Math.max(0, initialScrollAnchor.offsetPx ?? 0)
|
|
848
|
+
const safeIndexFrom = minmax(safeIndex - overscanCount * 2, 0, itemCount - 1)
|
|
849
|
+
const safeIndexTo = minmax(safeIndex + overscanCount * 2, 0, itemCount - 1)
|
|
850
|
+
const options = safeIndex > 0 || anchorOffsetPx > 0 ? { materializeOption: { materialize: true, ranges: [{ from: safeIndexFrom, to: safeIndexTo }] } } : undefined
|
|
851
|
+
const { cumulative, total: materializedTotal, currentValue } = fenwickTree.prefixSum(safeIndex, options)
|
|
852
|
+
const logicalOffset = Math.max(cumulative - currentValue + anchorOffsetPx, 0)
|
|
853
|
+
position = toPanePositionWithInset(logicalOffset, resolvedInsets.top)
|
|
854
|
+
total = materializedTotal ?? fenwickTree.getTotal()
|
|
855
|
+
} else if (typeof initialScrollIndex === "number") {
|
|
781
856
|
const safeIndex = minmax(initialScrollIndex, 0, itemCount - 1)
|
|
782
857
|
const safeIndexFrom = minmax(safeIndex - overscanCount * 2, 0, itemCount - 1)
|
|
783
858
|
const safeIndexTo = minmax(safeIndex + overscanCount * 2, 0, itemCount - 1)
|
|
@@ -1068,7 +1143,10 @@ const VirtualScrollInner = <T,>(
|
|
|
1068
1143
|
didApplyInitialOffsetRef.current = true
|
|
1069
1144
|
// 目的: 初期オフセットを一度だけ適用し (同期処理)、ペインと論理位置を同期させる。
|
|
1070
1145
|
// 依存関係: initialScrollOffset (初回のみ実行されるガード節あり)
|
|
1071
|
-
|
|
1146
|
+
// ❗ initialScrollOffset の適用はそれが「最優先の初期位置指定」である場合のみ。
|
|
1147
|
+
// アンカー/インデックス指定と併用されたとき、この passive effect (描画後) が offset で
|
|
1148
|
+
// 上書きすると「初回描画は index 位置 → 1 コミット後に offset 位置へ跳ぶ」踏み潰しになる
|
|
1149
|
+
if (initialScrollAnchor == null && typeof initialScrollIndex !== "number" && typeof initialScrollOffset === "number") {
|
|
1072
1150
|
const paneOffset = toPanePositionWithInset(Math.max(initialScrollOffset, 0), resolvedInsets.top)
|
|
1073
1151
|
const needsPaneSync = Math.abs(paneOffset - latestScrollPositionRef.current) > 0.5
|
|
1074
1152
|
updateScrollPositionImmediate(paneOffset, { immediate: true })
|
|
@@ -1078,7 +1156,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1078
1156
|
} else {
|
|
1079
1157
|
updateScrollPositionImmediate(latestScrollPositionRef.current, { immediate: true })
|
|
1080
1158
|
}
|
|
1081
|
-
}, [initialScrollOffset, resolvedInsets.top, updateScrollPositionImmediate])
|
|
1159
|
+
}, [initialScrollAnchor, initialScrollIndex, initialScrollOffset, resolvedInsets.top, updateScrollPositionImmediate])
|
|
1082
1160
|
|
|
1083
1161
|
useEffect(() => {
|
|
1084
1162
|
// itemCount triggers recalculation of contentSize
|
|
@@ -1162,10 +1240,18 @@ const VirtualScrollInner = <T,>(
|
|
|
1162
1240
|
// 依存関係: resolvedInsets.top
|
|
1163
1241
|
const previousTop = previousTopInsetRef.current
|
|
1164
1242
|
if (previousTop === resolvedInsets.top) return
|
|
1243
|
+
previousTopInsetRef.current = resolvedInsets.top
|
|
1244
|
+
|
|
1245
|
+
// ❗ 保留アンカー (scrollToIndex / initialScrollAnchor 由来) が居る間は位置決めをドリフト補正
|
|
1246
|
+
// effect へ委譲する。ここでも相対シフトすると、アンカー基準の絶対位置決め (新 top で再計算済み)
|
|
1247
|
+
// と二重適用になり、論理位置が inset 差分ぶんズレる (2.0.0 の「論理位置は inset 非依存」契約の
|
|
1248
|
+
// 破れとして動的 inset ピンが検知した実バグ。previousTopInsetRef の更新だけ行う)
|
|
1249
|
+
if (pendingVisibleStartIndexRef.current) {
|
|
1250
|
+
return
|
|
1251
|
+
}
|
|
1165
1252
|
|
|
1166
1253
|
const logicalPosition = toLogicalPositionWithInset(latestScrollPositionRef.current, previousTop)
|
|
1167
1254
|
const panePosition = toPanePositionWithInset(logicalPosition, resolvedInsets.top)
|
|
1168
|
-
previousTopInsetRef.current = resolvedInsets.top
|
|
1169
1255
|
latestScrollPositionRef.current = panePosition
|
|
1170
1256
|
scrollPaneRef.current?.scrollTo(panePosition)
|
|
1171
1257
|
updateScrollPositionImmediate(panePosition, { immediate: true })
|
|
@@ -1322,6 +1408,21 @@ const VirtualScrollInner = <T,>(
|
|
|
1322
1408
|
[fenwickTree, overscanCount, itemCount, resolvedInsets.top, resolvedInsets.bottom, viewportSize, updateScrollPositionImmediate],
|
|
1323
1409
|
)
|
|
1324
1410
|
|
|
1411
|
+
// アンカー付きマウントが itemCount 0 で始まった場合の遅延適用 (一度きり)。
|
|
1412
|
+
// 初期値経路 (useState) はマウント時データが前提のため、データ後着の消費者では
|
|
1413
|
+
// アンカーが無言に落ちる。最初の 0 → N 遷移で scrollToIndex 経由で適用する
|
|
1414
|
+
// (アンカー近傍の具現化 + 保留アンカー設定を再利用。初回コミット厳密性はデータが
|
|
1415
|
+
// ある状態でのマウントのみで保証され、遅延適用は近似 → ドリフト補正で自己修復)
|
|
1416
|
+
const deferredInitialAnchorRef = useRef(initialScrollAnchor && itemCount === 0 ? initialScrollAnchor : null)
|
|
1417
|
+
useEffect(() => {
|
|
1418
|
+
const deferred = deferredInitialAnchorRef.current
|
|
1419
|
+
if (!deferred || itemCount === 0) {
|
|
1420
|
+
return
|
|
1421
|
+
}
|
|
1422
|
+
deferredInitialAnchorRef.current = null
|
|
1423
|
+
scrollToIndex(minmax(Math.trunc(deferred.index), 0, itemCount - 1), { align: "top", offset: -Math.max(0, deferred.offsetPx ?? 0) })
|
|
1424
|
+
}, [itemCount, scrollToIndex])
|
|
1425
|
+
|
|
1325
1426
|
/**
|
|
1326
1427
|
* Scrolls to a raw offset while resolving to an index.
|
|
1327
1428
|
*
|
|
@@ -1346,6 +1447,33 @@ const VirtualScrollInner = <T,>(
|
|
|
1346
1447
|
[fenwickTree, itemCount, scrollToIndex],
|
|
1347
1448
|
)
|
|
1348
1449
|
|
|
1450
|
+
/**
|
|
1451
|
+
* Captures the current top-row anchor for exact position restore across remounts.
|
|
1452
|
+
* 再マウント越しの厳密な位置復元のために、現在の先頭可視行アンカーを取得する処理。
|
|
1453
|
+
*
|
|
1454
|
+
* offsetPx は「先頭可視行の上端がビューポート上端より上に隠れている px」(正の値、論理座標)。
|
|
1455
|
+
* 復元は initialScrollAnchor へそのまま渡す。可視ウィンドウの行高さは描画のたびに実測が
|
|
1456
|
+
* ツリーへ照合されるため、保存時点のアンカーは常に正確で、生 px と違い再マウント後の
|
|
1457
|
+
* 推定空間の違いに対して不変 (px 復元は深い位置で別の行に着地する)。
|
|
1458
|
+
*/
|
|
1459
|
+
const getScrollAnchor = useCallback((): { index: number; offsetPx: number } | null => {
|
|
1460
|
+
if (itemCount === 0) {
|
|
1461
|
+
return null
|
|
1462
|
+
}
|
|
1463
|
+
const logical = toLogicalPositionWithInset(latestScrollPositionRef.current, resolvedInsets.top)
|
|
1464
|
+
const { index, cumulative, currentValue } = fenwickTree.findIndexAtOrAfter(logical, { materializeOption: { materialize: false } })
|
|
1465
|
+
if (index === -1) {
|
|
1466
|
+
// 末尾越え (推定総高さより深い位置) は最終行アンカーへ丸める
|
|
1467
|
+
return { index: itemCount - 1, offsetPx: 0 }
|
|
1468
|
+
}
|
|
1469
|
+
if (cumulative === logical) {
|
|
1470
|
+
// 行の下端がちょうどビューポート上端 = 可視先頭は次の行 (可視域計算と同じ境界規約)
|
|
1471
|
+
return { index: minmax(index + 1, 0, itemCount - 1), offsetPx: 0 }
|
|
1472
|
+
}
|
|
1473
|
+
const itemTop = (cumulative ?? 0) - (currentValue ?? 0)
|
|
1474
|
+
return { index, offsetPx: Math.max(0, logical - itemTop) }
|
|
1475
|
+
}, [fenwickTree, itemCount, resolvedInsets.top])
|
|
1476
|
+
|
|
1349
1477
|
/**
|
|
1350
1478
|
* Imperative scroll entry supporting updater functions.
|
|
1351
1479
|
*
|
|
@@ -1359,7 +1487,9 @@ const VirtualScrollInner = <T,>(
|
|
|
1359
1487
|
const panePosition = scrollPaneRef.current?.getScrollPosition()
|
|
1360
1488
|
const effectivePosition = typeof panePosition === "number" ? panePosition : latestScrollPositionRef.current
|
|
1361
1489
|
updateScrollPositionImmediate(effectivePosition)
|
|
1362
|
-
|
|
1490
|
+
// ❗ 返値も論理座標 (2.0.0)。ペイン座標を返すと「入力は論理・返値はペイン」の非対称が
|
|
1491
|
+
// 復活し、返値をミラーへ書き戻す消費者が inset 分ずれる
|
|
1492
|
+
return toLogicalPositionWithInset(effectivePosition, resolvedInsetsTopRef.current)
|
|
1363
1493
|
},
|
|
1364
1494
|
[resolvedInsets.top, scrollTo, updateScrollPositionImmediate],
|
|
1365
1495
|
)
|
|
@@ -1887,7 +2017,12 @@ const VirtualScrollInner = <T,>(
|
|
|
1887
2017
|
useImperativeHandle(
|
|
1888
2018
|
ref,
|
|
1889
2019
|
() => ({
|
|
1890
|
-
getScrollPosition: () =>
|
|
2020
|
+
getScrollPosition: () => {
|
|
2021
|
+
// ❗ 論理座標で返す (2.0.0)。ペイン座標を返す実装へ戻さないこと: 消費者の換算コードが
|
|
2022
|
+
// 座標混在バグの温床になる (この統一が本質修正)。inset は走行中も最新を ref から読む
|
|
2023
|
+
const panePosition = scrollPaneRef.current?.getScrollPosition()
|
|
2024
|
+
return typeof panePosition === "number" ? toLogicalPositionWithInset(panePosition, resolvedInsetsTopRef.current) : -1
|
|
2025
|
+
},
|
|
1891
2026
|
getContentSize: () => scrollPaneRef.current?.getContentSize() ?? -1,
|
|
1892
2027
|
getViewportSize: () => scrollPaneRef.current?.getViewportSize() ?? -1,
|
|
1893
2028
|
scrollTo: scrollToHandle,
|
|
@@ -1896,9 +2031,10 @@ const VirtualScrollInner = <T,>(
|
|
|
1896
2031
|
getFenwickSize: () => fenwickTree.getSize(),
|
|
1897
2032
|
focusItemAtIndex,
|
|
1898
2033
|
getRange: () => currentRangeRef.current,
|
|
2034
|
+
getScrollAnchor,
|
|
1899
2035
|
updateItemSize,
|
|
1900
2036
|
}),
|
|
1901
|
-
[scrollToHandle, scrollToIndex, fenwickTree, focusItemAtIndex, updateItemSize],
|
|
2037
|
+
[scrollToHandle, scrollToIndex, fenwickTree, focusItemAtIndex, getScrollAnchor, updateItemSize],
|
|
1902
2038
|
)
|
|
1903
2039
|
|
|
1904
2040
|
const totalContentHeight = fenwickTree.getTotal() + resolvedInsets.top + resolvedInsets.bottom
|