@aiquants/virtualscroll 3.7.1 → 3.8.1
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 +158 -39
- package/README.md +76 -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 +2031 -1990
- 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 +265 -6
- package/src/computeTapScrollVelocity.ts +3 -3
- package/src/styles/virtualscroll.css +1 -1
- package/src/useGridTapScroll.ts +12 -12
package/src/VirtualScroll.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
1
|
+
import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"
|
|
2
2
|
import { resolveVirtualScrollLabels, type VirtualScrollLabelOverrides, type VirtualScrollLocale } from "./labels.ts"
|
|
3
3
|
import { Logger } from "./logger.ts"
|
|
4
4
|
import { ScrollPane, type ScrollPaneContentInsets, type ScrollPaneHandle, type ScrollPaneProps } from "./ScrollPane.tsx"
|
|
@@ -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,216 @@ 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
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Unsubscribe that removes nothing: the subscription of a window without media queries, or of a wrapper with no
|
|
606
|
+
* painting window yet.
|
|
607
|
+
*
|
|
608
|
+
* 何も外さない購読解除 (メディアクエリを持たないウィンドウ、または描くウィンドウがまだ無いラッパーの購読)。
|
|
609
|
+
*/
|
|
610
|
+
const unsubscribeNothing = (): void => {}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Server snapshot of the painting ratio for `useSyncExternalStore`. A server has no screen, so it is `null`
|
|
614
|
+
* and the wrapper keeps the exact translate; hydration renders with it too, so the hydrated markup matches the
|
|
615
|
+
* server HTML, and React replaces it with the snapped translate right after hydration.
|
|
616
|
+
*
|
|
617
|
+
* `useSyncExternalStore` に渡す、描く比のサーバーのスナップショット。サーバーに画面は無いので `null` で、
|
|
618
|
+
* ラッパーは厳密な平行移動のまま。ハイドレーションもこの値で描くのでサーバーの HTML と一致し、
|
|
619
|
+
* ハイドレーションの直後に React が揃えた平行移動へ置き換える。
|
|
620
|
+
*
|
|
621
|
+
* @returns Always `null` / 常に `null`
|
|
622
|
+
*/
|
|
623
|
+
const readServerDevicePixelRatio = (): null => null
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Returns the window of the JavaScript realm that runs React, or `null` outside a browser realm (server rendering).
|
|
627
|
+
* It is the expected painting window of a wrapper that has not attached yet: render cannot see the document it
|
|
628
|
+
* will be inserted into, and a host renders into its own window's document unless it deliberately renders into
|
|
629
|
+
* another window's document (an iframe or an opened window), which the attach detects (see `ItemsWrapper`).
|
|
630
|
+
*
|
|
631
|
+
* React を実行している JavaScript のレルムのウィンドウを返す処理 (ブラウザのレルムの外 = サーバー描画では
|
|
632
|
+
* `null`)。まだ取り付いていないラッパーの、描くと見込むウィンドウ。描画からは挿入先の文書が見えず、ホストは
|
|
633
|
+
* 別のウィンドウの文書 (iframe・開いたウィンドウ) へ意図して描くのでない限り自分のウィンドウの文書へ描くため。
|
|
634
|
+
* 見込みの確かめは取り付けが行う (`ItemsWrapper` を参照)。
|
|
635
|
+
*
|
|
636
|
+
* @returns The realm's window, or `null` without one / レルムのウィンドウ (無ければ `null`)
|
|
637
|
+
*/
|
|
638
|
+
const readRealmWindow = (): Window | null => (typeof window === "undefined" ? null : window)
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Subscribes to the device-pixel-ratio changes of a window (browser zoom, or the window moving to a screen of
|
|
642
|
+
* another density) through a `(resolution: <ratio>dppx)` media-query watch that is re-armed at each new ratio.
|
|
643
|
+
* A window without media queries (jsdom) cannot change its ratio, so nothing is armed there.
|
|
644
|
+
*
|
|
645
|
+
* ウィンドウの装置の画素比の変化 (ブラウザの拡大縮小・密度の違う画面へのウィンドウの移動) を、比が変わる
|
|
646
|
+
* たびに新しい比で張り直す `(resolution: <比>dppx)` のメディアクエリの監視で購読する処理。メディアクエリを
|
|
647
|
+
* 持たないウィンドウ (jsdom) は比が変わり得ないので何も張らない。
|
|
648
|
+
*
|
|
649
|
+
* @param view - Window whose ratio is watched / 比を監視するウィンドウ
|
|
650
|
+
* @param onChange - Called after each ratio change, once the watch is re-armed at the new ratio / 比が変わるたび、新しい比で監視を張り直した後に呼ぶ
|
|
651
|
+
* @returns Unsubscribe that removes the current watch / 今の監視を外す購読解除
|
|
652
|
+
*/
|
|
653
|
+
const subscribeToDevicePixelRatio = (view: Window, onChange: () => void): (() => void) => {
|
|
654
|
+
if (typeof view.matchMedia !== "function") {
|
|
655
|
+
return unsubscribeNothing
|
|
656
|
+
}
|
|
657
|
+
let resolutionQuery = view.matchMedia(`(resolution: ${view.devicePixelRatio}dppx)`)
|
|
658
|
+
/**
|
|
659
|
+
* Re-arms the watch at the window's new ratio, then reports the change.
|
|
660
|
+
*
|
|
661
|
+
* ウィンドウの新しい比で監視を張り直してから変化を知らせる処理。
|
|
662
|
+
*/
|
|
663
|
+
const handleResolutionChange = (): void => {
|
|
664
|
+
resolutionQuery.removeEventListener("change", handleResolutionChange)
|
|
665
|
+
// ❗ `(resolution: Ndppx)` が知らせるのは比 N との一致・不一致の切り替わりだけ。N 以外の比どうしの変化
|
|
666
|
+
// (もう一度の拡大縮小・さらに別の画面への移動) を捉えるには、変化のたびに新しい比で張り直すしかない
|
|
667
|
+
resolutionQuery = view.matchMedia(`(resolution: ${view.devicePixelRatio}dppx)`)
|
|
668
|
+
resolutionQuery.addEventListener("change", handleResolutionChange)
|
|
669
|
+
onChange()
|
|
670
|
+
}
|
|
671
|
+
resolutionQuery.addEventListener("change", handleResolutionChange)
|
|
672
|
+
/**
|
|
673
|
+
* Removes the current watch.
|
|
674
|
+
*
|
|
675
|
+
* 今の監視を外す処理。
|
|
676
|
+
*/
|
|
677
|
+
const unsubscribe = (): void => {
|
|
678
|
+
resolutionQuery.removeEventListener("change", handleResolutionChange)
|
|
679
|
+
}
|
|
680
|
+
return unsubscribe
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Props of `ItemsWrapper`.
|
|
685
|
+
*
|
|
686
|
+
* `ItemsWrapper` の props。
|
|
687
|
+
*/
|
|
688
|
+
type ItemsWrapperProps = {
|
|
689
|
+
/** Exact translate in CSS px: top inset + render anchor - pane position / 厳密な平行移動 (CSS px。上のインセット + 描画アンカー - ペイン位置) */
|
|
690
|
+
readonly translateY: number
|
|
691
|
+
/** The rendered rows and the bottom inset / 描いた行と下のインセット */
|
|
692
|
+
readonly children: ReactNode
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The items wrapper: a compositor layer (`will-change: transform`) moved by `translateY`, snapped to the
|
|
697
|
+
* device-pixel grid of the window that paints it (`snapToDevicePixelGrid`). Rule: the translate that reaches
|
|
698
|
+
* the screen is always snapped with the ratio of the window that paints the wrapper. Render reads the ratio
|
|
699
|
+
* through `useSyncExternalStore` from the expected painting window — the realm's window (`readRealmWindow`)
|
|
700
|
+
* until the wrapper has attached, since render cannot see the target document. The attach confirms it with the
|
|
701
|
+
* wrapper's own window (`windowOf`): when they agree (a list rendered into its own window's document), the first
|
|
702
|
+
* commit is already snapped and nothing is scheduled in the commit phase; when they differ (a list rendered into
|
|
703
|
+
* an iframe or an opened window), the attach switches to that window; the update is scheduled in the commit
|
|
704
|
+
* phase, so React re-renders the wrapper synchronously before the browser paints. Ratio changes re-render
|
|
705
|
+
* through the store subscription (`subscribeToDevicePixelRatio`), outside the commit phase. The server snapshot
|
|
706
|
+
* is `null`: server HTML and hydration carry the exact translate, replaced by the snapped one right after
|
|
707
|
+
* hydration.
|
|
708
|
+
*
|
|
709
|
+
* 行ラッパー。`translateY` で動かす合成層 (`will-change: transform`) で、平行移動は描くウィンドウの装置の画素の
|
|
710
|
+
* 格子へ揃える (`snapToDevicePixelGrid`)。規則は「画面へ届く平行移動は、常にラッパーを描くウィンドウの比で
|
|
711
|
+
* 揃っている」。描画は `useSyncExternalStore` で、描くと見込むウィンドウから比を読む — 描画からは挿入先の文書が
|
|
712
|
+
* 見えないので、ラッパーが取り付くまではレルムのウィンドウ (`readRealmWindow`)。取り付けがラッパー自身の
|
|
713
|
+
* ウィンドウ (`windowOf`) と突き合わせて確かめ、一致すれば (自分のウィンドウの文書に描いた一覧) 最初の確定から
|
|
714
|
+
* 揃っていて確定の段では何も予約しない。違えば (iframe・開いたウィンドウに描いた一覧) 取り付けがそのウィンドウへ
|
|
715
|
+
* 切り替え、この更新は確定の段で予約されるので、React はブラウザの paint の前にラッパーを同期で描き直す。
|
|
716
|
+
* 比の変化はストアの購読 (`subscribeToDevicePixelRatio`) で確定の段の外から描き直す。サーバーのスナップショットは
|
|
717
|
+
* `null` で、サーバーの HTML とハイドレーションは厳密な平行移動を持ち、ハイドレーションの直後に揃えた値へ置き換わる。
|
|
718
|
+
*
|
|
719
|
+
* @param props - The exact translate and the wrapped rows / 厳密な平行移動と包む行
|
|
720
|
+
* @returns The wrapper element / ラッパーの要素
|
|
721
|
+
* @throws {Error} When the wrapper attaches to a document without a window (see `windowOf`) / ウィンドウを持たない文書へ取り付いたとき (`windowOf` を参照)
|
|
722
|
+
* @throws {RangeError} When the painting window reports a ratio that is not a finite number > 0 (see `snapToDevicePixelGrid`) / 描くウィンドウの比が 0 より大きい有限数でないとき (`snapToDevicePixelGrid` を参照)
|
|
723
|
+
*/
|
|
724
|
+
const ItemsWrapper = ({ translateY, children }: ItemsWrapperProps) => {
|
|
725
|
+
const [paintingWindow, setPaintingWindow] = useState<Window | null>(readRealmWindow)
|
|
726
|
+
/**
|
|
727
|
+
* Subscribes the store to the painting window's ratio changes.
|
|
728
|
+
*
|
|
729
|
+
* 描くウィンドウの比の変化へストアを購読させる処理。
|
|
730
|
+
*/
|
|
731
|
+
const subscribe = useCallback((onChange: () => void): (() => void) => (paintingWindow === null ? unsubscribeNothing : subscribeToDevicePixelRatio(paintingWindow, onChange)), [paintingWindow])
|
|
732
|
+
/**
|
|
733
|
+
* Reads the painting window's current ratio (`null` while no window is known).
|
|
734
|
+
*
|
|
735
|
+
* 描くウィンドウの今の比を読む処理 (ウィンドウが分からない間は `null`)。
|
|
736
|
+
*/
|
|
737
|
+
const readDevicePixelRatio = useCallback((): number | null => (paintingWindow === null ? null : paintingWindow.devicePixelRatio), [paintingWindow])
|
|
738
|
+
// 大域の devicePixelRatio と同名にしない — 宣言を消したとき参照が黙って大域のウィンドウの比へ化けるため
|
|
739
|
+
const paintingRatio = useSyncExternalStore(subscribe, readDevicePixelRatio, readServerDevicePixelRatio)
|
|
740
|
+
/**
|
|
741
|
+
* Ref callback that confirms the expected painting window against the window of the wrapper's document.
|
|
742
|
+
*
|
|
743
|
+
* 描くと見込んだウィンドウを、ラッパーの文書のウィンドウと突き合わせて確かめる ref コールバック。
|
|
744
|
+
*/
|
|
745
|
+
const confirmPaintingWindow = useCallback(
|
|
746
|
+
(wrapper: HTMLDivElement | null): void => {
|
|
747
|
+
if (wrapper === null) {
|
|
748
|
+
return
|
|
749
|
+
}
|
|
750
|
+
const view = windowOf(wrapper)
|
|
751
|
+
// 同じウィンドウなら描画で読んだ比が正しく、確定の段の更新 (paint 前の同期の描き直し) は要らない
|
|
752
|
+
if (view !== paintingWindow) {
|
|
753
|
+
setPaintingWindow(view)
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
[paintingWindow],
|
|
757
|
+
)
|
|
758
|
+
// ❗ ラッパーは合成層なので、平行移動が装置の画素の端数を持つとブラウザは層ごと再標本化する (実測: -2440.5px で
|
|
759
|
+
// 2px の輪郭・2px の隙間・2px の輪が 3+1+3 行に滲み、下辺の隙間が消える)。揃えるのは層の平行移動だけでよい —
|
|
760
|
+
// 層の中の行の端数の位置は描画が画素へ揃える
|
|
761
|
+
const wrapperTranslateY = paintingRatio === null ? translateY : snapToDevicePixelGrid(translateY, paintingRatio)
|
|
762
|
+
return (
|
|
763
|
+
<div ref={confirmPaintingWindow} className="aqvs-items-wrapper" style={{ top: 0, transform: `translateY(${wrapperTranslateY}px)`, willChange: "transform" }}>
|
|
764
|
+
{children}
|
|
765
|
+
</div>
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
|
|
523
769
|
/**
|
|
524
770
|
* Pixels emitted per horizontal arrow key press. Matches the ~40px browsers scroll for an arrow key.
|
|
525
771
|
*
|
|
@@ -1047,7 +1293,7 @@ const VirtualScrollInner = <T,>(
|
|
|
1047
1293
|
}: VirtualScrollProps<T>,
|
|
1048
1294
|
ref: React.Ref<VirtualScrollHandle>,
|
|
1049
1295
|
) => {
|
|
1050
|
-
const { width: scrollBarWidth, enableThumbDrag, enableTrackClick, enableArrowButtons, enableScrollToTopBottomButtons, renderThumbOverlay, tapScrollCircleOptions } = scrollBarOptions ?? {}
|
|
1296
|
+
const { width: scrollBarWidth, enableThumbDrag, enableTrackClick, enableArrowButtons, enableArrowButtonTabStops, enableScrollToTopBottomButtons, renderThumbOverlay, tapScrollCircleOptions } = scrollBarOptions ?? {}
|
|
1051
1297
|
const resolvedLabels = useMemo(() => resolveVirtualScrollLabels(locale, labels), [locale, labels])
|
|
1052
1298
|
|
|
1053
1299
|
const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, enableEscapeRowReturn = false, wheelSpeedMultiplier, inertiaOptions, overscrollBehavior, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
|
|
@@ -2426,6 +2672,18 @@ const VirtualScrollInner = <T,>(
|
|
|
2426
2672
|
visibleStartIndex,
|
|
2427
2673
|
])
|
|
2428
2674
|
|
|
2675
|
+
/**
|
|
2676
|
+
* Renders the items wrapper for the pane's current scroll position (ScrollPane's `children` render prop).
|
|
2677
|
+
* The wrapper carries the scroll offset, the top inset and the render anchor as one translate, which
|
|
2678
|
+
* `ItemsWrapper` snaps to the device-pixel grid of the window that paints it; rows keep their exact tops.
|
|
2679
|
+
*
|
|
2680
|
+
* ペインの現在のスクロール位置に対する行ラッパーを描く処理 (ScrollPane の `children` の描画関数)。スクロール位置・
|
|
2681
|
+
* 上のインセット・描画アンカーを 1 つの平行移動としてラッパーが運び、`ItemsWrapper` がそれを描くウィンドウの
|
|
2682
|
+
* 装置の画素の格子へ揃える。行の上端は厳密値のまま。
|
|
2683
|
+
*
|
|
2684
|
+
* @param currentScrollPosition - The pane's scroll position in PANE coordinates / ペイン座標のスクロール位置
|
|
2685
|
+
* @returns The items wrapper, or the empty state when there are no items / 行ラッパー (項目が無ければ空状態)
|
|
2686
|
+
*/
|
|
2429
2687
|
const renderVisibleItems = useCallback(
|
|
2430
2688
|
(currentScrollPosition: number) => {
|
|
2431
2689
|
const shouldUseThrottledPosition = (callbackThrottleMs ?? 0) > 0
|
|
@@ -2480,10 +2738,10 @@ const VirtualScrollInner = <T,>(
|
|
|
2480
2738
|
}))
|
|
2481
2739
|
|
|
2482
2740
|
return (
|
|
2483
|
-
<
|
|
2741
|
+
<ItemsWrapper translateY={containerTop}>
|
|
2484
2742
|
{visibleItems}
|
|
2485
2743
|
{bottomInset}
|
|
2486
|
-
</
|
|
2744
|
+
</ItemsWrapper>
|
|
2487
2745
|
)
|
|
2488
2746
|
},
|
|
2489
2747
|
[callbackThrottleMs, itemCount, fenwickTree, logicalScrollPosition, renderAnchor, renderingEndIndex, renderingStartIndex, resolvedInsets, scrollPosition, viewportSize, visibleItems],
|
|
@@ -2602,6 +2860,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2602
2860
|
enableThumbDrag={enableThumbDrag}
|
|
2603
2861
|
enableTrackClick={enableTrackClick}
|
|
2604
2862
|
enableArrowButtons={enableArrowButtons}
|
|
2863
|
+
enableArrowButtonTabStops={enableArrowButtonTabStops}
|
|
2605
2864
|
enablePointerDrag={enablePointerDrag}
|
|
2606
2865
|
pointerDragInputs={pointerDragInputs}
|
|
2607
2866
|
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) {
|