@aiquants/virtualscroll 3.7.1 → 3.8.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 +127 -39
- package/README.md +73 -5
- package/dist/ScrollBar.d.cts +35 -1
- package/dist/ScrollBar.d.ts +35 -1
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.cts +15 -0
- package/dist/ScrollPane.d.ts +15 -0
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +70 -57
- package/dist/VirtualGrid.d.ts +70 -57
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +59 -2
- package/dist/VirtualScroll.d.ts +59 -2
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/computeTapScrollVelocity.d.cts +3 -3
- package/dist/computeTapScrollVelocity.d.ts +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +1982 -1948
- package/dist/useGridTapScroll.d.cts +11 -11
- package/dist/useGridTapScroll.d.ts +11 -11
- package/dist/useGridTapScroll.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ScrollBar.tsx +49 -7
- package/src/ScrollPane.tsx +17 -0
- package/src/VirtualGrid.tsx +111 -70
- package/src/VirtualScroll.tsx +167 -5
- package/src/computeTapScrollVelocity.ts +3 -3
- package/src/styles/virtualscroll.css +1 -1
- package/src/useGridTapScroll.ts +12 -12
package/src/VirtualScroll.tsx
CHANGED
|
@@ -127,13 +127,49 @@ export type VirtualScrollHandle = {
|
|
|
127
127
|
updateItemSize: (index: number, size: number) => void
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Scrollbar options of `VirtualScroll` (forwarded to its ScrollPane and bar) and of `VirtualGrid`
|
|
132
|
+
* (see `VirtualGridProps["scrollBarOptions"]` for which members reach which of the grid's two bars).
|
|
133
|
+
*
|
|
134
|
+
* `VirtualScroll` のスクロールバー設定 (ScrollPane とそのバーへ転送) であり、`VirtualGrid` の
|
|
135
|
+
* スクロールバー設定でもある (グリッドの 2 本のバーのどちらへ届くかは `VirtualGridProps["scrollBarOptions"]` 参照)。
|
|
136
|
+
*/
|
|
130
137
|
export type VirtualScrollScrollBarOptions = {
|
|
138
|
+
/** Scrollbar thickness in px (default 12). / スクロールバーの太さ (px、既定 12)。 */
|
|
131
139
|
width?: number
|
|
140
|
+
/** Whether dragging the thumb scrolls (default `true`). / つまみのドラッグ操作を許可するかどうか (既定 `true`)。 */
|
|
132
141
|
enableThumbDrag?: boolean
|
|
142
|
+
/** Whether pressing the track scrolls there (default `true`). / トラックの押下によるスクロールを許可するかどうか (既定 `true`)。 */
|
|
133
143
|
enableTrackClick?: boolean
|
|
144
|
+
/** Whether the arrow buttons scroll (default `true`). / 矢印ボタンによるスクロールを許可するかどうか (既定 `true`)。 */
|
|
134
145
|
enableArrowButtons?: boolean
|
|
146
|
+
/**
|
|
147
|
+
* Whether the scrollbar's arrow buttons are Tab stops (default `true`). Set it to `false` when
|
|
148
|
+
* the host already provides keyboard scrolling (roving row focus with Arrow / Page / Home / End):
|
|
149
|
+
* the arrows then only add two redundant Tab stops per bar — native scrollbars are never Tab
|
|
150
|
+
* stops either. `false` changes `tabIndex` alone (`-1`): the arrows stay pointer-operable and
|
|
151
|
+
* named, and Enter / Space still scroll when one is focused from script.
|
|
152
|
+
* The default stays `true` because the rows are not Tab stops, so without a host keyboard model
|
|
153
|
+
* the arrows are the only scrolling control a keyboard user can reach with Tab. Full contract:
|
|
154
|
+
* `ScrollBarProps["enableArrowButtonTabStops"]`.
|
|
155
|
+
* スクロールバーの矢印ボタンを Tab の止まり先にするかどうか (既定 `true`)。ホストがキーボード
|
|
156
|
+
* スクロールを既に提供する場合 (行のロービングフォーカスと矢印 / Page / Home / End) に `false` —
|
|
157
|
+
* 矢印はバー 1 本あたり冗長な Tab の止まり先を 2 つ足すだけになるため (ネイティブのスクロールバーも
|
|
158
|
+
* Tab の止まり先にならない)。`false` が変えるのは `tabIndex` (`-1`) だけで、ポインタ操作・
|
|
159
|
+
* アクセシブルネームと、スクリプトからフォーカスした矢印の Enter / Space は維持。既定が `true`
|
|
160
|
+
* なのは、行が Tab の止まり先ではないため、ホストのキーボードモデルが無ければ矢印がキーボード
|
|
161
|
+
* 利用者の Tab で届く唯一のスクロール操作部品だから。契約の全文は
|
|
162
|
+
* `ScrollBarProps["enableArrowButtonTabStops"]`。
|
|
163
|
+
*/
|
|
164
|
+
enableArrowButtonTabStops?: boolean
|
|
165
|
+
/**
|
|
166
|
+
* Whether to show the auto-hiding Top / Bottom pills (default `false`; texts from `labels.scrollToTop` / `labels.scrollToBottom`).
|
|
167
|
+
* 自動で隠れる Top / Bottom ピルを表示するかどうか (既定 `false`。文言は `labels.scrollToTop` / `labels.scrollToBottom`)。
|
|
168
|
+
*/
|
|
135
169
|
enableScrollToTopBottomButtons?: boolean
|
|
170
|
+
/** Renderer for UI anchored near the thumb. / つまみ付近に重ねる UI のレンダラー。 */
|
|
136
171
|
renderThumbOverlay?: ScrollPaneProps["renderThumbOverlay"]
|
|
172
|
+
/** Tap-scroll circle options (VirtualGrid: its single two-axis circle). / タップスクロールサークルの設定 (VirtualGrid では唯一の 2 軸サークル)。 */
|
|
137
173
|
tapScrollCircleOptions?: ScrollPaneProps["tapScrollCircleOptions"]
|
|
138
174
|
}
|
|
139
175
|
|
|
@@ -488,11 +524,11 @@ const toPanePositionWithInset = (logical: number, top: number) => (logical <= 0
|
|
|
488
524
|
* Maximum run length of consecutive zero-height rows scanned linearly before attempting an
|
|
489
525
|
* O(log n) jump to the next non-zero row via the Fenwick tree. Module-level export (NOT in the
|
|
490
526
|
* package barrel) so VirtualGrid's column axis shares the SAME constant instead of duplicating
|
|
491
|
-
* the value (
|
|
527
|
+
* the value (a shared constant is never copied — same treatment as ANCHOR_REBASE_DISTANCE).
|
|
492
528
|
*
|
|
493
529
|
* 高さ 0 行の連続をこの件数まで線形走査し、超えたら Fenwick 木で次の非 0 行へ O(log n) ジャンプ
|
|
494
530
|
* する閾値。モジュールレベル export (バレル非公開) — VirtualGrid 列軸が値の複製でなく同一定数を
|
|
495
|
-
* 共有するため (
|
|
531
|
+
* 共有するため (共有定数は複製しない — ANCHOR_REBASE_DISTANCE と同じ扱い)。
|
|
496
532
|
*/
|
|
497
533
|
export const ZERO_HEIGHT_RUN_LIMIT = 1000
|
|
498
534
|
|
|
@@ -520,6 +556,51 @@ export const MAX_RENDERED_ITEMS = 2000
|
|
|
520
556
|
*/
|
|
521
557
|
export const ANCHOR_REBASE_DISTANCE = 1_048_576 // 2^20 px — VirtualGrid の横アンカーが共有 import する (パッケージバレルへは非公開)
|
|
522
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Snaps a CSS-px offset to the device-pixel grid of a window: the nearest offset whose device-px value is a
|
|
561
|
+
* whole number, `Math.round(cssPx × ratio) / ratio` (an exact half rounds toward +∞, as `Math.round` does). The items-wrapper translate goes through it: a composited layer moved by a fraction of a
|
|
562
|
+
* device pixel is resampled as a whole (2-px outlines, gaps and rings smear across neighbouring device rows and
|
|
563
|
+
* text blurs), while fractional layout positions inside the layer are painted on whole pixels already.
|
|
564
|
+
* Idempotent: a snapped value snaps to itself. A non-finite `cssPx` propagates (`NaN` in, `NaN` out).
|
|
565
|
+
* Module-level export (NOT in the package barrel).
|
|
566
|
+
*
|
|
567
|
+
* CSS px のオフセットをウィンドウの装置の画素の格子へ揃える処理。装置 px で整数になる最も近いオフセット
|
|
568
|
+
* `Math.round(cssPx × ratio) / ratio` (ちょうど半分は `Math.round` どおり +∞ 側)。
|
|
569
|
+
* 行ラッパーの平行移動はここを通す — 合成層を装置の画素の端数だけ動かすとブラウザは層全体を再標本化し
|
|
570
|
+
* (2px の輪郭・隙間・輪が隣の装置の行へ滲み、文字もぼける)、層の中の端数のレイアウト位置は描画が既に画素へ
|
|
571
|
+
* 揃えるため。冪等 (揃えた値はそのまま)。有限でない `cssPx` はそのまま伝わる (`NaN` は `NaN`)。
|
|
572
|
+
* モジュールレベル export (バレル非公開)。
|
|
573
|
+
*
|
|
574
|
+
* @param cssPx - Offset in CSS px; negative and fractional values included / CSS px のオフセット (負・小数を含む)
|
|
575
|
+
* @param ratio - Device px per CSS px of the window that paints the offset (its `devicePixelRatio`); a finite number > 0 / オフセットを描くウィンドウの CSS px あたりの装置 px (そのウィンドウの `devicePixelRatio`。0 より大きい有限数)
|
|
576
|
+
* @returns The snapped offset in CSS px / 揃えたオフセット (CSS px)
|
|
577
|
+
* @throws {RangeError} When `ratio` is not a finite number greater than 0 / `ratio` が 0 より大きい有限数でないとき
|
|
578
|
+
*/
|
|
579
|
+
export const snapToDevicePixelGrid = (cssPx: number, ratio: number): number => {
|
|
580
|
+
if (!(Number.isFinite(ratio) && ratio > 0)) {
|
|
581
|
+
throw new RangeError(`[VirtualScroll] devicePixelRatio must be a finite number > 0, received ${ratio}`)
|
|
582
|
+
}
|
|
583
|
+
return Math.round(cssPx * ratio) / ratio
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Returns the window of the document the element belongs to — the window whose screen paints it.
|
|
588
|
+
*
|
|
589
|
+
* 要素が属する文書のウィンドウ (要素を描く画面を持つウィンドウ) を返す処理。大域の `window` は使わない
|
|
590
|
+
* (別ウィンドウ・iframe へ描いた一覧の装置の画素比は、そのウィンドウのもの)。
|
|
591
|
+
*
|
|
592
|
+
* @param element - Element of the list / 一覧の要素
|
|
593
|
+
* @returns The window / ウィンドウ
|
|
594
|
+
* @throws {Error} When the element's document has no window (a document outside any browsing context, which paints nothing) / 要素の文書がウィンドウを持たないとき (閲覧の文脈の外の文書で、何も描かれない)
|
|
595
|
+
*/
|
|
596
|
+
const windowOf = (element: Element): Window => {
|
|
597
|
+
const view = element.ownerDocument.defaultView
|
|
598
|
+
if (view === null) {
|
|
599
|
+
throw new Error("[VirtualScroll] the items wrapper belongs to a document without a window, so it has no device-pixel ratio")
|
|
600
|
+
}
|
|
601
|
+
return view
|
|
602
|
+
}
|
|
603
|
+
|
|
523
604
|
/**
|
|
524
605
|
* Pixels emitted per horizontal arrow key press. Matches the ~40px browsers scroll for an arrow key.
|
|
525
606
|
*
|
|
@@ -1047,7 +1128,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1047
1128
|
}: VirtualScrollProps<T>,
|
|
1048
1129
|
ref: React.Ref<VirtualScrollHandle>,
|
|
1049
1130
|
) => {
|
|
1050
|
-
const { width: scrollBarWidth, enableThumbDrag, enableTrackClick, enableArrowButtons, enableScrollToTopBottomButtons, renderThumbOverlay, tapScrollCircleOptions } = scrollBarOptions ?? {}
|
|
1131
|
+
const { width: scrollBarWidth, enableThumbDrag, enableTrackClick, enableArrowButtons, enableArrowButtonTabStops, enableScrollToTopBottomButtons, renderThumbOverlay, tapScrollCircleOptions } = scrollBarOptions ?? {}
|
|
1051
1132
|
const resolvedLabels = useMemo(() => resolveVirtualScrollLabels(locale, labels), [locale, labels])
|
|
1052
1133
|
|
|
1053
1134
|
const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, enableEscapeRowReturn = false, wheelSpeedMultiplier, inertiaOptions, overscrollBehavior, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
|
|
@@ -2426,6 +2507,78 @@ const VirtualScrollInner = <T,>(
|
|
|
2426
2507
|
visibleStartIndex,
|
|
2427
2508
|
])
|
|
2428
2509
|
|
|
2510
|
+
// 行ラッパーを描くウィンドウの装置の画素比。ラッパーの最初の取り付けまで (サーバー描画と初回描画) は null。
|
|
2511
|
+
// 大域の devicePixelRatio と同名にしない — 宣言を消したとき参照が黙って大域のウィンドウの比へ化けるため
|
|
2512
|
+
const [wrapperDevicePixelRatio, setWrapperDevicePixelRatio] = useState<number | null>(null)
|
|
2513
|
+
|
|
2514
|
+
/**
|
|
2515
|
+
* Ref callback of the items wrapper. Commits the device-pixel ratio of the window the wrapper is painted in
|
|
2516
|
+
* (`windowOf`, never the global `window`), and commits it again whenever that ratio changes (browser zoom,
|
|
2517
|
+
* or the window moving to a screen of another density) through a `(resolution: <ratio>dppx)` media-query
|
|
2518
|
+
* watch that is re-armed at each new ratio. The first commit runs in React's commit phase, so a client mount
|
|
2519
|
+
* paints the snapped translate in its first frame. A window without media queries (jsdom) cannot change its
|
|
2520
|
+
* ratio: the ratio read at attach applies and no watch is armed. The callback identity is stable, so React
|
|
2521
|
+
* calls it once per attach. React 19 calls the returned cleanup on detach; it calls the ref with `null`
|
|
2522
|
+
* only when the attach threw (no cleanup exists), and that call does nothing.
|
|
2523
|
+
*
|
|
2524
|
+
* 行ラッパーの ref コールバック。ラッパーを描くウィンドウ (`windowOf`。大域の `window` ではない) の装置の
|
|
2525
|
+
* 画素比を状態へ確定し、比が変わるたび (ブラウザの拡大縮小・密度の違う画面へのウィンドウの移動) に
|
|
2526
|
+
* `(resolution: <比>dppx)` のメディアクエリの監視で確定し直す (監視は新しい比で張り直す)。最初の確定は
|
|
2527
|
+
* React の確定の段で走るので、クライアントでのマウントは最初のフレームから揃えた平行移動で描く。
|
|
2528
|
+
* メディアクエリを持たないウィンドウ (jsdom) は比が変わり得ないので、取り付け時に読んだ比を使い監視は
|
|
2529
|
+
* 張らない。コールバックの同一性は安定しているので、React は取り付け 1 回につき 1 回だけ呼ぶ。取り外し時に
|
|
2530
|
+
* React 19 は返した後始末を呼び、ref を `null` で呼ぶのは取り付けが投げて後始末が無いときだけ (何もしない)。
|
|
2531
|
+
*
|
|
2532
|
+
* @param wrapper - The attached items wrapper, or `null` on the detach that follows an attach that threw / 取り付けた行ラッパー (投げた取り付けの後の取り外しでは `null`)
|
|
2533
|
+
* @returns Cleanup that removes the resolution watch, or `undefined` for `null` / 解像度の監視を外す後始末 (`null` には `undefined`)
|
|
2534
|
+
* @throws {Error} When the wrapper's document has no window (see `windowOf`) / ラッパーの文書がウィンドウを持たないとき (`windowOf` を参照)
|
|
2535
|
+
*/
|
|
2536
|
+
const observeItemsWrapperDevicePixelRatio = useCallback((wrapper: HTMLDivElement | null): (() => void) | undefined => {
|
|
2537
|
+
// 後始末を返した ref を React 19 は null で呼ばない。null が届くのは取り付けが投げた後だけで、外す監視も無い
|
|
2538
|
+
if (wrapper === null) {
|
|
2539
|
+
return undefined
|
|
2540
|
+
}
|
|
2541
|
+
const view = windowOf(wrapper)
|
|
2542
|
+
let resolutionQuery: MediaQueryList | undefined
|
|
2543
|
+
/**
|
|
2544
|
+
* Commits the window's current device-pixel ratio and re-arms the resolution watch at that ratio.
|
|
2545
|
+
*
|
|
2546
|
+
* ウィンドウの現在の装置の画素比を確定し、その比で解像度の監視を張り直す処理。
|
|
2547
|
+
*/
|
|
2548
|
+
const syncDevicePixelRatio = (): void => {
|
|
2549
|
+
resolutionQuery?.removeEventListener("change", syncDevicePixelRatio)
|
|
2550
|
+
const ratio = view.devicePixelRatio
|
|
2551
|
+
setWrapperDevicePixelRatio(ratio)
|
|
2552
|
+
// ❗ `(resolution: Ndppx)` が知らせるのは比 N との一致・不一致の切り替わりだけ。N 以外の比どうしの変化
|
|
2553
|
+
// (もう一度の拡大縮小・さらに別の画面への移動) を捉えるには、変化のたびに新しい比で張り直すしかない。
|
|
2554
|
+
// matchMedia を持たないウィンドウ (jsdom) は比が変わり得ないので監視だけを省く (読んだ比は使う)
|
|
2555
|
+
resolutionQuery = typeof view.matchMedia === "function" ? view.matchMedia(`(resolution: ${ratio}dppx)`) : undefined
|
|
2556
|
+
resolutionQuery?.addEventListener("change", syncDevicePixelRatio)
|
|
2557
|
+
}
|
|
2558
|
+
syncDevicePixelRatio()
|
|
2559
|
+
/**
|
|
2560
|
+
* Removes the resolution watch; React calls it when the wrapper detaches.
|
|
2561
|
+
*
|
|
2562
|
+
* 解像度の監視を外す後始末 (ラッパーの取り外し時に React が呼ぶ)。
|
|
2563
|
+
*/
|
|
2564
|
+
const stopObservingDevicePixelRatio = (): void => {
|
|
2565
|
+
resolutionQuery?.removeEventListener("change", syncDevicePixelRatio)
|
|
2566
|
+
}
|
|
2567
|
+
return stopObservingDevicePixelRatio
|
|
2568
|
+
}, [])
|
|
2569
|
+
|
|
2570
|
+
/**
|
|
2571
|
+
* Renders the items wrapper for the pane's current scroll position (ScrollPane's `children` render prop).
|
|
2572
|
+
* The wrapper carries the scroll offset, the top inset and the render anchor as one translate, snapped to
|
|
2573
|
+
* the device-pixel grid once the wrapper is attached (`snapToDevicePixelGrid`); rows keep their exact tops.
|
|
2574
|
+
*
|
|
2575
|
+
* ペインの現在のスクロール位置に対する行ラッパーを描く処理 (ScrollPane の `children` の描画関数)。スクロール位置・
|
|
2576
|
+
* 上のインセット・描画アンカーを 1 つの平行移動としてラッパーが運び、ラッパーの取り付け後はそれを装置の画素の
|
|
2577
|
+
* 格子へ揃える (`snapToDevicePixelGrid`)。行の上端は厳密値のまま。
|
|
2578
|
+
*
|
|
2579
|
+
* @param currentScrollPosition - The pane's scroll position in PANE coordinates / ペイン座標のスクロール位置
|
|
2580
|
+
* @returns The items wrapper, or the empty state when there are no items / 行ラッパー (項目が無ければ空状態)
|
|
2581
|
+
*/
|
|
2429
2582
|
const renderVisibleItems = useCallback(
|
|
2430
2583
|
(currentScrollPosition: number) => {
|
|
2431
2584
|
const shouldUseThrottledPosition = (callbackThrottleMs ?? 0) > 0
|
|
@@ -2458,6 +2611,12 @@ const VirtualScrollInner = <T,>(
|
|
|
2458
2611
|
// 毎フレームのリフローを避けコンポジタのみで完結させる。
|
|
2459
2612
|
// アンカーは visibleItems と同じ memo の返り値を使い、行 top との整合を保証する。
|
|
2460
2613
|
const containerTop = resolvedInsets.top + renderAnchor - rawEffectiveScrollPosition
|
|
2614
|
+
// ❗ ラッパーは合成層 (willChange: transform) なので、平行移動が装置の画素の端数を持つとブラウザは層ごと
|
|
2615
|
+
// 再標本化する (実測: -2440.5px で 2px の輪郭・2px の隙間・2px の輪が 3+1+3 行に滲み、下辺の隙間が消える)。
|
|
2616
|
+
// 揃えるのは層の平行移動だけでよい — 層の中の行の端数の位置は描画が画素へ揃える。比が決まるのは
|
|
2617
|
+
// ラッパーの取り付け時なので、それまで (サーバー描画と初回描画) は描く画面が無く厳密値のまま書く。
|
|
2618
|
+
// クライアントのマウントは取り付けの確定が paint 前に再描画するので、端数のまま描かれることはない
|
|
2619
|
+
const wrapperTranslateY = wrapperDevicePixelRatio === null ? containerTop : snapToDevicePixelGrid(containerTop, wrapperDevicePixelRatio)
|
|
2461
2620
|
|
|
2462
2621
|
const bottomInset =
|
|
2463
2622
|
resolvedInsets.bottom > 0 ? (
|
|
@@ -2474,19 +2633,21 @@ const VirtualScrollInner = <T,>(
|
|
|
2474
2633
|
|
|
2475
2634
|
Logger.debug("[VirtualScroll] Rendering items", () => ({
|
|
2476
2635
|
containerTop,
|
|
2636
|
+
wrapperDevicePixelRatio,
|
|
2637
|
+
wrapperTranslateY,
|
|
2477
2638
|
logicalScrollPosition,
|
|
2478
2639
|
resolvedInsets,
|
|
2479
2640
|
effectiveScrollPosition,
|
|
2480
2641
|
}))
|
|
2481
2642
|
|
|
2482
2643
|
return (
|
|
2483
|
-
<div className="aqvs-items-wrapper" style={{ top: 0, transform: `translateY(${
|
|
2644
|
+
<div ref={observeItemsWrapperDevicePixelRatio} className="aqvs-items-wrapper" style={{ top: 0, transform: `translateY(${wrapperTranslateY}px)`, willChange: "transform" }}>
|
|
2484
2645
|
{visibleItems}
|
|
2485
2646
|
{bottomInset}
|
|
2486
2647
|
</div>
|
|
2487
2648
|
)
|
|
2488
2649
|
},
|
|
2489
|
-
[callbackThrottleMs, itemCount, fenwickTree, logicalScrollPosition, renderAnchor, renderingEndIndex, renderingStartIndex, resolvedInsets, scrollPosition, viewportSize, visibleItems],
|
|
2650
|
+
[callbackThrottleMs, itemCount, fenwickTree, logicalScrollPosition, observeItemsWrapperDevicePixelRatio, renderAnchor, renderingEndIndex, renderingStartIndex, resolvedInsets, scrollPosition, viewportSize, visibleItems, wrapperDevicePixelRatio],
|
|
2490
2651
|
)
|
|
2491
2652
|
|
|
2492
2653
|
const currentRange = useMemo<VirtualScrollRange>(
|
|
@@ -2602,6 +2763,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2602
2763
|
enableThumbDrag={enableThumbDrag}
|
|
2603
2764
|
enableTrackClick={enableTrackClick}
|
|
2604
2765
|
enableArrowButtons={enableArrowButtons}
|
|
2766
|
+
enableArrowButtonTabStops={enableArrowButtonTabStops}
|
|
2605
2767
|
enablePointerDrag={enablePointerDrag}
|
|
2606
2768
|
pointerDragInputs={pointerDragInputs}
|
|
2607
2769
|
renderThumbOverlay={renderThumbOverlay}
|
|
@@ -60,7 +60,7 @@ export type TapScrollVelocityInput = {
|
|
|
60
60
|
* so never assert bit-equality (`toBe` / `Object.is`) of a pure-axis component against
|
|
61
61
|
* `computeTapScrollSpeed` outside the committed exact fixtures. The rejected order
|
|
62
62
|
* `s * (o / r)` would be bit-exact on pure axes (`o / r = ±1` exactly); the pin below
|
|
63
|
-
* trades that for fixture-order independence (
|
|
63
|
+
* trades that for fixture-order independence (see the evaluation-order pin below).
|
|
64
64
|
* - D2 (radial law under isotropy): when `s_x = s_y = s`, `|v| = s(r)` at every angle.
|
|
65
65
|
* - D3 (separability): `v_a = s_a(r) * u_a(theta)` for a direction-only unit field `u`.
|
|
66
66
|
* - D4 (isotropic direction fidelity — a CHOSEN axiom, not derived): when `s_x = s_y`,
|
|
@@ -69,8 +69,8 @@ export type TapScrollVelocityInput = {
|
|
|
69
69
|
* D1-D3 alone do NOT force this law: they admit the whole family
|
|
70
70
|
* `u = (cos phi(theta), sin phi(theta))` for any continuous strictly increasing angle remap
|
|
71
71
|
* `phi` fixing the four cardinal angles (e.g. `phi(theta) = theta - epsilon * sin(4 theta)`).
|
|
72
|
-
* D4 pins `phi = id`. That family is the
|
|
73
|
-
*
|
|
72
|
+
* D4 pins `phi = id`. That family is the invariant-preserving tuning point for near-axis
|
|
73
|
+
* leakage; the package ships `phi = id`.
|
|
74
74
|
*
|
|
75
75
|
* Evaluation-order pin: each component is computed literally as `s_a(r) * o_a / r`
|
|
76
76
|
* (left-to-right, i.e. `(s * o) / r`). The two orders differ by 1 ulp for SOME inputs —
|
|
@@ -482,7 +482,7 @@
|
|
|
482
482
|
transform: translateX(calc(var(--aqvs-grid-hx-residual, 0px) - var(--aqvs-grid-frozen-width, 0px)));
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
/*
|
|
485
|
+
/* 末尾列クリップ: left の max() は先頭優先の CSS エンコード。
|
|
486
486
|
* ❗ この max() 形は trailingVisibleSize の calc() 版そのもの (クリップ左端 = extent − W_T_vis
|
|
487
487
|
* が恒等) — CSS は JS ヘルパーを読めないための登記済み例外 (単一情報源レジスタのエントリ 1)。 */
|
|
488
488
|
.aqvs-grid-row-trailing {
|
package/src/useGridTapScroll.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { minmax } from "./utils.ts"
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Default corner-relative offset (px) for BOTH grid tap-circle axes — the reachability arm (R)
|
|
19
|
-
* of the three-arm placement law
|
|
19
|
+
* of the three-arm placement law. Derivation: full speed needs
|
|
20
20
|
* `maxVisualDistance = 240 px` of advancing pull toward the far screen edge, which is a hard
|
|
21
21
|
* stop on touch (`touch-action: none`). With `off = −200` the disc center sits
|
|
22
22
|
* `sbw − off − size/2 = 12 + 200 − 20 = 192 px` inboard of each far root edge, giving a
|
|
@@ -26,11 +26,11 @@ import { minmax } from "./utils.ts"
|
|
|
26
26
|
* nav-zone envelope ≥ ~2/3. Deliberately a LITERAL: it must NOT track a consumer-supplied
|
|
27
27
|
* `maxVisualDistance` (coupling two knobs would make one silently move the other), and
|
|
28
28
|
* shrinking `maxVisualDistance` instead is forbidden (it changes the speed law's `maxDistance`
|
|
29
|
-
* input
|
|
29
|
+
* input, so a pure-axis drag would no longer reproduce the bar's own speed law).
|
|
30
30
|
* グリッドタップサークル両軸のコーナー相対既定オフセット (px) — 3 アーム配置則の到達性アーム
|
|
31
31
|
* (R)。導出: 全速には前進方向へ 240 px の引きが要り、タッチではスクリーン端がハードストップ。
|
|
32
32
|
* −200 で中心は各遠端から 192 px 内側 = 最悪ケース引き分率 0.80 (ナビ帯込み 0.66)。意図的な
|
|
33
|
-
* リテラル — 消費側 `maxVisualDistance` へ連動させない (
|
|
33
|
+
* リテラル — 消費側 `maxVisualDistance` へ連動させない (2 つのノブを連動させると一方が他方を黙って動かす)。
|
|
34
34
|
*/
|
|
35
35
|
export const GRID_TAP_CIRCLE_DEFAULT_OFFSET = -200
|
|
36
36
|
|
|
@@ -51,27 +51,27 @@ export type UseGridTapScrollParams = {
|
|
|
51
51
|
enabled: boolean
|
|
52
52
|
/** Shared pull range (= `max(maxVisualDistance, 1)`). / 共有引き範囲 (= `max(maxVisualDistance, 1)`)。 */
|
|
53
53
|
maxDistance: number
|
|
54
|
-
/** Horizontal speed parameters (
|
|
54
|
+
/** Horizontal speed parameters (Pₓ, built from the scroll-band width and the column count). / 横軸速度パラメータ (Pₓ。スクロール帯幅と列数から構成)。 */
|
|
55
55
|
xSpeedParams: TapScrollAxisSpeedParams
|
|
56
|
-
/** Vertical speed parameters (
|
|
56
|
+
/** Vertical speed parameters (P_y, built from the embedded pane height and the scroll-row count). / 縦軸速度パラメータ (P_y。埋め込みペイン高とスクロール行数から構成)。 */
|
|
57
57
|
ySpeedParams: TapScrollAxisSpeedParams
|
|
58
58
|
/** The grid's x apply seam (absolute position in, clamped applied position out). / グリッドの x 適用シーム (絶対位置 → クランプ後位置)。 */
|
|
59
59
|
applyHxRef: { readonly current: (next: number) => number }
|
|
60
60
|
/** Synchronous-fresh hx read. / hx の同期・最新読み。 */
|
|
61
61
|
getHx: () => number
|
|
62
|
-
/** The ONE extracted x clamp law (`getMaxHx` SSOT —
|
|
62
|
+
/** The ONE extracted x clamp law (`getMaxHx` SSOT — never re-derived from the grid's tree). / 唯一抽出の x クランプ則 (`getMaxHx` SSOT — グリッドの木から再導出しない)。 */
|
|
63
63
|
getMaxHx: () => number
|
|
64
|
-
/** y apply seam: wraps `scrollBy(delta)` AND syncs `vyRef.current = applied` before returning (
|
|
64
|
+
/** y apply seam: wraps `scrollBy(delta)` AND syncs `vyRef.current = applied` before returning (NOT a bare handle passthrough — without the sync, x-frame notifications during a diagonal drag carry a stale y). / y 適用シーム: `scrollBy(delta)` をラップし `vyRef.current = applied` を同期してから返す (素のハンドル素通しではない — 同期が無いと対角ドラッグ中の x フレーム通知が stale な y を運ぶ)。 */
|
|
65
65
|
applyVy: (delta: number) => number
|
|
66
66
|
/** Synchronous-fresh vy read — MUST read the same authority `applyVy` resolves against (the embedded pane's position), never a notification mirror with a second writer (v3.6.1 — a lagging mirror base overstates `actualDelta` and drives the residual negative = yo-yo). / vy の同期・最新読み — `applyVy` が解決するのと同一権威 (埋め込みペイン位置) を読むこと。第 2 の書き手を持つ通知鏡像は禁止 (v3.6.1 — 遅延鏡像基準は `actualDelta` を過大化し残差を負へ落とすヨーヨー)。 */
|
|
67
67
|
getVy: () => number
|
|
68
68
|
/** y extent read through the embedded handle's freshness channel (never re-derived). / 埋め込みハンドルの鮮度チャネル経由の y 延長 (再導出禁止)。 */
|
|
69
69
|
getMaxVy: () => number
|
|
70
|
-
/** Pending column anchor — cleared ONLY on frames whose APPLIED hx delta ≠ 0 (
|
|
70
|
+
/** Pending column anchor — cleared ONLY on frames whose APPLIED hx delta ≠ 0 (a request the clamp swallows keeps it). / 保留列アンカー — 適用済み hx デルタ ≠ 0 のフレームのみ解除 (クランプに吸収された要求では残す)。 */
|
|
71
71
|
pendingColAnchorRef: { current: unknown }
|
|
72
|
-
/** x extent-growth freshness key (columnWindow / total width —
|
|
72
|
+
/** x extent-growth freshness key (columnWindow / total width — a change re-arms a loop parked at the edge). / x 延長成長の鮮度キー (columnWindow / 総幅 — 変化で端に止まったループを再武装)。 */
|
|
73
73
|
xExtentFreshness: unknown
|
|
74
|
-
/** y extent-growth freshness key (the vertical range state —
|
|
74
|
+
/** y extent-growth freshness key (the vertical range state — a change re-arms a loop parked at the edge). / y 延長成長の鮮度キー (縦レンジ state — 変化で端に止まったループを再武装)。 */
|
|
75
75
|
yExtentFreshness: unknown
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -89,7 +89,7 @@ export type UseGridTapScrollResult = {
|
|
|
89
89
|
* Owns the unified tap circle's drag state and the two-axis rAF integration loop.
|
|
90
90
|
* 統合タップサークルのドラッグ状態と 2 軸 rAF 積分ループを所有するフック。
|
|
91
91
|
*
|
|
92
|
-
* Per-frame semantics
|
|
92
|
+
* Per-frame semantics:
|
|
93
93
|
*
|
|
94
94
|
* ```text
|
|
95
95
|
* dt = min(max((t − last)/1000, 0), TAP_SCROLL_MAX_FRAME_DELTA_SECONDS); skip if dt ≤ 0
|
|
@@ -186,7 +186,7 @@ export const useGridTapScroll = (params: UseGridTapScrollParams): UseGridTapScro
|
|
|
186
186
|
*/
|
|
187
187
|
const step = useCallback(
|
|
188
188
|
(timestamp: number) => {
|
|
189
|
-
// この rAF ループ意味論は ScrollBar.tsx の tap ループ (stepAutoScroll) と双子 — 片方を直したらもう片方も直すこと
|
|
189
|
+
// この rAF ループ意味論は ScrollBar.tsx の tap ループ (stepAutoScroll) と双子 — 片方を直したらもう片方も直すこと
|
|
190
190
|
const current = paramsRef.current
|
|
191
191
|
const state = tapDragStateRef.current
|
|
192
192
|
if (!state.active || state.direction === 0) {
|