@aiquants/virtualscroll 2.5.0 → 2.7.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 +77 -0
- package/README.md +21 -3
- package/dist/ScrollPane.d.cts +28 -1
- package/dist/ScrollPane.d.ts +28 -1
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +5 -0
- package/dist/VirtualScroll.d.ts +5 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1361 -1351
- package/package.json +1 -1
- package/src/ScrollPane.tsx +86 -28
- package/src/VirtualScroll.tsx +37 -7
package/package.json
CHANGED
package/src/ScrollPane.tsx
CHANGED
|
@@ -99,6 +99,21 @@ export type ScrollPaneProps = {
|
|
|
99
99
|
renderThumbOverlay?: (props: ScrollBarThumbOverlayRenderProps) => React.ReactNode
|
|
100
100
|
/** Multiplier applied to wheel delta for faster or slower scrolling. / スクロール速度を調整するためのホイールデルタの倍率。 */
|
|
101
101
|
wheelSpeedMultiplier?: number
|
|
102
|
+
/**
|
|
103
|
+
* How wheel input behaves at the pane's edges (default: "auto").
|
|
104
|
+
* ペインの端でホイールがどう振る舞うか (既定: "auto")。
|
|
105
|
+
*
|
|
106
|
+
* - `"auto"` (既定): その向きへ**もう動けない**縦ホイールは消費せず、祖先 (ページ) の
|
|
107
|
+
* スクロールへ連鎖させる。ネイティブのスクロール領域と同じ挙動
|
|
108
|
+
* (CSS `overscroll-behavior: auto` 相当)。端に**達する**ノッチは部分消費して端で止まり、
|
|
109
|
+
* **次の**ノッチから連鎖する (ネイティブと同じ段付き)。
|
|
110
|
+
* - `"contain"`: 端でも消費し続け、ページへ連鎖させない (2.6.0 以前の挙動)。
|
|
111
|
+
* モーダル内の一覧など「背後を絶対に動かしたくない」消費側向け。
|
|
112
|
+
*
|
|
113
|
+
* ❗ 対象は**縦ホイールのみ**。横成分は `onWheelHorizontal` の所有者 (消費側) の責務であり、
|
|
114
|
+
* ポインタドラッグ / タッチの連鎖はネイティブ相当の慣性譲渡が別問題のため対象外。
|
|
115
|
+
*/
|
|
116
|
+
overscrollBehavior?: "auto" | "contain"
|
|
102
117
|
/**
|
|
103
118
|
* Callback delegating horizontal wheel/trackpad delta to an upstream owner.
|
|
104
119
|
* When provided, horizontal-dominant (or shift+wheel) gestures are consumed here
|
|
@@ -234,7 +249,16 @@ const toggleListeners = (target: HTMLElement | Window, entries: ListenerEntry[],
|
|
|
234
249
|
}
|
|
235
250
|
|
|
236
251
|
export type ScrollPaneHandle = {
|
|
237
|
-
|
|
252
|
+
/**
|
|
253
|
+
* Jumps to a position, clamped against the last-committed dims (or an explicit override).
|
|
254
|
+
* 位置へジャンプする。クランプは**最後に commit された寸法** (または明示指定) に対して行う。
|
|
255
|
+
*
|
|
256
|
+
* `dimsOverride` は「commit より新しい寸法を知っている呼び出し元」専用の口である
|
|
257
|
+
* (例: Fenwick 木を同期更新した直後のレイアウトシフト補正は、まだ commit されていない
|
|
258
|
+
* 木の合計高さでクランプしたい)。通常の呼び出しでは指定しないこと — 指定を誤ると
|
|
259
|
+
* 画面に出ていない寸法でクランプされ、A-1 の汚染を自分の手で再現することになる。
|
|
260
|
+
*/
|
|
261
|
+
scrollTo: (newPosition: number | ((prev: number) => number), dimsOverride?: { contentSize: number; viewportSize: number }) => number
|
|
238
262
|
getScrollPosition: () => number
|
|
239
263
|
getContentSize: () => number
|
|
240
264
|
getViewportSize: () => number
|
|
@@ -278,6 +302,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
278
302
|
itemCount,
|
|
279
303
|
renderThumbOverlay,
|
|
280
304
|
wheelSpeedMultiplier = 1,
|
|
305
|
+
overscrollBehavior = "auto",
|
|
281
306
|
onWheelHorizontal,
|
|
282
307
|
contentInsets,
|
|
283
308
|
visibleStartIndex,
|
|
@@ -477,12 +502,20 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
477
502
|
// contentSize と viewportSize を ref として保持
|
|
478
503
|
// const sizeRef = useRef({ contentSize, viewportSize })
|
|
479
504
|
|
|
480
|
-
// ❗ render
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
505
|
+
// ❗ **render 本体では代入しない (layout effect のみ = 常に「最後に commit された寸法」)。**
|
|
506
|
+
// render 中に書くと、中断されたトランジションの**コミットされなかった寸法**が残り、
|
|
507
|
+
// 保留中のホイール/ジャンプが画面に出ていない寸法でクランプされる (実測: 保留中の縮小で
|
|
508
|
+
// 一覧が先頭へ飛ぶ / ホイールが死ぬ。transitionClamp.spec.tsx が固定)。
|
|
509
|
+
//
|
|
510
|
+
// かつて「layout effect 化はタップスクロール E2E を退行させる」と記録されていたが、
|
|
511
|
+
// 8 回 x 3 系列の A/B/A 対照実験で**反証済み** (失敗は書き込み位置と無関係の
|
|
512
|
+
// コールドスタート負荷フレークで、両変種とも 6-7/8 で同率に失敗した)。加えて全計 1,893 回の
|
|
513
|
+
// scrollTo クランプ監査で committed 値と render 新値の食い違いは 0 件 — 同 commit 内で
|
|
514
|
+
// 新値を要する呼び出し元は存在しない (子 effect は本 effect より先に走るが、その窓に
|
|
515
|
+
// scrollTo 呼び出し元は無いことを確認済み)。commit より新しい寸法が要る唯一の呼び出し元
|
|
516
|
+
// (レイアウトシフト補正) は「committed + 今適用した delta」を `dimsOverride` で明示的に
|
|
517
|
+
// 渡す (木合計は渡さない — 木は破棄された WIP render にも変異される共有物)。
|
|
484
518
|
const sizeRef = useRef({ contentSize, viewportSize })
|
|
485
|
-
sizeRef.current = { contentSize, viewportSize }
|
|
486
519
|
|
|
487
520
|
const isScrollable = useMemo(() => contentSize > viewportSize, [contentSize, viewportSize])
|
|
488
521
|
|
|
@@ -520,8 +553,18 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
520
553
|
// )
|
|
521
554
|
|
|
522
555
|
const scrollTo = useCallback(
|
|
523
|
-
(newPosition: number | ((prev: number) => number)) => {
|
|
524
|
-
|
|
556
|
+
(newPosition: number | ((prev: number) => number), dimsOverride?: { contentSize: number; viewportSize: number }) => {
|
|
557
|
+
// 既定は「最後に commit された寸法」。dimsOverride は commit より新しい寸法を
|
|
558
|
+
// 知っている呼び出し元 (レイアウトシフト補正) だけが渡す。
|
|
559
|
+
// ❗ 非有限の override は警告して無視する (position の NaN ガードと同じ姿勢)。
|
|
560
|
+
// NaN の contentSize は「スクロール不能」判定へ落ち、位置を 0 へ強制 +
|
|
561
|
+
// onScroll(0) まで発火してしまう
|
|
562
|
+
let effectiveDims = dimsOverride ?? sizeRef.current
|
|
563
|
+
if (dimsOverride && !(Number.isFinite(dimsOverride.contentSize) && Number.isFinite(dimsOverride.viewportSize))) {
|
|
564
|
+
Logger.warn("[ScrollPane] scrollTo received non-finite dimsOverride; falling back to the committed dims", { dimsOverride })
|
|
565
|
+
effectiveDims = sizeRef.current
|
|
566
|
+
}
|
|
567
|
+
const { contentSize: currentContentSize, viewportSize: currentViewportSize } = effectiveDims
|
|
525
568
|
const currentIsScrollable = currentContentSize > currentViewportSize
|
|
526
569
|
const prevPosition = scrollPositionRef.current
|
|
527
570
|
|
|
@@ -743,18 +786,14 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
743
786
|
|
|
744
787
|
// ホイール適用の方針値。保持されたハンドル越しの呼び出しでも凍結させないため ref で持つ。
|
|
745
788
|
//
|
|
746
|
-
// ❗ **代入のタイミングは `sizeRef`
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
// layout effect へ移すのが筋だが、それだけではタップスクロールの走破が 1 コミット遅れて
|
|
755
|
-
// 短くなる退行が出る (実機 E2E で実証済み)。恒久対応にはクランプ経路の見直しが必要。
|
|
756
|
-
const wheelPolicyRef = useRef({ isScrollable, viewportSize, wheelSpeedMultiplier })
|
|
757
|
-
wheelPolicyRef.current = { isScrollable, viewportSize, wheelSpeedMultiplier }
|
|
789
|
+
// ❗ **代入のタイミングは `sizeRef` と必ず揃える (どちらも layout effect のみ)。**
|
|
790
|
+
// 片方だけ render 本体に残すと、中断されたトランジション中に `applyWheel` と `scrollTo` が
|
|
791
|
+
// 別々の世界の寸法で判断する最悪の組み合わせになる。
|
|
792
|
+
const wheelPolicyRef = useRef({ isScrollable, viewportSize, wheelSpeedMultiplier, overscrollBehavior })
|
|
793
|
+
useLayoutEffect(() => {
|
|
794
|
+
// sizeRef と同じ理由で「最後に commit された値」だけを保持する (A-1。sizeRef のコメント参照)
|
|
795
|
+
wheelPolicyRef.current = { isScrollable, viewportSize, wheelSpeedMultiplier, overscrollBehavior }
|
|
796
|
+
}, [isScrollable, viewportSize, wheelSpeedMultiplier, overscrollBehavior])
|
|
758
797
|
|
|
759
798
|
/**
|
|
760
799
|
* Applies one wheel event with the pane's own rules and reports whether it was consumed.
|
|
@@ -774,7 +813,11 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
774
813
|
* 2. 横優勢 (shift+ホイールを含む) は `onWheelHorizontal` へ委譲。未指定なら消費せず祖先へ委ねる
|
|
775
814
|
* 3. スクロール不能なら消費しない
|
|
776
815
|
* 4. px 換算後の縦成分が 0 なら消費しない (適用できる量が無いのに祖先のスクロールを奪わない)
|
|
777
|
-
* 5.
|
|
816
|
+
* 5. 実効 delta の確定 (速度倍率を先に適用)
|
|
817
|
+
* 6. `overscrollBehavior` が "contain" でなければ端判定: その向きへもう動けなければ
|
|
818
|
+
* 消費せず祖先へ連鎖 (2.7.0)
|
|
819
|
+
* 7. 消費済みの印 + `preventDefault()` + 慣性停止
|
|
820
|
+
* 8. 相対スクロール
|
|
778
821
|
*
|
|
779
822
|
* @param event - The wheel event to apply / 適用するホイールイベント
|
|
780
823
|
* @returns True when this pane applied the event / このペインが適用した場合に true。
|
|
@@ -800,7 +843,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
800
843
|
// 保持済みハンドルは preventDefault し続け、ページのスクロールを殺す)。
|
|
801
844
|
// `VirtualScrollHandle.applyWheel` は毎回 ref を辿るため凍結しない。両者の
|
|
802
845
|
// 非対称は `WheelBridgeTarget` が両方を等価に扱う設計と噛み合わない。
|
|
803
|
-
const { isScrollable: currentIsScrollable, viewportSize: currentViewportSize, wheelSpeedMultiplier: currentWheelSpeedMultiplier } = wheelPolicyRef.current
|
|
846
|
+
const { isScrollable: currentIsScrollable, viewportSize: currentViewportSize, wheelSpeedMultiplier: currentWheelSpeedMultiplier, overscrollBehavior: currentOverscrollBehavior } = wheelPolicyRef.current
|
|
804
847
|
const axes = resolveWheelAxes(event, currentViewportSize)
|
|
805
848
|
|
|
806
849
|
// ❗ ctrl+wheel はブラウザのズーム (ピンチズーム/ctrl+shift+ホイール等) なので横取りせず常に素通しする。
|
|
@@ -838,15 +881,30 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
838
881
|
return false
|
|
839
882
|
}
|
|
840
883
|
|
|
841
|
-
markWheelConsumed(event)
|
|
842
|
-
event.preventDefault()
|
|
843
|
-
stopInertia()
|
|
844
|
-
|
|
845
884
|
let deltaY = axes.verticalDelta
|
|
846
885
|
if (currentWheelSpeedMultiplier !== 1) {
|
|
847
886
|
deltaY *= currentWheelSpeedMultiplier
|
|
848
887
|
}
|
|
849
888
|
|
|
889
|
+
// ❗ 端の連鎖 (A-2): その向きへ**もう動けない**ホイールは消費せず祖先へ委ねる
|
|
890
|
+
// (ネイティブの `overscroll-behavior: auto` 相当)。判定は倍率適用後の実効 delta の
|
|
891
|
+
// 向き × committed 位置で行う。端に**達する**ノッチはここを素通りして部分消費され
|
|
892
|
+
// (クランプが端で止める)、次のノッチからこの分岐で連鎖する — ネイティブと同じ段付き。
|
|
893
|
+
// `"contain"` は 2.6.0 以前のトラップ挙動 (モーダル内の一覧など背後を動かしたくない
|
|
894
|
+
// 消費側向け) を明示的に維持する。
|
|
895
|
+
if (currentOverscrollBehavior !== "contain") {
|
|
896
|
+
const currentPosition = scrollPositionRef.current
|
|
897
|
+
// contentSize は sizeRef (committed。wheelPolicyRef と同じ layout effect 同期) から読む
|
|
898
|
+
const maxScrollPosition = Math.max(sizeRef.current.contentSize - currentViewportSize, 0)
|
|
899
|
+
if ((deltaY > 0 && currentPosition >= maxScrollPosition) || (deltaY < 0 && currentPosition <= 0)) {
|
|
900
|
+
return false
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
markWheelConsumed(event)
|
|
905
|
+
event.preventDefault()
|
|
906
|
+
stopInertia()
|
|
907
|
+
|
|
850
908
|
// ホットパスのためログ引数はサンクで遅延評価し、抑制時は DOM 読み取り (scrollTop) も発生させない
|
|
851
909
|
Logger.debug("[ScrollPane] wheel event", () => ({ deltaY, scrollPosition: scrollPositionRef.current, wheelSpeedMultiplier: currentWheelSpeedMultiplier, deltaMode: event.deltaMode, scrollTop: contentAreaRef.current?.scrollTop }))
|
|
852
910
|
|
|
@@ -905,9 +963,9 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
905
963
|
useImperativeHandle(
|
|
906
964
|
ref,
|
|
907
965
|
() => ({
|
|
908
|
-
scrollTo: (pos) => {
|
|
966
|
+
scrollTo: (pos, dimsOverride) => {
|
|
909
967
|
stopInertia()
|
|
910
|
-
return scrollTo(pos)
|
|
968
|
+
return scrollTo(pos, dimsOverride)
|
|
911
969
|
},
|
|
912
970
|
getScrollPosition: () => scrollPositionRef.current,
|
|
913
971
|
getContentSize: () => contentSize,
|
package/src/VirtualScroll.tsx
CHANGED
|
@@ -150,6 +150,11 @@ export type VirtualScrollBehaviorOptions = {
|
|
|
150
150
|
enableKeyboardNavigation?: boolean
|
|
151
151
|
wheelSpeedMultiplier?: number
|
|
152
152
|
inertiaOptions?: ScrollPaneProps["inertiaOptions"]
|
|
153
|
+
/**
|
|
154
|
+
* How wheel input behaves at the list's edges (default: "auto" = chain to ancestors).
|
|
155
|
+
* 一覧の端でホイールがどう振る舞うか (既定: "auto" = 祖先へ連鎖)。`ScrollPaneProps["overscrollBehavior"]` 参照。
|
|
156
|
+
*/
|
|
157
|
+
overscrollBehavior?: ScrollPaneProps["overscrollBehavior"]
|
|
153
158
|
clipItemHeight?: boolean
|
|
154
159
|
/**
|
|
155
160
|
* When `true`, the underlying Fenwick tree fully re-samples on every `getItemHeight` change
|
|
@@ -914,7 +919,7 @@ const VirtualScrollInner = <T,>(
|
|
|
914
919
|
) => {
|
|
915
920
|
const { width: scrollBarWidth, enableThumbDrag, enableTrackClick, enableArrowButtons, enableScrollToTopBottomButtons, renderThumbOverlay, tapScrollCircleOptions } = scrollBarOptions ?? {}
|
|
916
921
|
|
|
917
|
-
const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, wheelSpeedMultiplier, inertiaOptions, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
|
|
922
|
+
const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, wheelSpeedMultiplier, inertiaOptions, overscrollBehavior, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
|
|
918
923
|
|
|
919
924
|
// viewportSize 未指定のときは ScrollPane が自分の帯を計測し、その値をこのコールバックで通知する。
|
|
920
925
|
// 描画枚数 (computeRenderingRanges) とアライン計算 (scrollToIndex / ドリフト補正) はこの解決済み値を使う。
|
|
@@ -1305,7 +1310,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1305
1310
|
* 乖離した場合 (同一同期バッチ内の旧 contentSize によるクランプ) は、新 contentSize
|
|
1306
1311
|
* コミット後の補正再発行を予約。
|
|
1307
1312
|
*/
|
|
1308
|
-
const issueCompensationScroll = useCallback((targetPosition: number): number => {
|
|
1313
|
+
const issueCompensationScroll = useCallback((targetPosition: number, appliedDelta = 0): number => {
|
|
1309
1314
|
const pane = scrollPaneRef.current
|
|
1310
1315
|
if (!pane) {
|
|
1311
1316
|
return targetPosition
|
|
@@ -1313,7 +1318,20 @@ const VirtualScrollInner = <T,>(
|
|
|
1313
1318
|
// クランプで位置が変化しない場合 onScroll は発火しない。事前位置と比較して増分を戻すために保持する
|
|
1314
1319
|
const beforePosition = pane.getScrollPosition()
|
|
1315
1320
|
isCompensatingRef.current += 1
|
|
1316
|
-
|
|
1321
|
+
// ❗ クランプは **「committed 寸法 + 今適用した delta」**で行う (dimsOverride)。この補正は木を
|
|
1322
|
+
// 同期更新した**直後・commit 前**に走るため、ペインの committed sizeRef は必ず 1 commit 古く、
|
|
1323
|
+
// 既定のままだと旧最大値でクランプされ二段補正 (§8.6) の再発行に頼ることになる。
|
|
1324
|
+
//
|
|
1325
|
+
// ❗ `fenwickTree.getTotal()` ではなく committed + delta を使う。木は**破棄された WIP render
|
|
1326
|
+
// にも変異される共有物** (保留トランジション中は未 commit の行数の合計を返すことを
|
|
1327
|
+
// transitionClamp.spec.tsx の前提アサートで実証済み) であり、木合計でのクランプは
|
|
1328
|
+
// fenwick の並行安全設計の内部タイミングに依存する。committed + delta は定義から
|
|
1329
|
+
// 「この補正が保とうとしている committed 空間 + 今適用した変化」そのものであり、
|
|
1330
|
+
// WIP 変異のタイミングと無関係に正しい (通常時は木合計と同値で、一段目がそのまま
|
|
1331
|
+
// 目標へ届き、二段補正はバックストップとして休眠する)
|
|
1332
|
+
const freshContentSize = pane.getContentSize() + appliedDelta
|
|
1333
|
+
const freshViewportSize = pane.getViewportSize()
|
|
1334
|
+
const appliedPosition = pane.scrollTo(targetPosition, { contentSize: freshContentSize, viewportSize: freshViewportSize })
|
|
1317
1335
|
if (appliedPosition === beforePosition) {
|
|
1318
1336
|
// onScroll 不発: 補正カウンタの増分を巻き戻し、次の手動スクロールが補正扱いされるのを防ぐ
|
|
1319
1337
|
isCompensatingRef.current = Math.max(0, isCompensatingRef.current - 1)
|
|
@@ -1507,7 +1525,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1507
1525
|
// 同一同期バッチ内では ScrollPane の sizeRef が旧 contentSize のままのため、scrollTo は
|
|
1508
1526
|
// 旧最大値でクランプされ得る。issueCompensationScroll がカウンタのリークを防ぎつつ乖離時の
|
|
1509
1527
|
// 再発行を予約し、論理位置 (latestScrollPositionRef) には補正目標を保持して二段目の収束先とする。
|
|
1510
|
-
issueCompensationScroll(newPosition)
|
|
1528
|
+
issueCompensationScroll(newPosition, delta)
|
|
1511
1529
|
updateScrollPositionImmediate(newPosition, { immediate: true })
|
|
1512
1530
|
Logger.debug("[VirtualScroll] Adjusted scroll for layout shift (manual update)", { from: currentPanePosition, to: newPosition, causedByIndex: safeIndex, delta, activeVisibleStartIndex })
|
|
1513
1531
|
}
|
|
@@ -1746,7 +1764,18 @@ const VirtualScrollInner = <T,>(
|
|
|
1746
1764
|
|
|
1747
1765
|
// If manual scroll (neither programmatic nor compensating), clear the pending anchor
|
|
1748
1766
|
// This ensures that manual scrolling immediately detaches from any previous scroll target
|
|
1749
|
-
|
|
1767
|
+
// ❗ isResizingRef は render 本体で立つ (in-commit の §9 クランプ連鎖が読むため必須) が、
|
|
1768
|
+
// 中断されたトランジションの render でも立ってしまい、commit が無いので降りない。
|
|
1769
|
+
// このクロージャの itemCount は **commit を通ってしか届かない** (破棄 render のクロージャは
|
|
1770
|
+
// committed ツリーに残らない) ため、ref と閉じ込め値の照合で「committed な resize か」を
|
|
1771
|
+
// 判別できる。保留中 (ref=新 / クロージャ=旧) は不一致 → 手動スクロール扱いでアンカーが
|
|
1772
|
+
// 正しく外れる。resize の commit 内連鎖 (ref=新 / クロージャ=新) は一致 → 従来どおり。
|
|
1773
|
+
// ❗ 純粋な同数往復 (500→300 保留→500 bailout) は照合が正しく効く (bailout は ref の
|
|
1774
|
+
// 再前進も防ぐため不一致になる)。残存する盲点は「戻りに別 state 変更が同伴して一覧が
|
|
1775
|
+
// 再 render される」形のみ (ref が committed と同値へ前進し、同伴 commit ではリセット
|
|
1776
|
+
// 経路が走らない)。仕様書 §8.4 の既知の盲点を参照
|
|
1777
|
+
const isCommittedResize = isResizingRef.current && prevItemCountRef.current === itemCount
|
|
1778
|
+
if (!(isProgrammatic || isCompensating || isCommittedResize)) {
|
|
1750
1779
|
pendingVisibleStartIndexRef.current = null
|
|
1751
1780
|
}
|
|
1752
1781
|
|
|
@@ -1775,7 +1804,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1775
1804
|
}
|
|
1776
1805
|
}
|
|
1777
1806
|
},
|
|
1778
|
-
[updateScrollPositionImmediate, enableScrollToTopBottomButtons],
|
|
1807
|
+
[updateScrollPositionImmediate, enableScrollToTopBottomButtons, itemCount],
|
|
1779
1808
|
)
|
|
1780
1809
|
|
|
1781
1810
|
// レンダリング範囲を計算
|
|
@@ -2189,7 +2218,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2189
2218
|
const newPosition = panePosition + shiftAmount
|
|
2190
2219
|
// updateItemSize と同型: scrollTo は旧 contentSize でクランプされ得るため、
|
|
2191
2220
|
// issueCompensationScroll でリークを防ぎつつ、乖離時は contentSize 同期 effect で再発行する。
|
|
2192
|
-
issueCompensationScroll(newPosition)
|
|
2221
|
+
issueCompensationScroll(newPosition, shiftAmount)
|
|
2193
2222
|
updateScrollPositionImmediate(newPosition, { immediate: true })
|
|
2194
2223
|
Logger.debug("[VirtualScroll] Adjusted scroll for layout shift (auto update)", { from: panePosition, to: newPosition, shiftAmount })
|
|
2195
2224
|
} else if (panePosition !== latestScrollPositionRef.current) {
|
|
@@ -2341,6 +2370,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2341
2370
|
background={background}
|
|
2342
2371
|
tapScrollCircleOptions={tapScrollCircleOptions}
|
|
2343
2372
|
inertiaOptions={inertiaOptions}
|
|
2373
|
+
overscrollBehavior={overscrollBehavior}
|
|
2344
2374
|
itemCount={itemCount}
|
|
2345
2375
|
scrollBarWidth={scrollBarWidth}
|
|
2346
2376
|
enableThumbDrag={enableThumbDrag}
|