@aiquants/virtualscroll 3.9.2 → 3.11.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 +149 -0
- package/README.md +135 -40
- package/dist/ScrollBar.d.cts +45 -38
- package/dist/ScrollBar.d.ts +45 -38
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.cts +12 -14
- package/dist/ScrollPane.d.ts +12 -14
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +6 -6
- package/dist/VirtualGrid.d.ts +6 -6
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +184 -30
- package/dist/VirtualScroll.d.ts +184 -30
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/cli.js +26 -47
- package/dist/index.cjs +1 -1
- package/dist/index.js +2385 -2413
- package/dist/labels.d.cts +38 -7
- package/dist/labels.d.ts +38 -7
- package/dist/labels.d.ts.map +1 -1
- package/dist/logger.d.cts +1 -16
- package/dist/logger.d.ts +1 -16
- package/dist/logger.d.ts.map +1 -1
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/dist/useFenwickMapTree.d.cts +21 -1
- package/dist/useFenwickMapTree.d.ts +21 -1
- package/dist/useFenwickMapTree.d.ts.map +1 -1
- package/dist/useLruCache.d.ts.map +1 -1
- package/dist/utils.d.cts +14 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/ScrollBar.tsx +115 -55
- package/src/ScrollPane.tsx +12 -14
- package/src/VirtualGrid.tsx +27 -25
- package/src/VirtualScroll.tsx +486 -234
- package/src/labels.ts +58 -7
- package/src/logger.ts +1 -25
- package/src/styles/virtualscroll.css +51 -3
- package/src/useFenwickMapTree.ts +60 -98
- package/src/useLruCache.ts +14 -8
- package/src/utils.ts +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,155 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiquants/virtualscroll` are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.11.0 (2026-10-05)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`scrollToIndex(index, { align: "nearest" })` — 行を見せる操作**: 行が表示域にまるごと入る最短の距離だけスクロールし、既に
|
|
10
|
+
まるごと入っていれば何もしない (状態を書かず、確定を足さない)。上にかかる行は上端を、下にかかる行は下端を表示域の端へ着地させ、
|
|
11
|
+
表示域より高い行は上端を揃える。見えている端へ戻るピル (`enableScrollToTopBottomButtons`) が覆う端 (その端からピルの向こう側
|
|
12
|
+
まで。画面から測るので文字の大きさに従う — 既定の文字で 31px、200 % で 47px) は表示域に数えない。着地は `"top"` / `"bottom"`
|
|
13
|
+
の揃えとして保つので、後でピルが隠れても何も動かない。`"nearest"` は `offset` を取らない (型が許さず、型の無い呼び出しには
|
|
14
|
+
`RangeError`)。ホストが自分のフォーカスのモデルを持つまま、フォーカスした行を見せるのに使う (README「Revealing a row」)。
|
|
15
|
+
- **スクロールバーとつまみの値の文字 (`aria-valuetext`)**: `aria-valuenow` / `aria-valuemax` は論理 px のままで、長い一覧では
|
|
16
|
+
数千万になり読み上げても位置が分からない。バー (`role="scrollbar"`) とつまみ (`role="slider"`) は、スクロールの割合を
|
|
17
|
+
`locale` の `Intl.NumberFormat` の小数なしの百分率で持つ (en・ja とも `"25%"`。スクロールできないバーは `"0%"`、中身が縮んで
|
|
18
|
+
一時的に最大位置を越えた位置は `"100%"`)。数の書式なのでラベルのカタログのキーは足さない。ポインタ専用
|
|
19
|
+
(`enableArrowButtonTabStops: false`) のバーは支援技術から隠れるので持たない。
|
|
20
|
+
- **`FenwickMapTree.revision`**: 木が保持する値か要素数が変わるたびに上がり、それ以外では変わらない版。木は同一性を保ったまま
|
|
21
|
+
中身が変わるので、接頭辞和から何かを導く呼び出し側は 2 回の読み取りを比べて古さを判定する。
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **フォーカスの移動と行のキー移動は、行を最短で見せる**: `focusItemAtIndex(index)` (`ensureVisible` 既定) と
|
|
26
|
+
`behaviorOptions.enableKeyboardNavigation` の矢印 / Page キー、Escape 行復帰は、`scrollToIndex(index, { align: "nearest" })` と
|
|
27
|
+
同じ規則で行を見せる。従来は表示域の外へかかる行を上端へ揃えたので、下端の行から下へキーを押すと一覧が 1 画面ぶん飛んだ。
|
|
28
|
+
- **ポインタ専用の矢印はキーを受けない**: `enableArrowButtonTabStops: false` の下の矢印は、スクリプトがフォーカスしても Enter /
|
|
29
|
+
Space でスクロールせず、既定の動作も取り消さない (バーは支援技術から隠れ、キーボードのスクロールはホストが持つ)。既定の矢印は
|
|
30
|
+
変わらない。
|
|
31
|
+
- **`ScrollBarProps["onScroll"]` の第 2 引数は必ず渡る**: バーは要求を解いた元の位置をいつも渡していたので、型を省略可から必須へ
|
|
32
|
+
改めた (`onScroll` を実装する側は変更不要)。
|
|
33
|
+
- **網羅率のつめ車の対象に `scripts` を加えた**: `vitest.config.ts` の `COVERAGE_INCLUDE` は `src/**/*.{ts,tsx}` と
|
|
34
|
+
`scripts/**/*.mjs` で、つめ車のスクリプトと入口も分岐・関数とも 100 % の下限で測る。下限を上げた (`useFenwickMapTree.ts`・
|
|
35
|
+
`useLruCache.ts`・`logger.ts`・`cli.server.ts`・`tapScrollCircleSampleVisual.tsx` は 100、`VirtualGrid.tsx` は分岐 98 と関数 100、
|
|
36
|
+
`VirtualScroll.tsx` は分岐 95 と関数 96)。届かない分岐は試験を足し、到達しない分岐は消した (`getLowestSetBit` の 0 と、その呼び出し
|
|
37
|
+
側の打ち切り・`_findIndexLarge` の空の木・`reset` の種の打ち切り・`_modeOrMedian` と標本化の空の入力・同じ計算どうしを比べる
|
|
38
|
+
「Inconsistent Fenwick Tree state」の整合の検査・`useLruCache` の刈り込みの安全弁・`Logger` の使われない `setPrefix` と静的な
|
|
39
|
+
`info`・`VirtualGrid` の総幅と列の左端の死んだ既定値)。
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **差の和が 0 の測り直しの組で、行が古い上端と高さのまま残っていた**: 描いた行を置き直す合図が中身の寸法 (総和) の変化だけ
|
|
44
|
+
だったので、同じバッチの `updateItemSize` の差が打ち消し合う (総和が変わらない) と、行は古い木の上端と高さで描かれたままになった
|
|
45
|
+
(例: 行 1 を +20、行 3 を −20 で、行 2〜4 が古い上端のまま)。描画範囲の memo は木に依存していなかったので、表示域の中の行が
|
|
46
|
+
縮んでも新しく見える行が描かれず (overscan 0 で 300px の空白)、`getRange()` も古い範囲を返した。木を描画の外で変える経路
|
|
47
|
+
(`updateItemSize`・高さの照合・`scrollToIndex` の具現化) は木の版 (`FenwickMapTree.revision`) が動いたときだけ木の版数を進め、
|
|
48
|
+
木を読む memo (描画範囲・描いた行・`getRange()`) はどれもそれに依存するので、総和が変わるかどうかによらず次の確定で置き直す。
|
|
49
|
+
差の和が 0 のバッチが足す確定はちょうど 1 回。
|
|
50
|
+
- **端へ戻るピルが、キーで見せた端の行を覆っていた**: ピルは表示域の端の行の上に描かれ、行を見せる処理はピルを数えなかったので、
|
|
51
|
+
キーで端の行へ進むと行がピルの下に残った (320 × 640・文字 200 % で Bottom ピルが行の中心を覆った)。見せる処理はどれも
|
|
52
|
+
ピルが覆う端を表示域に数えない (上の Added と Changed)。
|
|
53
|
+
- **行が `Number.MAX_SAFE_INTEGER` 以上の一覧の描画範囲が、先頭の可視行を別の規則で決めていた**: 巨大な一覧の経路
|
|
54
|
+
(`computeRenderingRangesHuge`) は位置 0 では先頭の行を、末尾では最後の行を無条件に選び、先頭の行の隠れた量も数えなかったので、
|
|
55
|
+
高さ 0 の先頭行・末尾の高さ 0 の行・行の途中の位置で通常の経路と違う範囲を返した。先頭の可視行を通常の経路と同じ
|
|
56
|
+
`resolveVisibleStartRow` で決め、前方の走査もその行の隠れた量から数える。
|
|
57
|
+
- **`VirtualGrid` がアンマウントの後に読み上げを予約していた**: 埋め込みの一覧はアンマウントで待っていた範囲の通知を同期で
|
|
58
|
+
配り、それはグリッド自身の後始末の後に届くので、ライブリージョンがアンマウントの後に消費側の `buildMessage` を呼んでいた。
|
|
59
|
+
アンマウントの後は読み上げを予約しない。
|
|
60
|
+
- **文書**: `FenwickMapTree.rebuildTree({ materialize: true })` は全行ではなく初期の窓だけを具現化する (リセットと同じ) ことを
|
|
61
|
+
docstring に書いた。「`tabIndex=-1` の矢印もスクリプトからフォーカスすればキーで動く」という約束は、既定の矢印だけの約束へ
|
|
62
|
+
改めた (上の Changed)。
|
|
63
|
+
|
|
64
|
+
## 3.10.0 (2026-10-05)
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- **スクロールバーとつまみの名前 (WCAG 4.1.2)**: どのバーも名前付きの `role="scrollbar"` で、名前付きの `role="slider"` の
|
|
69
|
+
つまみを持つ (従来はどちらも名前が無く、支援技術は名前の無いスクロールバーとスライダーを示していた)。名前はラベルの
|
|
70
|
+
カタログの 4 キー: バーの `verticalScrollBar` / `horizontalScrollBar` (支援技術がロールを付け足すので、2 本のバーを
|
|
71
|
+
区別する向きだけ。en `Vertical` / `Horizontal`、ja `縦方向` / `横方向`) と、つまみの `verticalScrollThumb` /
|
|
72
|
+
`horizontalScrollThumb` (値が表すもの。en `Vertical scroll position` / `Horizontal scroll position`、ja `縦スクロール位置` /
|
|
73
|
+
`横スクロール位置`)。ほかのキーと同じく `labels` で 1 キーずつ上書きでき、空白の値は描画時に `RangeError`。
|
|
74
|
+
`VIRTUAL_SCROLL_LABEL_KEYS` はカタログ順の 11 キー (`scrollRight` の後に 4 つ) になり、型 `VirtualScrollLabels` は必須の
|
|
75
|
+
キーを 4 つ増やす。カタログに `labels` を重ねる代わりに `VirtualScrollLabels` を丸ごと手で組み立てるホストはこの 4 つを
|
|
76
|
+
足し、`VIRTUAL_SCROLL_LABEL_KEYS` を自分のキー表へ連結して件数を固定するホストは件数が 4 増える。
|
|
77
|
+
- **強制配色の描き方 (README「Forced colours」)**: `forced-colors: active` ではブラウザがシステム色でない地色をどれも `Canvas`
|
|
78
|
+
に置き換えるので、地色だけで描く部品にシステム色の描き方を与えた。バーとトラックは内側の辺の 1px の `GrayText` の輪郭、
|
|
79
|
+
つまみは `CanvasText` で塗り、ホバー中とドラッグ中は `Highlight`、つまみのドラッグを切ったとき (`enableThumbDrag: false`) は
|
|
80
|
+
`GrayText`。つまみは強制の置き換えから外す (`forced-color-adjust: none`) ので、地色が `Canvas` に戻ることはない。
|
|
81
|
+
`tapScrollCircleSampleVisual` の光と棒は `CanvasText`。
|
|
82
|
+
- **網羅率のつめ車**: `coverage-floors.json` (ファイルごとの分岐と関数の網羅率の下限) を Vitest がファイルごとの閾値として
|
|
83
|
+
読み (`thresholds: { perFile: true, ... }`。対象は `src` のソースすべてで、どの spec も読み込まないファイルは 0 %)、
|
|
84
|
+
`scripts/ratchet-coverage.mjs` が項目の無いファイルと古い項目を落とし、下限を上げるただ 1 つの手段になる。スクリプトは
|
|
85
|
+
`test:coverage` (全テストの網羅率つきの実行と検証)・`check:coverage`・`coverage:ratchet` (`--write`。新しいファイルは
|
|
86
|
+
100 % のときだけ入る)・`coverage:adopt` (`--adopt`。つめ車を取り入れるときに 1 度だけ、項目の無いファイルを測った
|
|
87
|
+
網羅率で記録する)。`src/devicePixelGrid.ts` は分岐も関数も 100 で入る。
|
|
88
|
+
|
|
89
|
+
### Changed
|
|
90
|
+
|
|
91
|
+
- **`enableArrowButtonTabStops: false` は、ホストがキーボードのスクロールを自分で持つことのただ 1 つの合図**: 従来の `false`
|
|
92
|
+
は矢印 2 個を `tabIndex={-1}` にするだけで、名前の無いバーとつまみは支援技術に残り、バーの押下はフォーカスを動かし
|
|
93
|
+
(Chromium は無効の矢印の押下でも、最も近いフォーカス可能な祖先 = `tabIndex={-1}` のバーへフォーカスを移す)、端へ戻る
|
|
94
|
+
ピルは Tab の止まり先のままだった。いまの `false` でバーはネイティブのスクロールバーと同じポインタ専用の部品になる。
|
|
95
|
+
バーは `aria-hidden="true"` を持ち、矢印は `tabIndex={-1}`、バーとつまみの器は `tabindex` を持たない。バーのどこを
|
|
96
|
+
どのボタンで押しても押下の既定動作を取り消すので、フォーカスは部品へも、ホストが置いた場所の外へも動かない (押下
|
|
97
|
+
そのもの、ポインタのイベントと click は部品へ届く)。ポインタのスクロール (矢印と長押しの連続・トラック・つまみ・
|
|
98
|
+
タップのサークル) は変わらない。`VirtualScroll` の端へ戻るピル (`enableScrollToTopBottomButtons`) も同じ合図に従い、
|
|
99
|
+
覆いは見えている間も `aria-hidden="true"`、ピルは Tab の止まり先にならず、押下はフォーカスを動かさず、click は端へ
|
|
100
|
+
スクロールする。既定 (`true`) は名前が付いたことのほかは変わらない。オプションの名前と意味は `ScrollBar` /
|
|
101
|
+
`ScrollPane` の props と `VirtualScroll` / `VirtualGrid` の `scrollBarOptions` で同じ (グリッドは両方のバーに適用)。
|
|
102
|
+
README の節は「Scrollbar accessibility and the Tab order」に改めた。
|
|
103
|
+
|
|
104
|
+
### Fixed
|
|
105
|
+
|
|
106
|
+
- **窓をずらす 1 段のレイアウトが毎回文書の根から始まっていた**: 行ラッパーの包含ブロックはペインの中身で、これは flex の
|
|
107
|
+
子なので配置の境界 (relayout boundary) になれず、行を足し引きする 1 段のレイアウトは文書の根から始まっていた (日報の
|
|
108
|
+
アプリで 4 倍の CPU のとき、キー 1 回のレイアウトの CPU の中央値は一覧 3.99ms・詳細一覧 8.12ms で、すべてが文書の根から)。
|
|
109
|
+
ラッパーを `.aqvs-items-boundary` (ペインの中身のパディングの上・左・右の辺に付く高さ 0 の絶対配置の箱で、
|
|
110
|
+
`contain: size layout style`) の中に置き、その箱を包含ブロックで配置の境界にした。Chromium 148 で、2,231 個の
|
|
111
|
+
レイアウトのオブジェクトを持つページの 1 行ずつの窓のずれ 60 回は、文書の根からのレイアウト 60 回から、218 個の部分の
|
|
112
|
+
レイアウト 60 回になった。比 1・1.25・1.5・1.75・2・3、明暗、4 つのオフセットで画素は同一。箱は何も描かず、高さ 0 なので
|
|
113
|
+
自分の当たり判定を取らない (行の脇の押下は `background` の部品へ届く)。描画は封じ込めないので、切り取りはペインの中身の
|
|
114
|
+
ままで、窓をずらす 1 段の描き直しの振る舞いは変わらない。`.aqvs-items-boundary` は README の「Custom styling」の表に
|
|
115
|
+
加えた。配置の封じ込めで行のはみ出しはインクのはみ出しになり、ペインの中身のスクロールできる範囲に数えなくなる。
|
|
116
|
+
そのため、ブラウザが自分で行を見せるスクロール (オーバースキャンの行の中のフォーカス可能な要素への Tab・ページ内検索) は
|
|
117
|
+
ペインの中身を動かせず (従来もペインがすぐ 0 へ戻していた)、行の箱が外側のスクローラーの外にあればそちらを動かす。
|
|
118
|
+
スクロールするページで、ウィンドウの下端で終わる一覧のすぐ下の 100px のオーバースキャンの行へ Tab すると、ページが
|
|
119
|
+
300px 動いた。内蔵のキー操作は `focus({ preventScroll: true })` で動かすので影響しない (README「What a scroll step paints」)。
|
|
120
|
+
- **`scrollTo` が行の境界で、上に隠れた行を留めていた**: `scrollTo` は位置を切り捨ててから `findIndexAtOrAfter` で行を
|
|
121
|
+
引いていたので、行 i の下端にちょうど一致する位置では行 i をオフセット `-h_i` で留め、`getScrollAnchor` が知らせる行
|
|
122
|
+
(i + 1、オフセット 0) を留めなかった。`updateItemSize` は保留中の揃えの行を可視の先頭とみなすので、その後で行 i を
|
|
123
|
+
測り直すと、補正も `onScrollAdjust` も無いまま見えている中身が変化の分ずれた (300px の行と 500px の行 9 で
|
|
124
|
+
`scrollTo(3200)` の後に `updateItemSize(9, 300)` を呼ぶと、錨が `{ index: 10, offsetPx: 0 }` から
|
|
125
|
+
`{ index: 10, offsetPx: 200 }` へ動いた)。可視の先頭の境界の規則を 1 つの関数 `resolveVisibleStartRow` (モジュール
|
|
126
|
+
レベル。バレル非公開) にまとめ、`scrollTo`・`getScrollAnchor`・`updateItemSize`・`computeRenderingRanges` がどれもそれを
|
|
127
|
+
呼ぶ。位置を含む行、行の境界ではそこから始まる行をオフセット 0、末尾 (最後の行の下端とその先) では最後の行をその上端で
|
|
128
|
+
返す。同じ測り直しはいま錨 `{ 10, 0 }` を保ち、`"item-resize"` (差 -200) を 1 回知らせる。
|
|
129
|
+
- **強制配色でスクロールバーのつまみが消えていた**: 強制配色の規則はタップのサークルの輪郭だけで、つまみも溝も地色だけで
|
|
130
|
+
描くため、どちらも `Canvas` に置き換わった (Chromium の強制配色で、既定のテーマでも日報のテーマでも、つまみと溝が
|
|
131
|
+
どちらも rgb(255, 255, 255))。上の Added の描き方で、既定のつまみは rgb(255, 255, 255) の溝の上の rgb(0, 0, 0)
|
|
132
|
+
(`CanvasText`) になった。
|
|
133
|
+
|
|
134
|
+
### Tests
|
|
135
|
+
|
|
136
|
+
- `arrowButtonTabStops.spec.tsx` 24 → 38 (4 つの公開入口ごとに、既定のバーとつまみの名前、`false` のバーの `aria-hidden` と
|
|
137
|
+
支援技術から見えるスクロールバーとスライダーが 0 であること、バーの中の Tab の止まり先が 0 でルートとつまみの器が
|
|
138
|
+
`tabindex` を持たないこと、矢印が有効と無効のそれぞれで、バーのどの部品をどのボタン (左・右・中) で押してもフォーカスが
|
|
139
|
+
動かないこと、対照として既定のバーでは右ボタンで押したトラックからフォーカスがバーへ移ること、トラックの押下とつまみの
|
|
140
|
+
ドラッグのスクロール。端へ戻るピルの既定と `false` の 2 件)、`ScrollBar.spec.tsx` +3 (名前の既定値・`locale="ja"` と
|
|
141
|
+
キーごとの上書き・空白の上書きの `RangeError`)、`labels.spec.ts` (11 キーとその値)、`scrollStepRepaint.spec.tsx` +2 (ラッパーの
|
|
142
|
+
親が、位置指定され大きさと配置を封じ込め flex の子でも grid の子でもない高さ 0 の箱であること、グリッドの行も同じ箱の中で、
|
|
143
|
+
空の一覧は箱を描かないこと)、`renderedClassContract.spec.tsx` (表の `.aqvs-items-boundary` を描いた箱で確かめる)。
|
|
144
|
+
新しい spec: `scrollToBoundaryPin.spec.tsx` 8 (規則の関数の内側・境界・末尾・木の過渡状態と `computeRenderingRanges` の
|
|
145
|
+
一致、`scrollTo` の境界の錨・1px 下の対照・すべての行の上端)、`forcedColors.spec.tsx` 4 (地色だけで描く部品をスタイル
|
|
146
|
+
シートから導き、どれもシステム色の規則を持つこと・置き換えから外した部品の状態の言い直し・規則の順序・描いた要素の
|
|
147
|
+
一致)、`devicePixelGrid.server.spec.tsx` 4 (node の環境のサーバー描画: 比 `null`・行ラッパーとつまみの揃える前の
|
|
148
|
+
オフセット・名前付きのスクロールバー)、`devicePixelGrid.realm.spec.tsx` 1 (ウィンドウを持たないレルムのクライアントの描画:
|
|
149
|
+
最初の描画は比 `null` で購読せず、取り付けが要素のウィンドウへ切り替える)、`ratchetCoverage.spec.ts` 17 (つめ車の検証・
|
|
150
|
+
`--write`・`--adopt`・誤りの終了コード 2)。
|
|
151
|
+
- `tests/e2e/home-controls.spec.ts` の Top/Bottom のボタンの試験は、スクロール領域を `data-testid="aqvs-scroll-pane-content"` で探す。行の親の親をたどる
|
|
152
|
+
書き方は、行の外に置いた配置の境界 (`.aqvs-items-boundary`、大きさ 0) を指して「見えない」と判定していた。
|
|
153
|
+
|
|
5
154
|
## 3.9.2 (2026-10-04)
|
|
6
155
|
|
|
7
156
|
### Fixed
|
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ High-performance virtual scrolling component for React with variable item height
|
|
|
13
13
|
- ↔️ **Horizontal Delegation**: Opt-in `onWheelHorizontal` hands horizontal wheel / trackpad (and shift+wheel) gestures to the parent — build frozen-column data grids
|
|
14
14
|
- 🎨 **Customizable**: Flexible styling and theming options
|
|
15
15
|
- 🌀 **Ultrafast Tap Scroll**: Adaptive tap scroll circle that scales speed up to 120× for massive datasets
|
|
16
|
-
- ♿ **Accessibility opt-ins
|
|
17
|
-
- 🌐 **Localization**: The built-in chrome strings (scrollbar arrow labels, scroll-to-edge pills, empty state) ship in English (default) and Japanese via `locale`, with per-key `labels` overrides. See [Localization](#localization)
|
|
16
|
+
- ♿ **Accessibility**: a named scrollbar and thumb, a forced-colours rendering, and opt-ins — Escape row-return (`enableEscapeRowReturn`), a screen-reader live region (`liveRegion`) announcing the visible range with your own wording, and a pointer-only scrollbar for apps with their own keyboard scrolling (`enableArrowButtonTabStops: false`)
|
|
17
|
+
- 🌐 **Localization**: The built-in chrome strings (scrollbar and thumb names, arrow labels, scroll-to-edge pills, empty state) ship in English (default) and Japanese via `locale`, with per-key `labels` overrides. See [Localization](#localization)
|
|
18
18
|
- 🔧 **TypeScript**: Full TypeScript support with comprehensive type definitions
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
@@ -186,8 +186,29 @@ rounding:
|
|
|
186
186
|
|
|
187
187
|
## What a scroll step paints
|
|
188
188
|
|
|
189
|
-
The items wrapper
|
|
190
|
-
|
|
189
|
+
The items wrapper sits in `.aqvs-items-boundary`, its containing block and a relayout boundary: an absolutely
|
|
190
|
+
positioned box attached to the top, left and right padding edges of the row viewport, with a height of 0 and
|
|
191
|
+
`contain: size layout style`. Chromium lays out from a box instead of the document root (a relayout
|
|
192
|
+
boundary) only when the box is neither a flex nor a grid item and, for example, is positioned with size and
|
|
193
|
+
layout containment. The row viewport is a flex item, so without the box a step that mounts or unmounts rows
|
|
194
|
+
lays out from the document root. With it, that layout starts at the box. In Chromium 148, 60 one-row window shifts in a page of 2,231 layout objects (nested flex columns beside
|
|
195
|
+
a 400-item menu and a 600-paragraph side pane) ran 60 layouts from the document root without the box, and 60
|
|
196
|
+
partial layouts of 218 objects with it. With and without the box, a 1280×800 list paints identical pixels at
|
|
197
|
+
ratios 1, 1.25, 1.5, 1.75, 2 and 3, light and dark, at four offsets. The box paints nothing and takes no
|
|
198
|
+
pointer event of its own (its height is 0): the rows overflow it unclipped, the row viewport still clips them,
|
|
199
|
+
and a press beside the rows still reaches the `background` slot.
|
|
200
|
+
|
|
201
|
+
Layout containment turns that overflow into ink overflow, so the rows are no longer part of the row
|
|
202
|
+
viewport's scrollable area. A scroll the browser makes on its own to reveal a row outside the viewport — Tab
|
|
203
|
+
into a focusable element of an overscan row, a find-in-page match there — can no longer scroll the row
|
|
204
|
+
viewport (which the pane reset to 0 at once anyway). It scrolls the nearest outer scroller instead whenever
|
|
205
|
+
the row's box lies outside that scroller's view: in a page that scrolls, Tab into a 100 px overscan row just
|
|
206
|
+
below a list that ends at the bottom of a 500 px window centres that row's box and scrolls the page by
|
|
207
|
+
300 px. Focus rows with `focus({ preventScroll: true })`, as the built-in keyboard navigation does, or keep
|
|
208
|
+
one Tab stop per list (roving focus), so that Tab never lands in an overscan row.
|
|
209
|
+
|
|
210
|
+
The items wrapper itself is a plain compositor layer: `will-change: transform`, moved by a 2D translate that
|
|
211
|
+
is snapped to the device-pixel grid (see above), with nothing that takes its subtree out of 2D compositing.
|
|
191
212
|
Measured in Chromium 148 (1280×800, ratio 1, overscan 15, rows whose text ends in an ellipsis; 20 steps,
|
|
192
213
|
3 runs, median of the run medians):
|
|
193
214
|
|
|
@@ -214,12 +235,12 @@ Measured in Chromium 148 (1280×800, ratio 1, overscan 15, rows whose text ends
|
|
|
214
235
|
pixels, at most one blended pixel, floor(2 × ratio) gap pixels and floor(2 × ratio) outline pixels.
|
|
215
236
|
- The scrollbar thumb moves the same way. Its wrapper stays at `top: 0` (`left: 0` on a horizontal bar) and is
|
|
216
237
|
moved by a 2D translate (`translateY`, `translateX` on a horizontal bar) snapped to the same device-pixel
|
|
217
|
-
grid, with no transition, so a step writes only that `transform` and the slider's
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
9
|
|
222
|
-
unsnapped offset.
|
|
238
|
+
grid, with no transition, so a step writes only that `transform` and the slider's value attributes
|
|
239
|
+
(`aria-valuenow`, and `aria-valuetext` when the whole percent changes), and the thumb adds no layout. A
|
|
240
|
+
thumb positioned by `top` adds a layout from the document root to every step: in Chromium, 60 steps of
|
|
241
|
+
40 px that kept the rendering window (200,000 rows of 160 px inside nested flex columns beside a 600-row
|
|
242
|
+
sidebar) ran 60 layouts in 13.9 ms with the thumb moved by `top`, and 30 layouts in 9.1 ms with the thumb
|
|
243
|
+
held still. `thumbPosition` in the `renderThumbOverlay` props stays the exact, unsnapped offset.
|
|
223
244
|
|
|
224
245
|
## Position changes the list makes on its own
|
|
225
246
|
|
|
@@ -279,6 +300,28 @@ render in them keep their own transitions. A host that does not load the package
|
|
|
279
300
|
transitions. In Chromium 148 the computed `transition-duration` of these parts drops from 0.08–0.5 s to
|
|
280
301
|
`0s`, while a row's own 0.2 s transition stays 0.2 s.
|
|
281
302
|
|
|
303
|
+
## Forced colours
|
|
304
|
+
|
|
305
|
+
Under `forced-colors: active` (for example a Windows contrast theme) the browser replaces every background
|
|
306
|
+
colour that is not a system colour with `Canvas`. A part painted by its background alone would then vanish
|
|
307
|
+
into its surroundings — the thumb would be `Canvas` on a `Canvas` track — so the shipped stylesheet gives
|
|
308
|
+
each such part a system-colour rendering:
|
|
309
|
+
|
|
310
|
+
- the bar and its track: a 1 px `GrayText` outline on their inner edge (the bar holds the arrows and the
|
|
311
|
+
track; the track is the range the thumb moves in);
|
|
312
|
+
- the thumb: filled with `CanvasText`, with `Highlight` while it is hovered or dragged and `GrayText` while
|
|
313
|
+
thumb dragging is off (`enableThumbDrag: false`). The thumb opts out of the browser's forcing
|
|
314
|
+
(`forced-color-adjust: none`), so its fill is never replaced by `Canvas`;
|
|
315
|
+
- the tap scroll circle: a 1 px `CanvasText` outline, since its gradient visual flattens;
|
|
316
|
+
- the bright spot and the rod of `tapScrollCircleSampleVisual`: `CanvasText`, since they show the direction
|
|
317
|
+
and the amount of the pull.
|
|
318
|
+
|
|
319
|
+
The other parts draw with text colour, borders or outlines, which the browser maps to system colours itself.
|
|
320
|
+
In Chromium 148 with forced colours emulated, the default thumb paints rgb(0, 0, 0) (`CanvasText`) on an
|
|
321
|
+
rgb(255, 255, 255) track instead of white on white. A host rule that restyles the thumb still wins here (see
|
|
322
|
+
[Custom styling](#custom-styling)); since the thumb opts out of forcing, the host's colour is then painted as
|
|
323
|
+
it is, so restate it inside `@media (forced-colors: active)` with system colours if you theme the thumb.
|
|
324
|
+
|
|
282
325
|
## Horizontal Scrolling
|
|
283
326
|
|
|
284
327
|
`VirtualScroll` virtualizes and scrolls the **vertical** axis only. To add a horizontal axis — e.g. a
|
|
@@ -471,16 +514,31 @@ itself has focus (an `Escape` on the row means whatever *you* decide — e.g. de
|
|
|
471
514
|
does act it calls `preventDefault()` only and does not stop propagation, the same contract as the
|
|
472
515
|
arrow keys.
|
|
473
516
|
|
|
474
|
-
## Scrollbar
|
|
517
|
+
## Scrollbar accessibility and the Tab order
|
|
518
|
+
|
|
519
|
+
Every bar is exposed to assistive technology as a named `role="scrollbar"` holding a named
|
|
520
|
+
`role="slider"` thumb, whose value is the scroll position (`aria-valuenow`, `aria-valuemin`,
|
|
521
|
+
`aria-valuemax`). The names come from the label catalog: `verticalScrollBar` / `horizontalScrollBar`
|
|
522
|
+
for the bar and `verticalScrollThumb` / `horizontalScrollThumb` for the thumb (see
|
|
523
|
+
[Localization](#localization)). Assistive technology appends the role itself, so the bar's name
|
|
524
|
+
carries only what tells the two bars of a grid apart.
|
|
475
525
|
|
|
476
|
-
The
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
and
|
|
526
|
+
The value counts px, which runs to tens of millions on a long list, so the bar and the thumb also
|
|
527
|
+
carry `aria-valuetext`: the scroll fraction as a whole percent formatted for the bar's `locale`
|
|
528
|
+
(`Intl.NumberFormat` with `style: "percent"`, `"25%"` in `en` and `ja`). A bar with nothing to scroll
|
|
529
|
+
reads `"0%"`, and a position briefly past the end (after the content shrinks) reads `"100%"`.
|
|
530
|
+
|
|
531
|
+
The bar's two arrow buttons are Tab stops by default, because they are the only scrolling control a
|
|
532
|
+
keyboard user can reach with Tab: the pane moves its content by transform (there is no native
|
|
533
|
+
scroller for the browser to make focusable) and the rows are not Tab stops. Focus an arrow and
|
|
534
|
+
Enter / Space scrolls one step. The arrows are descendants of the `role="scrollbar"` element, whose
|
|
535
|
+
children ARIA 1.2 makes presentational, so whether assistive technology lists them as buttons of
|
|
536
|
+
their own is the browser's choice (Chromium does). While the arrows are disabled
|
|
537
|
+
(`enableArrowButtons: false`, or nothing to scroll) they are not focusable at all.
|
|
480
538
|
|
|
481
539
|
When your app already provides keyboard scrolling — roving focus with Arrow / Page / Home / End on
|
|
482
|
-
the rows, a grid keyboard model — the
|
|
483
|
-
|
|
540
|
+
the rows, a grid keyboard model — the bar only duplicates that path. Say so with the one signal,
|
|
541
|
+
`enableArrowButtonTabStops: false`:
|
|
484
542
|
|
|
485
543
|
```tsx
|
|
486
544
|
<VirtualScroll
|
|
@@ -489,16 +547,44 @@ the Tab order:
|
|
|
489
547
|
/>
|
|
490
548
|
```
|
|
491
549
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
550
|
+
The bar then becomes what a native scrollbar is, a pointer-only control:
|
|
551
|
+
|
|
552
|
+
- The bar carries `aria-hidden="true"`, so assistive technology meets no scrollbar and no slider for
|
|
553
|
+
this viewport; your keyboard model is the one it announces.
|
|
554
|
+
- Nothing in the bar is a Tab stop: the arrows get `tabIndex={-1}`, and the bar and the thumb wrapper
|
|
555
|
+
carry no `tabindex`. The arrows answer the pointer only: even when a script focuses one, Enter and
|
|
556
|
+
Space do nothing there (they are not default-prevented either), so the hidden bar never scrolls
|
|
557
|
+
alongside your keyboard model. The bar carries no `aria-valuetext` either.
|
|
558
|
+
- A press anywhere on the bar, with any button and on a disabled arrow too, cancels the press's
|
|
559
|
+
default action, so it never moves focus: neither onto a part of the bar nor away from where your
|
|
560
|
+
app put it. The press itself, its pointer events and its click still reach the part.
|
|
561
|
+
- Pointer scrolling is unchanged: the arrows (with press-and-hold repeat), the track, the thumb and
|
|
562
|
+
the tap circle work exactly as by default.
|
|
563
|
+
- The scroll-to-edge pills of `VirtualScroll` (`enableScrollToTopBottomButtons`) follow the same
|
|
564
|
+
signal: their overlay carries `aria-hidden="true"` while it shows, the pills are never Tab stops, a
|
|
565
|
+
press on one never moves focus, and its click still scrolls to the edge.
|
|
566
|
+
|
|
567
|
+
The option has one name and one meaning everywhere: `ScrollBar` and `ScrollPane` props, and
|
|
568
|
+
`scrollBarOptions` of `VirtualScroll` and `VirtualGrid` (the grid applies it to both of its bars).
|
|
569
|
+
|
|
570
|
+
## Revealing a row
|
|
571
|
+
|
|
572
|
+
One rule brings a row into view, whoever asks: `scrollToIndex(index, { align: "nearest" })`, `focusItemAtIndex(index)` and
|
|
573
|
+
the row keyboard steps of `behaviorOptions.enableKeyboardNavigation` (Arrow / Page keys, and the Escape row-return). It scrolls
|
|
574
|
+
the least distance that puts the row wholly inside the viewport, and nothing at all when it already is: a row above lands with
|
|
575
|
+
its top at the top, a row below with its bottom at the bottom, and a row taller than the viewport with its top at the top — the
|
|
576
|
+
way a native list scrolls to its focused option.
|
|
577
|
+
|
|
578
|
+
The auto-hiding Top / Bottom pills (`scrollBarOptions.enableScrollToTopBottomButtons`) are drawn over the first and last
|
|
579
|
+
visible rows. While a pill shows, the edge it covers — from that edge to the pill's far side, measured on screen and divided
|
|
580
|
+
back by any ancestor `transform` scale, so it follows the text size — does not count as inside: a row there is revealed below
|
|
581
|
+
the Top pill or above the Bottom pill. The landing is kept like a `"top"` / `"bottom"` alignment, so nothing moves when the
|
|
582
|
+
pill hides. `"top"`, `"bottom"` and `"center"` keep aligning to the viewport itself, pill or not.
|
|
583
|
+
|
|
584
|
+
```tsx
|
|
585
|
+
// keep your own focus model, let the list bring the focused row into view
|
|
586
|
+
handle.scrollToIndex(focusedIndex, { align: "nearest" })
|
|
587
|
+
```
|
|
502
588
|
|
|
503
589
|
## Screen-reader live region
|
|
504
590
|
|
|
@@ -518,7 +604,7 @@ settles:
|
|
|
518
604
|
```
|
|
519
605
|
|
|
520
606
|
The live region has **no built-in strings** — its wording and language are yours, and `locale` /
|
|
521
|
-
`labels` do not touch it (the built-in catalog covers only the
|
|
607
|
+
`labels` do not touch it (the built-in catalog covers only the eleven chrome strings, see
|
|
522
608
|
[Localization](#localization)). `format` returning the same string leaves the DOM untouched; `""`
|
|
523
609
|
clears the region. An invalid `debounceMs` (negative / non-finite) logs a warning and disables
|
|
524
610
|
announcements instead of silently substituting the default. ⚠️ If your app already maintains its
|
|
@@ -526,7 +612,7 @@ own live region for the list, keep using `onRangeChange` instead — two regions
|
|
|
526
612
|
|
|
527
613
|
## Localization
|
|
528
614
|
|
|
529
|
-
The components render exactly **
|
|
615
|
+
The components render exactly **eleven strings of their own** (UI chrome). They come from a built-in
|
|
530
616
|
catalog in English (`"en"`, the default) and Japanese (`"ja"`):
|
|
531
617
|
|
|
532
618
|
| Key | Where it appears | `en` | `ja` |
|
|
@@ -535,10 +621,17 @@ catalog in English (`"en"`, the default) and Japanese (`"ja"`):
|
|
|
535
621
|
| `scrollDown` | vertical ScrollBar end arrow (`aria-label`) | Scroll down | 下へスクロール |
|
|
536
622
|
| `scrollLeft` | horizontal ScrollBar start arrow (`aria-label`) | Scroll left | 左へスクロール |
|
|
537
623
|
| `scrollRight` | horizontal ScrollBar end arrow (`aria-label`) | Scroll right | 右へスクロール |
|
|
624
|
+
| `verticalScrollBar` | vertical ScrollBar (`role="scrollbar"`, `aria-label`) | Vertical | 縦方向 |
|
|
625
|
+
| `horizontalScrollBar` | horizontal ScrollBar (`role="scrollbar"`, `aria-label`) | Horizontal | 横方向 |
|
|
626
|
+
| `verticalScrollThumb` | vertical ScrollBar thumb (`role="slider"`, `aria-label`) | Vertical scroll position | 縦スクロール位置 |
|
|
627
|
+
| `horizontalScrollThumb` | horizontal ScrollBar thumb (`role="slider"`, `aria-label`) | Horizontal scroll position | 横スクロール位置 |
|
|
538
628
|
| `scrollToTop` | VirtualScroll scroll-to-top pill (`enableScrollToTopBottomButtons`) | Top | 先頭へ |
|
|
539
629
|
| `scrollToBottom` | VirtualScroll scroll-to-bottom pill (`enableScrollToTopBottomButtons`) | Bottom | 末尾へ |
|
|
540
630
|
| `noItems` | VirtualScroll empty state (`itemCount === 0`; also VirtualGrid with no scroll rows) | No items | 項目がありません |
|
|
541
631
|
|
|
632
|
+
`locale` also formats the scroll bar's value text (`aria-valuetext`, the scroll fraction as a whole percent through
|
|
633
|
+
`Intl.NumberFormat`). That is a number format, not a catalog string, so it has no key and `labels` cannot override it.
|
|
634
|
+
|
|
542
635
|
Pick the language with `locale` and override single keys with `labels`, which are laid over the
|
|
543
636
|
catalog of `locale`:
|
|
544
637
|
|
|
@@ -583,7 +676,7 @@ catalog of `locale`:
|
|
|
583
676
|
a `lang` on an ancestor when the list's language differs from the page).
|
|
584
677
|
- Live-region wording stays yours (see [Screen-reader live region](#screen-reader-live-region)).
|
|
585
678
|
|
|
586
|
-
Exported API: `VIRTUAL_SCROLL_LOCALES` (`["en", "ja"]`), `VIRTUAL_SCROLL_LABEL_KEYS` (the
|
|
679
|
+
Exported API: `VIRTUAL_SCROLL_LOCALES` (`["en", "ja"]`), `VIRTUAL_SCROLL_LABEL_KEYS` (the eleven keys, in catalog order),
|
|
587
680
|
`VIRTUAL_SCROLL_LABEL_CATALOGS` (the frozen catalogs), `resolveVirtualScrollLocale(locale)` (`undefined`
|
|
588
681
|
→ `"en"`, otherwise the value or a `RangeError`), `resolveVirtualScrollLabels(locale, labels)` (the
|
|
589
682
|
effective frozen labels — the catalog object itself when `labels` is `undefined`), and the types
|
|
@@ -643,8 +736,9 @@ Contracts:
|
|
|
643
736
|
axis and consumes those deltas itself), and on the handle `scrollToIndex` (use `scrollToCell`)
|
|
644
737
|
and `getFenwickTreeTotalHeight` / `getFenwickSize` (use `getContentSize` and the counts you pass).
|
|
645
738
|
- `locale` / `labels` (see [Localization](#localization)) are forwarded to BOTH the embedded
|
|
646
|
-
`VirtualScroll` (vertical arrows and the "No items" empty state shown for 0 rows,
|
|
647
|
-
set or a degenerate band) and the horizontal `ScrollBar` (left / right
|
|
739
|
+
`VirtualScroll` (the vertical bar's names and arrows, and the "No items" empty state shown for 0 rows,
|
|
740
|
+
an all-frozen row set or a degenerate band) and the horizontal `ScrollBar` (its names and left / right
|
|
741
|
+
arrows).
|
|
648
742
|
- `scrollBarOptions`: the bar-local members — `width`, `enableThumbDrag`, `enableTrackClick`,
|
|
649
743
|
`enableArrowButtons` and `enableArrowButtonTabStops` — apply to BOTH bars alike, so no setting
|
|
650
744
|
covers only half of the grid. `tapScrollCircleOptions` configures the single two-axis circle;
|
|
@@ -805,7 +899,7 @@ exists in the DOM.
|
|
|
805
899
|
| `behaviorOptions` | `VirtualScrollBehaviorOptions` | ❌ | Options for scrolling behavior |
|
|
806
900
|
| `liveRegion` | `VirtualScrollLiveRegionOptions` | ❌ | Opt-in screen-reader live region announcing the visible range (nothing rendered when omitted). See [Screen-reader live region](#screen-reader-live-region) |
|
|
807
901
|
| `contentProps` | `React.AriaAttributes & { id?: string; role?: React.AriaRole }` | ❌ | ARIA / identity attributes for the scrollable **content** element — the correct host for a composite widget role. See [Composite widget roles](#composite-widget-roles-listbox--tree--grid). |
|
|
808
|
-
| `locale` | `VirtualScrollLocale` (`"en" \| "ja"`) | ❌ | UI chrome language of the built-in strings:
|
|
902
|
+
| `locale` | `VirtualScrollLocale` (`"en" \| "ja"`) | ❌ | UI chrome language of the built-in strings: the scrollbar's names (bar, thumb, arrows), scroll-to-edge pills, empty state (default: `"en"`). Unsupported values throw a `RangeError`. See [Localization](#localization) |
|
|
809
903
|
| `labels` | `VirtualScrollLabelOverrides` | ❌ | Per-key overrides laid over the catalog of `locale`: a plain object (prototype `Object.prototype` or `null`) whose own properties only are read. Unknown keys, blank or non-string values, and non-plain objects (arrays, class instances) throw a `RangeError`. See [Localization](#localization) |
|
|
810
904
|
|
|
811
905
|
### VirtualScrollScrollBarOptions
|
|
@@ -816,8 +910,8 @@ exists in the DOM.
|
|
|
816
910
|
| `enableThumbDrag` | `boolean` | Enable dragging the scrollbar thumb (default: true) |
|
|
817
911
|
| `enableTrackClick` | `boolean` | Enable clicking the scrollbar track (default: true) |
|
|
818
912
|
| `enableArrowButtons` | `boolean` | Enable arrow buttons on the scrollbar (default: true) |
|
|
819
|
-
| `enableArrowButtonTabStops` | `boolean` |
|
|
820
|
-
| `enableScrollToTopBottomButtons` | `boolean` | Enable the auto-hiding Top/Bottom pills (texts from `labels.scrollToTop` / `labels.scrollToBottom`; default: false) |
|
|
913
|
+
| `enableArrowButtonTabStops` | `boolean` | Whether the scrollbar is the keyboard user's scrolling control (default: true: a named scrollbar and thumb, the two arrows in the Tab order). Set `false` when your app already owns keyboard scrolling of the list: the bar and the scroll-to-edge pills become pointer-only (`aria-hidden`, no Tab stop, a press never moves focus) and pointer scrolling is unchanged. See [Scrollbar accessibility and the Tab order](#scrollbar-accessibility-and-the-tab-order) |
|
|
914
|
+
| `enableScrollToTopBottomButtons` | `boolean` | Enable the auto-hiding Top/Bottom pills (texts from `labels.scrollToTop` / `labels.scrollToBottom`; default: false). A pill is drawn over the rows at its edge; while it shows, every reveal keeps rows out from under it (see [Revealing a row](#revealing-a-row)) |
|
|
821
915
|
| `renderThumbOverlay` | `(props: ScrollBarThumbOverlayRenderProps) => ReactNode` | Render prop to anchor custom UI near the scrollbar thumb |
|
|
822
916
|
| `tapScrollCircleOptions` | `ScrollBarTapCircleOptions` | Customization for the auxiliary tap scroll circle |
|
|
823
917
|
|
|
@@ -827,7 +921,7 @@ exists in the DOM.
|
|
|
827
921
|
| --- | --- | --- |
|
|
828
922
|
| `enablePointerDrag` | `boolean` | Enable dragging the content area to scroll (default: true). ⚠️ Setting this to `false` removes the only way to scroll on touch devices — the pane is transform-based and has no native scroller. Use `pointerDragInputs` to exclude a single pointer type instead. |
|
|
829
923
|
| `pointerDragInputs` | `readonly ("mouse" \| "pen" \| "touch")[]` | Pointer types allowed to drag-scroll the content area (default: all three). `touch-action: none` is applied only when `"touch"` or `"pen"` is included. |
|
|
830
|
-
| `enableKeyboardNavigation` | `boolean` | Enable keyboard navigation (default: true). Arrow / Page keys move row focus. ⚠️ They fire **only when the row wrapper itself is the event target** — focus inside a row (a `role="slider"` cell, a link, a nested scroll region) keeps its own keys |
|
|
924
|
+
| `enableKeyboardNavigation` | `boolean` | Enable keyboard navigation (default: true). Arrow / Page keys move row focus and reveal the focused row (see [Revealing a row](#revealing-a-row)). ⚠️ They fire **only when the row wrapper itself is the event target** — focus inside a row (a `role="slider"` cell, a link, a nested scroll region) keeps its own keys |
|
|
831
925
|
| `enableEscapeRowReturn` | `boolean` | Opt-in (default: false): pressing `Escape` while focus sits on an element **inside** a row returns focus to the row wrapper. Listens in the bubble phase and respects `preventDefault` / `stopPropagation` / IME composition, so widgets inside the row keep first claim on their own `Escape`. Requires `enableKeyboardNavigation` |
|
|
832
926
|
| `wheelSpeedMultiplier` | `number` | Multiplier for mouse wheel scrolling speed (default: 1) |
|
|
833
927
|
| `inertiaOptions` | `ScrollPaneInertiaOptions` | Physics tuning for drag inertia |
|
|
@@ -839,20 +933,20 @@ exists in the DOM.
|
|
|
839
933
|
|
|
840
934
|
| Method | Type | Description |
|
|
841
935
|
| --- | --- | --- |
|
|
842
|
-
| `scrollTo` | `(position: number \| ((prev: number) => number)) => number` | **Jump** to a **logical** position (updater receives the current logical position). Returns the applied **logical** position (2.0.0). ⚠️ Not for bridging continuous input — see [Bridging input from outside the pane](#bridging-input-from-outside-the-pane) |
|
|
936
|
+
| `scrollTo` | `(position: number \| ((prev: number) => number)) => number` | **Jump** to a **logical** position (updater receives the current logical position). Returns the applied **logical** position (2.0.0). The position is held as the first visible row there plus the offset of its top — the row `getScrollAnchor()` reports: a position on a row boundary holds the row that starts there, at offset 0 — so a later size change above that row is compensated (`onScrollAdjust`, `"item-resize"`). ⚠️ Not for bridging continuous input — see [Bridging input from outside the pane](#bridging-input-from-outside-the-pane) |
|
|
843
937
|
| `scrollBy` | `(delta: number) => number` | Scroll **by a delta** with the pane's own wheel semantics (float accumulation, no anchor). Use this — not `scrollTo` — to bridge continuous input from outside the pane. Returns the applied **logical** position |
|
|
844
938
|
| `applyWheel` | `(event: WheelEvent) => boolean` | Apply one wheel event with the pane's own rules; returns whether it was consumed. The single entry point for wheel input originating outside the pane — normally reached through `useWheelBridge` |
|
|
845
939
|
| *(ScrollPaneHandle only)* `scrollTo` | `(pos, dimsOverride?)` | The pane-level jump. `dimsOverride` is an explicit clamp-space for callers that know **fresher-than-committed** dims (the layout-shift compensation passes `committed + delta`). Do not pass it casually — a wrong override clamps against dims that are not on screen |
|
|
846
|
-
| `scrollToIndex` | `(index: number, options?: { align?: "top" \| "bottom" \| "center"; offset?: number }) => void` | Scroll to specific item index with optional alignment and offset. Lands exactly on the aligned position (clamped to the content); a `"top"` (default) or `"bottom"` alignment is remembered there, so the device-pixel snap keeps that edge — see [Device-pixel snapping](#device-pixel-snapping) |
|
|
940
|
+
| `scrollToIndex` | `(index: number, options?: { align?: "top" \| "bottom" \| "center"; offset?: number } \| { align: "nearest" }) => void` | Scroll to specific item index with optional alignment and offset. Lands exactly on the aligned position (clamped to the content); a `"top"` (default) or `"bottom"` alignment is remembered there, so the device-pixel snap keeps that edge — see [Device-pixel snapping](#device-pixel-snapping). `"top"`, `"bottom"` and `"center"` align to the viewport itself. `"nearest"` is the reveal: the least scroll that brings the row wholly into view, keeping it out from under a visible scroll-to-edge pill, and nothing at all when the row is already wholly in view — see [Revealing a row](#revealing-a-row). `"nearest"` takes no `offset` (passing one throws a `RangeError`) |
|
|
847
941
|
| `getScrollPosition` | `() => number` | Get current **logical** scroll position (2.0.0; `-1` when the pane is not connected) |
|
|
848
942
|
| `getContentSize` | `() => number` | Get total content size (insets included). Returns the `-1` sentinel while the pane is unconnected |
|
|
849
943
|
| `getViewportSize` | `() => number` | Get viewport size. Returns the `-1` sentinel while the pane is unconnected |
|
|
850
|
-
| `focusItemAtIndex` | `(index: number, options?: { ensureVisible?: boolean }) => void` | Focus
|
|
944
|
+
| `focusItemAtIndex` | `(index: number, options?: { ensureVisible?: boolean }) => void` | Focus the row at an index (with `enableKeyboardNavigation`). With `ensureVisible` (default `true`) the row is first revealed exactly like `scrollToIndex(index, { align: "nearest" })`; with `false` a rendered row takes focus without scrolling |
|
|
851
945
|
| `getRange` | `() => VirtualScrollRange` | Get current range information (updated one render behind) |
|
|
852
946
|
| `getScrollAnchor` | `() => { index: number; offsetPx: number } \| null` | Capture the current top visible row anchor for exact restore via `initialScrollAnchor` (`null` when `itemCount` is 0) |
|
|
853
947
|
| `getFenwickTreeTotalHeight` | `() => number` | Total content height managed by the Fenwick tree (insets excluded) |
|
|
854
948
|
| `getFenwickSize` | `() => number` | Item count managed by the Fenwick tree |
|
|
855
|
-
| `updateItemSize` | `(index: number, size: number) => void` | Manually update one item's size (with layout-shift compensation: a row above the first visible row moves the position by the same delta before this returns, reported through `onScrollAdjust` as `"item-resize"`). `getItemHeight(index)` must return the same value afterwards |
|
|
949
|
+
| `updateItemSize` | `(index: number, size: number) => void` | Manually update one item's size (with layout-shift compensation: a row above the first visible row moves the position by the same delta before this returns, reported through `onScrollAdjust` as `"item-resize"`). `getItemHeight(index)` must return the same value afterwards. Every change of a row height made through `updateItemSize` or found by the height reconciliation re-places the rendered rows and re-resolves the rendering range (and `getRange()`) in the next commit, whether or not the total changes — a batch of calls whose changes cancel out included, at the cost of one commit |
|
|
856
950
|
|
|
857
951
|
**Coordinate system (2.0.0)**: every position the handle and callbacks accept or return is
|
|
858
952
|
**logical** (content px, insets excluded) — `onScroll` / `onRangeChange` / `initialScroll*` /
|
|
@@ -1044,6 +1138,7 @@ removing one is a breaking change:
|
|
|
1044
1138
|
| --- | --- |
|
|
1045
1139
|
| `.aqvs-scroll-pane` | The scroll root, where `className` lands |
|
|
1046
1140
|
| `.aqvs-scroll-pane-content` | The row viewport |
|
|
1141
|
+
| `.aqvs-items-boundary` | The rows' containing block and relayout boundary inside the row viewport: absolutely positioned, height 0, `contain: size layout style` (see [What a scroll step paints](#what-a-scroll-step-paints)). It paints nothing; keep its `position`, `height` and `contain` |
|
|
1047
1142
|
| `.aqvs-item-container` | Each row's container |
|
|
1048
1143
|
| `.aqvs-no-items-container` | The empty state (`itemCount` 0): the box at the top of the viewport |
|
|
1049
1144
|
| `.aqvs-no-items-text` | The empty state's message (`labels.noItems`), centred, `color: #6b7280` |
|