@aiquants/virtualscroll 1.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/virtualscroll",
3
- "version": "1.25.0",
3
+ "version": "2.0.0",
4
4
  "description": "High-performance virtual scrolling component for React with variable item heights",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -25,11 +25,35 @@ export type VirtualScrollRange = {
25
25
  }
26
26
 
27
27
  /**
28
- * Handle interface for controlling the VirtualScroll component externally.
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
- * 外部から VirtualScroll コンポーネントを制御するためのハンドルインターフェース。
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 = ScrollPaneHandle & {
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 で管理されている総高さを取得 */
@@ -737,7 +761,9 @@ const VirtualScrollInner = <T,>(
737
761
 
738
762
  const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, wheelSpeedMultiplier, inertiaOptions, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
739
763
 
740
- const scrollPaneRef = useRef<VirtualScrollHandle>(null)
764
+ // 内部ペイン ref の型は ScrollPaneHandle (ペイン座標)。VirtualScrollHandle と誤記すると
765
+ // 2.0.0 以降は型ドキュメントが「論理座標を返す」と嘘をつく (構造的互換で型検査は通ってしまう)
766
+ const scrollPaneRef = useRef<ScrollPaneHandle>(null)
741
767
  const currentRangeRef = useRef<VirtualScrollRange>({
742
768
  renderingStartIndex: 0,
743
769
  renderingEndIndex: 0,
@@ -1214,10 +1240,18 @@ const VirtualScrollInner = <T,>(
1214
1240
  // 依存関係: resolvedInsets.top
1215
1241
  const previousTop = previousTopInsetRef.current
1216
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
+ }
1217
1252
 
1218
1253
  const logicalPosition = toLogicalPositionWithInset(latestScrollPositionRef.current, previousTop)
1219
1254
  const panePosition = toPanePositionWithInset(logicalPosition, resolvedInsets.top)
1220
- previousTopInsetRef.current = resolvedInsets.top
1221
1255
  latestScrollPositionRef.current = panePosition
1222
1256
  scrollPaneRef.current?.scrollTo(panePosition)
1223
1257
  updateScrollPositionImmediate(panePosition, { immediate: true })
@@ -1453,7 +1487,9 @@ const VirtualScrollInner = <T,>(
1453
1487
  const panePosition = scrollPaneRef.current?.getScrollPosition()
1454
1488
  const effectivePosition = typeof panePosition === "number" ? panePosition : latestScrollPositionRef.current
1455
1489
  updateScrollPositionImmediate(effectivePosition)
1456
- return effectivePosition
1490
+ // ❗ 返値も論理座標 (2.0.0)。ペイン座標を返すと「入力は論理・返値はペイン」の非対称が
1491
+ // 復活し、返値をミラーへ書き戻す消費者が inset 分ずれる
1492
+ return toLogicalPositionWithInset(effectivePosition, resolvedInsetsTopRef.current)
1457
1493
  },
1458
1494
  [resolvedInsets.top, scrollTo, updateScrollPositionImmediate],
1459
1495
  )
@@ -1981,7 +2017,12 @@ const VirtualScrollInner = <T,>(
1981
2017
  useImperativeHandle(
1982
2018
  ref,
1983
2019
  () => ({
1984
- getScrollPosition: () => scrollPaneRef.current?.getScrollPosition() ?? -1,
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
+ },
1985
2026
  getContentSize: () => scrollPaneRef.current?.getContentSize() ?? -1,
1986
2027
  getViewportSize: () => scrollPaneRef.current?.getViewportSize() ?? -1,
1987
2028
  scrollTo: scrollToHandle,