@aiquants/virtualscroll 3.11.2 → 3.11.3
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 +16 -0
- package/README.md +9 -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 +1851 -1835
- package/package.json +1 -1
- package/src/VirtualScroll.tsx +150 -30
package/package.json
CHANGED
package/src/VirtualScroll.tsx
CHANGED
|
@@ -241,7 +241,12 @@ export type VirtualScrollScrollBarOptions = {
|
|
|
241
241
|
enableArrowButtonTabStops?: boolean
|
|
242
242
|
/**
|
|
243
243
|
* Whether to show the auto-hiding Top / Bottom pills (default `false`; texts from `labels.scrollToTop` / `labels.scrollToBottom`).
|
|
244
|
+
* A user scroll shows the pill of the edge it moves toward, or the other edge's pill once the pane rests at that edge; a pill
|
|
245
|
+
* shows only while the pane can still scroll toward its edge, so it hides as soon as the pane comes to rest there, and a pane
|
|
246
|
+
* that cannot scroll shows neither.
|
|
244
247
|
* 自動で隠れる Top / Bottom ピルを表示するかどうか (既定 `false`。文言は `labels.scrollToTop` / `labels.scrollToBottom`)。
|
|
248
|
+
* 利用者のスクロールは向かう端のピルを出し、ペインがその端に着いていれば反対の端のピルを出す。ピルはペインがまだその端の方へ
|
|
249
|
+
* スクロールできる間だけ見えるので、ペインがその端に着くとすぐ隠れ、スクロールできないペインはどちらも出さない。
|
|
245
250
|
*/
|
|
246
251
|
enableScrollToTopBottomButtons?: boolean
|
|
247
252
|
/** Renderer for UI anchored near the thumb. / つまみ付近に重ねる UI のレンダラー。 */
|
|
@@ -722,6 +727,54 @@ const edgeOfAlignment = (align: "top" | "bottom" | "center" | undefined): Aligne
|
|
|
722
727
|
*/
|
|
723
728
|
const canonicalAlignedEdge = (alignment: AlignedEdge | null): AlignedEdge | null => (alignment !== null && alignment.panePosition <= 0 ? null : alignment)
|
|
724
729
|
|
|
730
|
+
/**
|
|
731
|
+
* The edge a scroll-to-edge pill scrolls to: `"top"` (the Top pill, to the first row) or `"bottom"` (the Bottom pill, to the
|
|
732
|
+
* last row).
|
|
733
|
+
*
|
|
734
|
+
* 端へ戻るピルが送る先の端。`"top"` (Top ピル。最初の行へ) か `"bottom"` (Bottom ピル。最後の行へ)。
|
|
735
|
+
*/
|
|
736
|
+
type EdgePill = "top" | "bottom"
|
|
737
|
+
|
|
738
|
+
/** How long a pill stays after the user scroll that showed it, unless the pane comes to rest at its edge first (ms) / ピルを出した利用者のスクロールからピルが消えるまでの時間 (ms。先にペインがその端に着けばそこで消える) */
|
|
739
|
+
const EDGE_PILL_HIDE_DELAY_MS = 2000
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Whether the pane can still scroll toward an edge: toward the top while its position exceeds 0, toward the bottom while
|
|
743
|
+
* its position stays under the max scroll position, each by more than `EDGE_POSITION_TOLERANCE` (closer counts as resting
|
|
744
|
+
* at that edge). A pane that cannot scroll (max scroll position 0) can scroll toward neither edge.
|
|
745
|
+
*
|
|
746
|
+
* ペインがまだその端の方へスクロールできるかの判定。上端へは位置が 0 を上回り、下端へは位置が最大位置を下回り、どちらも
|
|
747
|
+
* `EDGE_POSITION_TOLERANCE` を超えて離れている間だけ (それより近ければその端に着いている)。スクロールできないペイン
|
|
748
|
+
* (最大位置 0) はどちらの端へもスクロールできない。
|
|
749
|
+
*
|
|
750
|
+
* @param edge - The edge / 端
|
|
751
|
+
* @param panePosition - Pane position (px) / ペイン位置 (px)
|
|
752
|
+
* @param maxScrollPosition - The pane's max scroll position (px) / ペインの最大位置 (px)
|
|
753
|
+
* @returns True while the pane can scroll toward the edge / その端の方へスクロールできる間は true
|
|
754
|
+
*/
|
|
755
|
+
const canScrollTowardEdge = (edge: EdgePill, panePosition: number, maxScrollPosition: number): boolean => (edge === "top" ? panePosition > EDGE_POSITION_TOLERANCE : panePosition < maxScrollPosition - EDGE_POSITION_TOLERANCE)
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Picks the pill a user scroll shows: the pill of the edge the scroll moves toward while the pane can still scroll toward
|
|
759
|
+
* that edge; once the pane rests at that edge, the pill of the other edge; and no pill when the pane can scroll toward
|
|
760
|
+
* neither edge.
|
|
761
|
+
*
|
|
762
|
+
* 利用者のスクロールが出すピルを選ぶ処理。スクロールが向かう端の方へまだスクロールできる間はその端のピル、ペインがその端に
|
|
763
|
+
* 着いたら反対の端のピル、どちらの端へもスクロールできなければピル無し。
|
|
764
|
+
*
|
|
765
|
+
* @param toward - The edge the scroll moves toward / スクロールが向かう端
|
|
766
|
+
* @param panePosition - Pane position after the scroll (px) / スクロール後のペイン位置 (px)
|
|
767
|
+
* @param maxScrollPosition - The pane's max scroll position (px) / ペインの最大位置 (px)
|
|
768
|
+
* @returns The pill to show, or `null` for none / 出すピル (無ければ `null`)
|
|
769
|
+
*/
|
|
770
|
+
const pickEdgePill = (toward: EdgePill, panePosition: number, maxScrollPosition: number): EdgePill | null => {
|
|
771
|
+
if (canScrollTowardEdge(toward, panePosition, maxScrollPosition)) {
|
|
772
|
+
return toward
|
|
773
|
+
}
|
|
774
|
+
const opposite: EdgePill = toward === "top" ? "bottom" : "top"
|
|
775
|
+
return canScrollTowardEdge(opposite, panePosition, maxScrollPosition) ? opposite : null
|
|
776
|
+
}
|
|
777
|
+
|
|
725
778
|
/**
|
|
726
779
|
* The parts of the viewport that something drawn over the rows covers — the visible scroll-to-edge pill — as px from each
|
|
727
780
|
* edge to the far side of what covers it. Module-level export (NOT in the package barrel), the parameter type of
|
|
@@ -1727,9 +1780,51 @@ const VirtualScrollInner = <T,>(
|
|
|
1727
1780
|
const pendingFocusIndexRef = useRef<number | null>(null)
|
|
1728
1781
|
const lastFocusedIndexRef = useRef<number | null>(null)
|
|
1729
1782
|
|
|
1730
|
-
|
|
1783
|
+
// スクロールの向きではなくピルそのものを持つ。端に着いたスクロールは反対の端のピルを出すので、向きからピルは決まらない。
|
|
1784
|
+
// 隠れた後も同じピルを描き続けるのは、隠れるフェードをそのピルのまま見せるため
|
|
1785
|
+
const [edgePill, setEdgePill] = useState<EdgePill | null>(null)
|
|
1731
1786
|
const [showScrollButtons, setShowScrollButtons] = useState(false)
|
|
1787
|
+
// 見えているピルの同期の写し (隠れている間は null)。スクロールの処理は状態を閉じ込めずに要否を決め、隠れているピルへ同じ値を
|
|
1788
|
+
// 書いて確定を足さない
|
|
1789
|
+
const shownEdgePillRef = useRef<EdgePill | null>(null)
|
|
1732
1790
|
const scrollButtonTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* Hides the visible scroll-to-edge pill and cancels its auto-hide timer; writes no state while no pill shows.
|
|
1794
|
+
*
|
|
1795
|
+
* 見えている端へ戻るピルを隠し、自動非表示のタイマーを止める処理。ピルが見えていない間は状態を書かない。
|
|
1796
|
+
*/
|
|
1797
|
+
const hideEdgePill = useCallback((): void => {
|
|
1798
|
+
if (scrollButtonTimerRef.current !== null) {
|
|
1799
|
+
clearTimeout(scrollButtonTimerRef.current)
|
|
1800
|
+
scrollButtonTimerRef.current = null
|
|
1801
|
+
}
|
|
1802
|
+
if (shownEdgePillRef.current === null) {
|
|
1803
|
+
return
|
|
1804
|
+
}
|
|
1805
|
+
shownEdgePillRef.current = null
|
|
1806
|
+
setShowScrollButtons(false)
|
|
1807
|
+
}, [])
|
|
1808
|
+
|
|
1809
|
+
/**
|
|
1810
|
+
* Shows a scroll-to-edge pill and (re)starts its auto-hide timer (`EDGE_PILL_HIDE_DELAY_MS`).
|
|
1811
|
+
*
|
|
1812
|
+
* 端へ戻るピルを出し、自動非表示のタイマー (`EDGE_PILL_HIDE_DELAY_MS`) を掛け直す処理。
|
|
1813
|
+
*
|
|
1814
|
+
* @param pill - The pill to show / 出すピル
|
|
1815
|
+
*/
|
|
1816
|
+
const showEdgePill = useCallback(
|
|
1817
|
+
(pill: EdgePill): void => {
|
|
1818
|
+
shownEdgePillRef.current = pill
|
|
1819
|
+
setEdgePill(pill)
|
|
1820
|
+
setShowScrollButtons(true)
|
|
1821
|
+
if (scrollButtonTimerRef.current !== null) {
|
|
1822
|
+
clearTimeout(scrollButtonTimerRef.current)
|
|
1823
|
+
}
|
|
1824
|
+
scrollButtonTimerRef.current = setTimeout(hideEdgePill, EDGE_PILL_HIDE_DELAY_MS)
|
|
1825
|
+
},
|
|
1826
|
+
[hideEdgePill],
|
|
1827
|
+
)
|
|
1733
1828
|
// 見せる操作は、画面に出ているピルを表示域 (行ラッパーの境界の箱の上端が表示域の上端) に対して測る
|
|
1734
1829
|
const edgeOverlayRef = useRef<HTMLDivElement>(null)
|
|
1735
1830
|
const edgePillRef = useRef<HTMLButtonElement>(null)
|
|
@@ -2534,9 +2629,17 @@ const VirtualScrollInner = <T,>(
|
|
|
2534
2629
|
const applyWheel = useCallback((event: WheelEvent): boolean => scrollPaneRef.current?.applyWheel(event) ?? false, [])
|
|
2535
2630
|
|
|
2536
2631
|
/**
|
|
2537
|
-
* Handles scroll events from the pane.
|
|
2632
|
+
* Handles scroll events from the pane. With the scroll-to-edge pills on, a scroll of any kind (user, programmatic or
|
|
2633
|
+
* compensating) that leaves the pane resting at the visible pill's edge hides that pill at once (`canScrollTowardEdge`),
|
|
2634
|
+
* and a user scroll of more than 1 px shows the pill `pickEdgePill` picks for its direction (none when the pane can
|
|
2635
|
+
* scroll toward neither edge) and restarts the auto-hide timer.
|
|
2636
|
+
*
|
|
2637
|
+
* ペインのスクロールイベントを処理。端へ戻るピルが有効なら、ペインを見えているピルの端に止めたスクロール (利用者・プログラム・
|
|
2638
|
+
* 補正のどれでも) はそのピルをすぐ隠し (`canScrollTowardEdge`)、1px を超える利用者のスクロールは向きから `pickEdgePill` が
|
|
2639
|
+
* 選ぶピルを出して (どちらの端へもスクロールできなければ出さない) 自動非表示のタイマーを掛け直す。
|
|
2538
2640
|
*
|
|
2539
|
-
*
|
|
2641
|
+
* @param newPosition - Pane position after the scroll (px) / スクロール後のペイン位置 (px)
|
|
2642
|
+
* @param prevPosition - Pane position before the scroll (px) / スクロール前のペイン位置 (px)
|
|
2540
2643
|
*/
|
|
2541
2644
|
const handleScroll = useCallback(
|
|
2542
2645
|
(newPosition: number, prevPosition: number) => {
|
|
@@ -2572,30 +2675,31 @@ const VirtualScrollInner = <T,>(
|
|
|
2572
2675
|
|
|
2573
2676
|
updateScrollPositionImmediate(newPosition)
|
|
2574
2677
|
|
|
2575
|
-
if (enableScrollToTopBottomButtons) {
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2678
|
+
if (!enableScrollToTopBottomButtons) {
|
|
2679
|
+
return
|
|
2680
|
+
}
|
|
2681
|
+
// 最大位置は確定済みの寸法でなく今の木から測る。補正のスクロールは確定前の新しい寸法でクランプされるため
|
|
2682
|
+
const maxScrollPosition = Math.max(0, fenwickTree.getTotal() + resolvedInsets.top + resolvedInsets.bottom - viewportSize)
|
|
2683
|
+
const shownPill = shownEdgePillRef.current
|
|
2684
|
+
// 端に着いたピルはもう何も動かさず、焦点を受けた端の行を覆うだけなので、着いた経路を問わず自動非表示を待たずに隠す
|
|
2685
|
+
if (shownPill !== null && !canScrollTowardEdge(shownPill, newPosition, maxScrollPosition)) {
|
|
2686
|
+
hideEdgePill()
|
|
2687
|
+
}
|
|
2688
|
+
if (isProgrammatic || isCompensating) {
|
|
2689
|
+
return
|
|
2690
|
+
}
|
|
2587
2691
|
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2692
|
+
const diff = newPosition - prevPosition
|
|
2693
|
+
Logger.debug("[VirtualScroll] Scroll diff:", diff, "New:", newPosition, "Prev:", prevPosition)
|
|
2694
|
+
if (Math.abs(diff) > 1) {
|
|
2695
|
+
const pill = pickEdgePill(diff > 0 ? "bottom" : "top", newPosition, maxScrollPosition)
|
|
2696
|
+
if (pill !== null) {
|
|
2697
|
+
showEdgePill(pill)
|
|
2698
|
+
Logger.debug("[VirtualScroll] Showing scroll-to-edge pill:", pill)
|
|
2595
2699
|
}
|
|
2596
2700
|
}
|
|
2597
2701
|
},
|
|
2598
|
-
[updateScrollPositionImmediate, forgetAlignedEdge, enableScrollToTopBottomButtons, itemCount],
|
|
2702
|
+
[updateScrollPositionImmediate, forgetAlignedEdge, enableScrollToTopBottomButtons, itemCount, fenwickTree, resolvedInsets.top, resolvedInsets.bottom, viewportSize, hideEdgePill, showEdgePill],
|
|
2599
2703
|
)
|
|
2600
2704
|
|
|
2601
2705
|
// レンダリング範囲を計算
|
|
@@ -2840,10 +2944,14 @@ const VirtualScrollInner = <T,>(
|
|
|
2840
2944
|
|
|
2841
2945
|
/**
|
|
2842
2946
|
* Renders the auto-hiding top/bottom pill overlay (texts from the resolved `scrollToTop` /
|
|
2843
|
-
* `scrollToBottom` labels), neutralized while hidden.
|
|
2947
|
+
* `scrollToBottom` labels), neutralized while hidden. It draws one pill — the one the last user
|
|
2948
|
+
* scroll picked (`edgePill`; the Bottom pill before any) — which shows only while the pane can
|
|
2949
|
+
* still scroll toward that pill's edge (`handleScroll` and the size check hide it at the edge).
|
|
2844
2950
|
*
|
|
2845
2951
|
* 自動非表示の先頭/末尾ピルのオーバーレイ (文言は解決済みラベルの `scrollToTop` /
|
|
2846
|
-
* `scrollToBottom`)
|
|
2952
|
+
* `scrollToBottom`) を描画し、非表示中は無効化する処理。描くピルは 1 つで、最後の利用者の
|
|
2953
|
+
* スクロールが選んだもの (`edgePill`。スクロールの前は Bottom ピル)。ペインがまだそのピルの端の
|
|
2954
|
+
* 方へスクロールできる間だけ見える (端に着くと `handleScroll` と大きさの照合が隠す)。
|
|
2847
2955
|
*
|
|
2848
2956
|
* ❗ 非表示は `opacity: 0` で表現するため、要素は DOM に残り続ける (フェードのために
|
|
2849
2957
|
* アンマウントしない)。`opacity` はフォーカス可能性に影響せず、CSS の `pointer-events: none`
|
|
@@ -2874,8 +2982,8 @@ const VirtualScrollInner = <T,>(
|
|
|
2874
2982
|
return null
|
|
2875
2983
|
}
|
|
2876
2984
|
|
|
2877
|
-
const isVisible = showScrollButtons
|
|
2878
|
-
const isTop =
|
|
2985
|
+
const isVisible = showScrollButtons
|
|
2986
|
+
const isTop = edgePill === "top"
|
|
2879
2987
|
const pillTabIndex = isVisible && !scrollChromeIsPointerOnly ? 0 : -1
|
|
2880
2988
|
|
|
2881
2989
|
return (
|
|
@@ -2892,7 +3000,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2892
3000
|
e.stopPropagation()
|
|
2893
3001
|
isProgrammaticScrollRef.current = true
|
|
2894
3002
|
scrollToIndex(0)
|
|
2895
|
-
|
|
3003
|
+
hideEdgePill()
|
|
2896
3004
|
}}>
|
|
2897
3005
|
{resolvedLabels.scrollToTop}
|
|
2898
3006
|
</button>
|
|
@@ -2909,7 +3017,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2909
3017
|
e.stopPropagation()
|
|
2910
3018
|
isProgrammaticScrollRef.current = true
|
|
2911
3019
|
scrollToIndex(itemCount - 1)
|
|
2912
|
-
|
|
3020
|
+
hideEdgePill()
|
|
2913
3021
|
}}>
|
|
2914
3022
|
{resolvedLabels.scrollToBottom}
|
|
2915
3023
|
</button>
|
|
@@ -2917,7 +3025,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2917
3025
|
)}
|
|
2918
3026
|
</div>
|
|
2919
3027
|
)
|
|
2920
|
-
}, [enableScrollToTopBottomButtons, scrollChromeIsPointerOnly, showScrollButtons,
|
|
3028
|
+
}, [enableScrollToTopBottomButtons, scrollChromeIsPointerOnly, showScrollButtons, edgePill, scrollToIndex, hideEdgePill, itemCount, resolvedLabels])
|
|
2921
3029
|
|
|
2922
3030
|
// 量子化アンカー (fix: LayoutUnit/f32 精度対策)。行 top はコンテンツ絶対座標そのままではなく
|
|
2923
3031
|
// 「絶対座標 - アンカー」で描画し、ラッパー側 translateY にアンカーを足し戻す。
|
|
@@ -3288,6 +3396,18 @@ const VirtualScrollInner = <T,>(
|
|
|
3288
3396
|
)
|
|
3289
3397
|
|
|
3290
3398
|
const totalContentHeight = fenwickTree.getTotal() + resolvedInsets.top + resolvedInsets.bottom
|
|
3399
|
+
const paneMaxScrollPosition = Math.max(0, totalContentHeight - viewportSize)
|
|
3400
|
+
|
|
3401
|
+
useLayoutEffect(() => {
|
|
3402
|
+
// 目的: 中身や表示域の大きさが変わると、位置が動かなくても最大位置が動いてペインが端に着く (中身が表示域に収まれば両端)。
|
|
3403
|
+
// スクロールの知らせが来ないこの着き方でも、見えているピルを描画の前に隠す。端から離れても出し直さないのは、ピルを出すのが
|
|
3404
|
+
// 利用者のスクロールだけだから (下に行が足されても、Bottom ピルは次の利用者のスクロールで出る)
|
|
3405
|
+
// 依存関係: paneMaxScrollPosition, hideEdgePill
|
|
3406
|
+
const shownPill = shownEdgePillRef.current
|
|
3407
|
+
if (shownPill !== null && !canScrollTowardEdge(shownPill, latestScrollPositionRef.current, paneMaxScrollPosition)) {
|
|
3408
|
+
hideEdgePill()
|
|
3409
|
+
}
|
|
3410
|
+
}, [paneMaxScrollPosition, hideEdgePill])
|
|
3291
3411
|
|
|
3292
3412
|
// ライブリージョン無効時はペインをそのまま根要素として返す (既存消費者の DOM 形状を変えない)
|
|
3293
3413
|
const pane = (
|