@aiquants/virtualscroll 3.8.2 → 3.9.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 +82 -0
- package/README.md +112 -24
- package/dist/ScrollBar.d.cts +8 -2
- package/dist/ScrollBar.d.ts +8 -2
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +122 -19
- package/dist/VirtualScroll.d.ts +122 -19
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/devicePixelGrid.d.cts +78 -0
- package/dist/devicePixelGrid.d.ts +79 -0
- package/dist/devicePixelGrid.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2145 -2093
- package/package.json +1 -1
- package/src/ScrollBar.tsx +24 -8
- package/src/VirtualScroll.tsx +341 -200
- package/src/devicePixelGrid.ts +308 -0
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@aiquants/virtualscroll` are documented here.
|
|
4
4
|
|
|
5
|
+
## 3.9.1 (2026-10-04)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **同じ揃えへの `scrollToIndex` が確定を 1 回足していた**: 3.9.0 は行ラッパーが守る端を決める「覚えた揃え」を
|
|
10
|
+
状態に持つようになったが、揃えを覚えるたびに、辺と位置が同じでも新しいオブジェクトを書いていた。そのため
|
|
11
|
+
既に揃っている位置への `scrollToIndex` (打鍵のたびに一覧を先頭へ戻すホストなど) のたびに、`VirtualScroll` の
|
|
12
|
+
確定が 1 回増えていた。いまは辺と位置が覚えた揃えと同じなら書かない。
|
|
13
|
+
- **位置 0 から始まる一覧は先頭の行の上端に揃った状態で始まる**: 初期行も錨も持たずに位置 0 でマウントした
|
|
14
|
+
一覧は、揃えを覚えないまま始まっていた。そのため、開いた直後に先頭へ戻すだけの最初の `scrollToIndex` が
|
|
15
|
+
揃えを書き、確定を 1 回足していた。位置 0 の行ラッパーは覚えた揃えによらず上端で揃えるので、見た目は
|
|
16
|
+
変わらない。いまは初期行や錨でマウントした一覧と同じく、その位置の上端揃えとして覚えて始める。
|
|
17
|
+
|
|
18
|
+
## 3.9.0 (2026-10-04)
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **自分で動かした位置を同期で知らせる `onScrollAdjust`**: `VirtualScroll` が入力なしにスクロール位置を自分で動かすたびに、
|
|
23
|
+
間引かず同期で、動かし終えてから呼ぶ。理由 (`cause`) は 4 つ: `"item-resize"` (`updateItemSize` が先頭の可視行より上の
|
|
24
|
+
行の高さを変え、位置を同じ差だけ動かした。`updateItemSize` から戻る前)、`"reconciliation"` (描画が、描いた行のうち先頭の
|
|
25
|
+
可視行より上の行に `getItemHeight` の新しい高さを見つけ、その描画の後のマイクロタスクが位置を同じ差だけ動かした)、
|
|
26
|
+
`"drift"` (寸法の変化の後、最後の `scrollToIndex` または `initialScrollAnchor` の保留中の揃えを、確定の後の effect で
|
|
27
|
+
留め直した)、`"re-issue"` (前の中身の寸法でクランプされた補正を、新しい寸法の確定の後の effect でもう一度発行した)。
|
|
28
|
+
引数は `{ position, delta, cause }` (`VirtualScrollAdjustment`。論理 px) で、`position` は動かした後の `getScrollPosition()`
|
|
29
|
+
の値、`delta` は適用した変化 (0 にはならない)。中で読む `getScrollPosition()` と `getScrollAnchor()` は動かした後の位置と
|
|
30
|
+
行の高さを返す。利用者やホストが始めたスクロール (ホイール・ドラッグと慣性・スクロールバー・行のキー移動・`scrollTo` /
|
|
31
|
+
`scrollBy` / `scrollToIndex` / `applyWheel`) と、`getScrollPosition()` を変えない変化では呼ばない。`onScroll` /
|
|
32
|
+
`onRangeChange` は間引いたうえで 1 フレーム遅れるため、項目の同一性で自前の錨を持つホストがそこでだけ錨を記録すると、
|
|
33
|
+
次の範囲の知らせより前に確定した一覧の変化が、既に動いた位置を巻き戻していた (補正の分ずれる、または隣の項目に着地する)。
|
|
34
|
+
そうしたホストはここでも錨を記録する (README「Position changes the list makes on its own」)。型
|
|
35
|
+
`VirtualScrollAdjustment` / `VirtualScrollAdjustmentCause` をバレルから公開する。`VirtualGrid` は公開しない (位置が 2 軸で、
|
|
36
|
+
自分で動かすのは縦だけのため)。
|
|
37
|
+
- **スタイルの口の表と空状態のクラス**: README の節を「Custom styling」へ改め、スタイルの口 (描かれる部品と組の
|
|
38
|
+
`aqvs-*` クラス) を表にした。空状態の `.aqvs-no-items-container` (表示域の上端の箱) と `.aqvs-no-items-text`
|
|
39
|
+
(`labels.noItems` の文言。既定の文字色 `#6b7280` は明るい背景でしか 4.5:1 に届かない) を加えた。表のクラスの改名・削除は
|
|
40
|
+
破壊的変更として扱う。
|
|
41
|
+
- **テスト**: `VirtualScroll.spec.ts` +15 (下記 Fixed の揃える関数の 2 件と守る端の選び方の 3 件、端数のビューポートで
|
|
42
|
+
揃えた端を越えないことのコンポーネントの 4 件、`onScrollAdjust` の 6 件 — 4 つの理由をそれぞれ実物で起こし 1 回ずつ適用した
|
|
43
|
+
差で知らせ、中で読むハンドルが動かした後の位置と錨を返すこと、利用者とホストのスクロール (`scrollBy`・`scrollTo`・
|
|
44
|
+
`scrollToIndex`・ホイール・スクロールバーの矢印・行の PageDown・ポインタのドラッグとその慣性・つまみのドラッグ・トラックの
|
|
45
|
+
押下) では知らせないこと)、`renderedClassContract.spec.tsx` +3 (README の表のクラスがこの契約が部品を引く表とちょうど一致し
|
|
46
|
+
空状態の 2 つを含むこと、どのクラスも名指す部品 (ARIA のロール・`data-*`・文言で引いた要素) のすべてに付いて描かれること、
|
|
47
|
+
空の一覧の文言と箱)、`scrollStepRepaint.spec.tsx` +2 (下記 Fixed のつまみ)。
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **端に揃えた行の余白を装置の画素への揃えが削っていた**: 行ラッパーの平行移動は最も近い格子点へ揃えていた (`Math.round`。
|
|
52
|
+
ちょうど半分は +∞ 側)。そのためビューポートの高さが端数のとき、最大位置の最後の行や `align: "bottom"` で見せた行が、
|
|
53
|
+
表示域の終端を最大で半装置画素越えた (比 1 の 705.5px のビューポートで、終端の 8px の余白が 7.5px。ホストの視覚の検査では
|
|
54
|
+
比 1.5 で 0.3335px が切れた)。揃えは守る端を選ぶ。位置 0 と、最後の `scrollToIndex` の `align: "top"` (既定。offset の
|
|
55
|
+
有無を問わない) の着地位置では、格子点を正確な値以上に取り (`Math.ceil`)、中身を始端側へ動かさない。最大位置と、最後の
|
|
56
|
+
`align: "bottom"` の着地位置では、正確な値以下に取り (`Math.floor`)、中身を終端側へ動かさない。それ以外 (中央揃え・
|
|
57
|
+
利用者やホストのスクロール) は従来どおり最も近い格子点。覚えた揃えは、揃えた行をその場に留めるレイアウトシフトの補正と
|
|
58
|
+
ドリフト補正で一緒に動き、ほかのスクロールで忘れる。位置 0 は最大位置に勝ち、どちらも覚えた揃えに勝つ。端から 2^-10 px
|
|
59
|
+
以内は端とみなす。揃えた端を保つため、`scrollToIndex` は揃えた位置へ厳密に着地し (従来は 0.5px 以下の差ではペインを
|
|
60
|
+
動かさなかった)、ドリフト補正は 2^-10 px を超えるずれで留め直す (従来は 1px 超)。平行移動は 2D で装置の画素の整数の
|
|
61
|
+
ままなので、`perspective: none` との画素の同一性は変わらない。揃える関数は `snapToDevicePixelGrid(cssPx, ratio, edge)`
|
|
62
|
+
(`edge` は `"none"` / `"start"` / `"end"`) として新しいモジュール `devicePixelGrid.ts` へ移した (モジュールレベル。
|
|
63
|
+
バレル非公開)。
|
|
64
|
+
- **層の中の行も整数の画素に描かれるという説明を正した**: 行ラッパーの docstring と README は「層の中の端数のレイアウト
|
|
65
|
+
位置は描画が画素へ揃える」としていたが、Chromium は角の丸い枠線・輪・輪郭を層の中の正確な位置で滲ませて描く (比 1.25 で、
|
|
66
|
+
描画のアンカーから 7814px の行の 1px の枠線が 75% と 50% の被覆で描かれた)。揃えるのは層の平行移動だけで、行が装置の画素
|
|
67
|
+
の整数から始まるのは、その上の行の高さの和が装置 px の整数のときだけ。README に、そのためのホストの契約 (行の高さを、
|
|
68
|
+
L × 比 が整数になる格子 L の倍数にする。比が 4 分の 1 刻みなら L = 4px) を書いた。
|
|
69
|
+
- **スクロールバーのつまみを `top` で動かしていた**: つまみの器を `top` (横のバーは `left`) で動かしていたため、描画の窓を
|
|
70
|
+
変えないスクロールの 1 段ごとに、文書の根からのレイアウトが 1 回足されていた (Chromium で、描画の窓を変えない 40px の
|
|
71
|
+
60 段が、`top` で動かすと Layout 60 回・13.9ms、つまみを止めると 30 回・9.1ms)。器は `top: 0; left: 0` に置いたまま、
|
|
72
|
+
行ラッパーと同じく描くウィンドウの装置の画素の格子へ最も近い点で揃えた `translateY` (横は `translateX`) で動かし、遷移は
|
|
73
|
+
付けない。描画の窓を変えない 1 段がつまみの器へ書くのは `transform` と `aria-valuenow` だけになった。オーバーレイの props の
|
|
74
|
+
`thumbPosition` / `thumbCenter` は揃える前の厳密な値のまま。ドラッグとホバーの当たり判定は器の `top` を読まない (位置は
|
|
75
|
+
状態から、当たりは `getBoundingClientRect` から求め、平行移動を含む)。ウィンドウを持たない文書へ描くと、行ラッパーと同じく
|
|
76
|
+
つまみの取り付けで `Error` を投げる (同じ確定で両方が投げると React はまとめた `AggregateError` を投げる)。装置の画素比の
|
|
77
|
+
監視 (`(resolution: <比>dppx)` のメディアクエリ) はウィンドウごとに 1 つで、行ラッパーとつまみが共有する。
|
|
78
|
+
- **テスト**: `ScrollBar.spec.tsx` (つまみの位置を平行移動から読み、`top` / `left` が 0 であることを確かめる 2 件の改修)、
|
|
79
|
+
`externalScrollBridge.spec.tsx` (`scrollTo` が 1px 未満の差を積み上げないことの対比を整数の位置から始める 1 件の改修。
|
|
80
|
+
`scrollTo` は floor した位置へ厳密に着地するので、端数の位置 0.75 から 1.0 を求めると 1 へ 1 度だけ跳ぶ)、
|
|
81
|
+
`reducedMotion.spec.tsx` (部品をパッケージ自身のクラスで引く箇所に、規則のセレクタが名指すクラスそのものが検査の対象で
|
|
82
|
+
あるという例外の注記を付け、サークルの中かどうかの判定・グリッドのサークルとその器・セルは `data-*` で引く)、E2E
|
|
83
|
+
`scrollbar-handle-speed.spec.ts` (つまみの厳密な位置はデモのつまみのオーバーレイの印から読んで等速の CV を変えずに検査し、
|
|
84
|
+
描かれたつまみがそれから半装置画素 + レイアウトの単位 1 つ以内であることを全サンプルで確かめる。つまみの `top` が常に 0 に
|
|
85
|
+
なったため、従来の `top` の読み取りでは CV が 0 になり検査が空になるところだった)。
|
|
86
|
+
|
|
5
87
|
## 3.8.2 (2026-10-03)
|
|
6
88
|
|
|
7
89
|
### Fixed
|
package/README.md
CHANGED
|
@@ -139,29 +139,50 @@ leaves the viewport, or a list that ignores the wheel entirely).
|
|
|
139
139
|
|
|
140
140
|
The rows move inside one wrapper that is translated with `transform: translateY(…)` on its own compositor
|
|
141
141
|
layer (`will-change: transform`). That translate is snapped to the device-pixel grid of the window the list
|
|
142
|
-
is painted in
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
is painted in: whole CSS pixels at a ratio of 1, steps of 0.8 px at 1.25, 2/3 px at 1.5 and 0.5 px at 2.
|
|
143
|
+
Fractional scroll positions (trackpad and inertia deltas, centred alignment, fractional row heights or
|
|
144
|
+
insets) therefore never move the layer by a fraction of a device pixel. A layer moved by a fraction is
|
|
145
|
+
resampled as a whole: 2 px borders, outlines, focus rings and the gaps between them smear across
|
|
146
|
+
neighbouring pixel rows, and text blurs.
|
|
147
147
|
|
|
148
|
+
The snap keeps the edge the list is aligned to, so aligned content never loses part of its gutter to the
|
|
149
|
+
rounding:
|
|
150
|
+
|
|
151
|
+
| Where the list rests | Grid point | Result |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| At position 0, or where the last `scrollToIndex` with `align: "top"` (the default, with or without `offset`) landed | At or after the exact translate: `Math.ceil(offset × ratio) / ratio` | The content never moves toward the viewport start, so the first row (or the aligned row, less its `offset`) never crosses it |
|
|
154
|
+
| At the maximum position, or where the last `scrollToIndex` with `align: "bottom"` landed | At or before the exact translate: `Math.floor(offset × ratio) / ratio` | The content never moves toward the viewport end, so the last row and the bottom inset (or the aligned row) never cross it |
|
|
155
|
+
| Anywhere else (a centred row, a scroll the user or the host started) | The nearest one: `Math.round(offset × ratio) / ratio` | At most half a device pixel either way |
|
|
156
|
+
|
|
157
|
+
- `scrollToIndex` lands exactly on the aligned position (clamped to the content), and a size change that
|
|
158
|
+
moves a pending alignment by more than 2^-10 px pins it again, so an aligned edge holds through fractional
|
|
159
|
+
row heights. A remembered alignment moves with the layout-shift compensation and the drift
|
|
160
|
+
correction that keep the aligned row in place, and any other scroll forgets it. Position 0 wins over the
|
|
161
|
+
maximum position (a list that does not scroll stays top-aligned), and both win over a remembered
|
|
162
|
+
alignment. A position within 2^-10 px of one of these counts as that position.
|
|
163
|
+
- Only the wrapper translate is snapped. The rows inside it paint at their exact layout positions, where
|
|
164
|
+
rounded borders, rings and outlines are anti-aliased, so a row starts on a whole device pixel only when the
|
|
165
|
+
sum of the row heights above it is a whole number of device pixels. A host that needs crisp row edges keeps
|
|
166
|
+
every row height on a lattice L with L × ratio a whole number: L = 4 px gives whole device pixels at every
|
|
167
|
+
ratio in quarter steps (1, 1.25, 1.5, 1.75, 2, …). Other zoom levels need other lattices (10 px at 1.1);
|
|
168
|
+
at a ratio the host's lattice does not fit, the rows keep their fractional positions.
|
|
169
|
+
- Every position the component reports (`onScroll`, `onRangeChange`, `onScrollAdjust`, `getScrollPosition()`,
|
|
170
|
+
`getScrollAnchor()`) stays exact. The rows are drawn less than one device pixel away from the exact scroll
|
|
171
|
+
position (at most half a device pixel where no edge is kept). `VirtualGrid` rows are drawn by the same
|
|
172
|
+
wrapper, so they are snapped vertically as well.
|
|
148
173
|
- The ratio comes from the window of the list's own document, and it is read while the wrapper renders.
|
|
149
174
|
Before the wrapper is attached, the component expects the window of the page that runs React, and the
|
|
150
175
|
attach confirms it. A list rendered into its own window's document is therefore snapped in its first
|
|
151
176
|
commit, with no extra render. A list rendered into an iframe or a second window switches to that
|
|
152
177
|
window's ratio when the wrapper attaches, with one extra synchronous render before the browser paints.
|
|
153
178
|
The ratio is read again whenever it changes (browser zoom, or moving the window to a screen of another
|
|
154
|
-
density), through
|
|
155
|
-
|
|
156
|
-
fractional positions inside a layer on whole pixels already), and every position the component reports
|
|
157
|
-
(`onScroll`, `onRangeChange`, `getScrollPosition()`, `getScrollAnchor()`) stays exact. The rows are drawn
|
|
158
|
-
at most half a device pixel away from the exact scroll position. `VirtualGrid` rows are drawn by the same
|
|
159
|
-
wrapper, so they are snapped vertically as well.
|
|
179
|
+
density), through one `(resolution: <ratio>dppx)` media query per window, shared by the wrappers and the
|
|
180
|
+
scrollbar thumbs painted there.
|
|
160
181
|
- Server-rendered HTML carries the exact offset, since there is no screen on the server. Hydration renders
|
|
161
182
|
the same exact offset, so it reports no mismatch, and the snapped offset replaces it right after hydration.
|
|
162
183
|
- Rendering into a document without a window (for example one made with
|
|
163
|
-
`document.implementation.createHTMLDocument()`) throws when the wrapper
|
|
164
|
-
so there is no ratio to snap to.
|
|
184
|
+
`document.implementation.createHTMLDocument()`) throws when the wrapper or a scrollbar thumb attaches:
|
|
185
|
+
nothing is painted there, so there is no ratio to snap to.
|
|
165
186
|
|
|
166
187
|
## What a scroll step paints
|
|
167
188
|
|
|
@@ -191,6 +212,51 @@ Measured in Chromium 148 (1280×800, ratio 1, overscan 15, rows whose text ends
|
|
|
191
212
|
outline renders on every side, at ratios 1, 1.25, 1.5, 1.75, 2 and 3, light and dark, and at 8 offsets
|
|
192
213
|
(0 to 1,572,864.5 px and the end of a 20,000-row list), as a full border pixel, floor(2 × ratio) ring
|
|
193
214
|
pixels, at most one blended pixel, floor(2 × ratio) gap pixels and floor(2 × ratio) outline pixels.
|
|
215
|
+
- The scrollbar thumb moves the same way. Its wrapper stays at `top: 0` (`left: 0` on a horizontal bar) and is
|
|
216
|
+
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 `aria-valuenow`, and the
|
|
218
|
+
thumb adds no layout. A thumb positioned by `top` adds a layout from the document root to every step: in
|
|
219
|
+
Chromium, 60 steps of 40 px that kept the rendering window (200,000 rows of 160 px inside nested flex
|
|
220
|
+
columns beside a 600-row sidebar) ran 60 layouts in 13.9 ms with the thumb moved by `top`, and 30 layouts in
|
|
221
|
+
9.1 ms with the thumb held still. `thumbPosition` in the `renderThumbOverlay` props stays the exact,
|
|
222
|
+
unsnapped offset.
|
|
223
|
+
|
|
224
|
+
## Position changes the list makes on its own
|
|
225
|
+
|
|
226
|
+
`onScroll` and `onRangeChange` report every position, throttled (`callbackThrottleMs`) and one animation frame
|
|
227
|
+
late. Four moves are made by the list itself, without a scroll from the user or the host, and `onScrollAdjust`
|
|
228
|
+
reports each of them synchronously and unthrottled once it is complete: `getScrollPosition()` and
|
|
229
|
+
`getScrollAnchor()` read inside the callback already return the moved position and the new row heights.
|
|
230
|
+
|
|
231
|
+
| `cause` | The move | When it runs |
|
|
232
|
+
| --- | --- | --- |
|
|
233
|
+
| `"item-resize"` | `updateItemSize` changed a row above the first visible row, and the position moved by the same delta, so the visible rows stay where they were | Inside `updateItemSize`, before it returns |
|
|
234
|
+
| `"reconciliation"` | A render found a new `getItemHeight` value for a rendered row above the first visible row, and the position moved by the same delta | In the microtask after that render |
|
|
235
|
+
| `"drift"` | After a size change (content, viewport, item count or insets), the pending alignment of the last `scrollToIndex` (or of `initialScrollAnchor`) was pinned again | In an effect after the commit |
|
|
236
|
+
| `"re-issue"` | A compensation that the pane clamped against the previous content size (several compensations in one batch near the end of the list) was issued again once the new size had committed | In an effect after the commit |
|
|
237
|
+
|
|
238
|
+
The argument is `{ position, delta, cause }` (`VirtualScrollAdjustment`): `position` is the **logical**
|
|
239
|
+
position after the move, what `getScrollPosition()` returns, and `delta` is the change the move applied to it
|
|
240
|
+
(never 0). Scrolls the user or the host start (wheel, drags and their inertia, the scrollbar, keyboard row
|
|
241
|
+
navigation, `scrollTo` / `scrollBy` / `scrollToIndex` / `applyWheel`) are not reported here, and neither is a
|
|
242
|
+
change that leaves `getScrollPosition()` where it was (a top-inset change keeps the logical position).
|
|
243
|
+
|
|
244
|
+
A host that keeps its own scroll anchor by item identity, re-finding the first visible item by key after a
|
|
245
|
+
list change (rows inserted or removed above it), records that anchor from `onRangeChange` and after its own
|
|
246
|
+
scrolls. It must also re-record it from `onScrollAdjust`. Otherwise a list change committed before the next
|
|
247
|
+
range report restores a position the list has already moved, and the view shifts by the compensation or
|
|
248
|
+
lands on a neighbouring item.
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
const recordAnchor = () => {
|
|
252
|
+
const anchor = listRef.current?.getScrollAnchor()
|
|
253
|
+
if (anchor) anchorRef.current = { key: keyOf(anchor.index), offsetPx: anchor.offsetPx }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
<VirtualScroll ref={listRef} onRangeChange={recordAnchor} onScrollAdjust={recordAnchor} {...props}>
|
|
257
|
+
{renderRow}
|
|
258
|
+
</VirtualScroll>
|
|
259
|
+
```
|
|
194
260
|
|
|
195
261
|
## Reduced motion
|
|
196
262
|
|
|
@@ -718,11 +784,12 @@ exists in the DOM.
|
|
|
718
784
|
| `getItemHeight` | `(index: number) => number` | ✅ | Function to get item height |
|
|
719
785
|
| `viewportSize` | `number` | ❌ | Height of the visible band. **Omit it** — the component then measures its host with a `ResizeObserver`. Pass it only when you know the band by calculation rather than measurement (e.g. a dropdown sized from row count × row height). See [Sizing](#sizing) |
|
|
720
786
|
| `overscanCount` | `number` | ❌ | Number of items to render outside viewport (default: 15) |
|
|
721
|
-
| `className` | `string` | ❌ | CSS class name. Lands on the scroll root (`.aqvs-scroll-pane`) — see [Custom
|
|
787
|
+
| `className` | `string` | ❌ | CSS class name. Lands on the scroll root (`.aqvs-scroll-pane`) — see [Custom styling](#custom-styling) |
|
|
722
788
|
| `getItemKey` | `(index: number) => React.Key` | ❌ | Stable React key per index (defaults to the index) |
|
|
723
789
|
| `testId` | `string` | ❌ | Emitted as `data-testid` on the scroll root. DOM hooks should use `data-*`, never class selectors |
|
|
724
790
|
| `onScroll` | `(position: number, totalHeight: number) => void` | ❌ | Scroll event handler |
|
|
725
791
|
| `onRangeChange` | `(range: VirtualScrollRange) => void` | ❌ | Range change handler |
|
|
792
|
+
| `onScrollAdjust` | `(adjustment: VirtualScrollAdjustment) => void` | ❌ | Called synchronously, without throttling, each time the list moves its own scroll position (layout-shift compensation, the re-pinning of a pending alignment, the second stage of a clamped compensation). `adjustment` is `{ position, delta, cause }` in **logical** px, and handle reads inside the callback already see the adjusted state. Never called for scrolls the user or the host start. See [Position changes the list makes on its own](#position-changes-the-list-makes-on-its-own) |
|
|
726
793
|
| `onWheelHorizontal` | `(deltaX: number) => void` | ❌ | Opt-in horizontal delegation. Horizontal-dominant wheel / trackpad gestures (and shift+wheel) are delegated to this handler so the parent can implement horizontal scrolling. ⚠️ **Keyboard deltas arrive here too** when `horizontalKeyInputs` is set. When omitted, horizontal gestures bypass vertical scrolling so parent native scrolling works. See [Horizontal Scrolling](#horizontal-scrolling). |
|
|
727
794
|
| `onPanHorizontal` | `(deltaX: number) => void` | ❌ | Opt-in delegation of the horizontal component of pointer pans (touch / pen drags) — the pan twin of `onWheelHorizontal` with the same sign convention, per-move increments divided by the press-time x-axis ancestor scale (3.2.0). When set, purely horizontal pans also activate the drag. Same seal family as `onWheelHorizontal` — wrappers sealing the horizontal axis must `Omit` this name too. |
|
|
728
795
|
| `horizontalKeyInputs` | `readonly "arrow"[]` | ❌ | Keyboard gestures that emit a horizontal delta through `onWheelHorizontal` (default: `[]`, so row-level arrow handling is never stolen). `Shift + ←/→` is never consumed. Fires only when the row itself is the event target. ⚠️ Requires `behaviorOptions.enableKeyboardNavigation` (default `true`) — with it off the rows have no key handler at all and this prop does nothing (the package warns) |
|
|
@@ -776,7 +843,7 @@ exists in the DOM.
|
|
|
776
843
|
| `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 |
|
|
777
844
|
| `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` |
|
|
778
845
|
| *(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 |
|
|
779
|
-
| `scrollToIndex` | `(index: number, options?: { align?: "top" \| "bottom" \| "center"; offset?: number }) => void` | Scroll to specific item index with optional alignment and offset |
|
|
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) |
|
|
780
847
|
| `getScrollPosition` | `() => number` | Get current **logical** scroll position (2.0.0; `-1` when the pane is not connected) |
|
|
781
848
|
| `getContentSize` | `() => number` | Get total content size (insets included). Returns the `-1` sentinel while the pane is unconnected |
|
|
782
849
|
| `getViewportSize` | `() => number` | Get viewport size. Returns the `-1` sentinel while the pane is unconnected |
|
|
@@ -785,7 +852,7 @@ exists in the DOM.
|
|
|
785
852
|
| `getScrollAnchor` | `() => { index: number; offsetPx: number } \| null` | Capture the current top visible row anchor for exact restore via `initialScrollAnchor` (`null` when `itemCount` is 0) |
|
|
786
853
|
| `getFenwickTreeTotalHeight` | `() => number` | Total content height managed by the Fenwick tree (insets excluded) |
|
|
787
854
|
| `getFenwickSize` | `() => number` | Item count managed by the Fenwick tree |
|
|
788
|
-
| `updateItemSize` | `(index: number, size: number) => void` | Manually update one item's size (with layout-shift compensation). `getItemHeight(index)` must return the same value afterwards |
|
|
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 |
|
|
789
856
|
|
|
790
857
|
**Coordinate system (2.0.0)**: every position the handle and callbacks accept or return is
|
|
791
858
|
**logical** (content px, insets excluded) — `onScroll` / `onRangeChange` / `initialScroll*` /
|
|
@@ -902,10 +969,11 @@ function AdvancedExample() {
|
|
|
902
969
|
}
|
|
903
970
|
```
|
|
904
971
|
|
|
905
|
-
### Custom
|
|
972
|
+
### Custom styling
|
|
906
973
|
|
|
907
|
-
`className` lands on the **scroll root** (`.aqvs-scroll-pane`). The
|
|
908
|
-
|
|
974
|
+
`className` lands on the **scroll root** (`.aqvs-scroll-pane`). The rows, the bar with its track, thumb and
|
|
975
|
+
arrow buttons, the scroll-to-edge pills and the empty-state message are all descendants of that element, so
|
|
976
|
+
one wrapper class reaches every part:
|
|
909
977
|
|
|
910
978
|
```tsx
|
|
911
979
|
<VirtualScroll
|
|
@@ -935,6 +1003,9 @@ and the scroll-to-edge pills are all descendants of that element, so one wrapper
|
|
|
935
1003
|
.custom-virtual-scroll .aqvs-scrollbar-arrow-button:enabled:hover { background-color: #cfe8f7; }
|
|
936
1004
|
.custom-virtual-scroll .aqvs-scroll-to-edge-button { background-color: rgba(0, 122, 204, 0.85); }
|
|
937
1005
|
.custom-virtual-scroll .aqvs-scroll-to-edge-button:hover { background-color: rgba(0, 92, 153, 0.9); }
|
|
1006
|
+
|
|
1007
|
+
/* The empty state (itemCount 0). Restate the colour on a dark background: the default #6b7280 reaches 4.5:1 only on a light one. */
|
|
1008
|
+
.custom-virtual-scroll .aqvs-no-items-text { color: #94a3b8; padding-block: 1rem; }
|
|
938
1009
|
```
|
|
939
1010
|
|
|
940
1011
|
The packaged rules live in `@layer components` in **both** artifacts, so an ordinary (unlayered) rule in
|
|
@@ -966,11 +1037,28 @@ Three consequences are worth knowing:
|
|
|
966
1037
|
users. Forcing `opacity: 1` on the hidden overlay therefore paints a pill that cannot be clicked or focused.
|
|
967
1038
|
Scope such rules to `[data-visible="true"]` as well.
|
|
968
1039
|
|
|
969
|
-
The
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1040
|
+
The styling hooks are the classes below. Each one is rendered on the part it names, and renaming or
|
|
1041
|
+
removing one is a breaking change:
|
|
1042
|
+
|
|
1043
|
+
| Class | Part |
|
|
1044
|
+
| --- | --- |
|
|
1045
|
+
| `.aqvs-scroll-pane` | The scroll root, where `className` lands |
|
|
1046
|
+
| `.aqvs-scroll-pane-content` | The row viewport |
|
|
1047
|
+
| `.aqvs-item-container` | Each row's container |
|
|
1048
|
+
| `.aqvs-no-items-container` | The empty state (`itemCount` 0): the box at the top of the viewport |
|
|
1049
|
+
| `.aqvs-no-items-text` | The empty state's message (`labels.noItems`), centred, `color: #6b7280` |
|
|
1050
|
+
| `.aqvs-scrollbar` | The bar (`role="scrollbar"`) |
|
|
1051
|
+
| `.aqvs-scrollbar-vertical` | A vertical bar |
|
|
1052
|
+
| `.aqvs-scrollbar-horizontal` | A horizontal bar |
|
|
1053
|
+
| `.aqvs-scrollbar-track` | The track behind the thumb |
|
|
1054
|
+
| `.aqvs-scrollbar-thumb-wrapper` | The thumb's wrapper (`role="slider"`); the package positions it with an inline translate |
|
|
1055
|
+
| `.aqvs-scrollbar-thumb` | The thumb, with `data-thumb-state` |
|
|
1056
|
+
| `.aqvs-scrollbar-thumb-vertical` | The thumb of a vertical bar |
|
|
1057
|
+
| `.aqvs-scrollbar-thumb-horizontal` | The thumb of a horizontal bar |
|
|
1058
|
+
| `.aqvs-scrollbar-arrow-button` | The arrow buttons |
|
|
1059
|
+
| `.aqvs-scroll-to-edge-overlay` | The scroll-to-edge pills' overlay (`enableScrollToTopBottomButtons`) |
|
|
1060
|
+
| `.aqvs-scroll-to-edge-button` | A scroll-to-edge pill |
|
|
1061
|
+
| `.aqvs-tap-scroll-circle` | The tap scroll circle |
|
|
974
1062
|
|
|
975
1063
|
### Tap Scroll Circle Configuration
|
|
976
1064
|
|
package/dist/ScrollBar.d.cts
CHANGED
|
@@ -296,9 +296,15 @@ export declare const computeTapScrollSpeed: ({ distance, maxDistance, viewportSi
|
|
|
296
296
|
*/
|
|
297
297
|
export declare const computeAutoTapScrollMaxSpeedMultiplier: (itemCount?: number) => number;
|
|
298
298
|
/**
|
|
299
|
-
* A custom scrollbar component.
|
|
299
|
+
* A custom scrollbar component. The thumb wrapper stays at the start of the track (`top: 0; left: 0`) and moves along it
|
|
300
|
+
* by a 2D translate snapped to the device-pixel grid of the window that paints it, so a scroll step rewrites only that
|
|
301
|
+
* transform and runs no layout. `thumbPosition` in the overlay props stays the exact, unsnapped offset. Rendering into a
|
|
302
|
+
* document without a window throws when the thumb attaches (there is no device-pixel ratio to snap to).
|
|
300
303
|
*
|
|
301
|
-
*
|
|
304
|
+
* カスタムスクロールバーコンポーネント。つまみの器はトラックの始端 (`top: 0; left: 0`) に置いたまま、描くウィンドウの装置の
|
|
305
|
+
* 画素の格子へ揃えた 2D の平行移動でトラックに沿って動くので、スクロールの 1 段が書き換えるのはその transform だけで、
|
|
306
|
+
* レイアウトは走らない。オーバーレイの props の `thumbPosition` は揃える前の厳密なオフセットのまま。ウィンドウを持たない
|
|
307
|
+
* 文書へ描くと、つまみの取り付けで例外を投げる (揃える装置の画素比が無い)。
|
|
302
308
|
*/
|
|
303
309
|
export declare const ScrollBar: ({ contentSize, viewportSize, scrollPosition, onScroll, enableThumbDrag, enableTrackClick, enableArrowButtons, enableArrowButtonTabStops, horizontal, enableHorizontalTapCircle, stretchMainSize, scrollBarWidth, className, ariaControls, tapScrollCircleOptions, itemCount, renderThumbOverlay, visibleStartIndex, visibleEndIndex, locale, labels, }: ScrollBarProps) => import("react").JSX.Element;
|
|
304
310
|
export {};
|
package/dist/ScrollBar.d.ts
CHANGED
|
@@ -296,9 +296,15 @@ export declare const computeTapScrollSpeed: ({ distance, maxDistance, viewportSi
|
|
|
296
296
|
*/
|
|
297
297
|
export declare const computeAutoTapScrollMaxSpeedMultiplier: (itemCount?: number) => number;
|
|
298
298
|
/**
|
|
299
|
-
* A custom scrollbar component.
|
|
299
|
+
* A custom scrollbar component. The thumb wrapper stays at the start of the track (`top: 0; left: 0`) and moves along it
|
|
300
|
+
* by a 2D translate snapped to the device-pixel grid of the window that paints it, so a scroll step rewrites only that
|
|
301
|
+
* transform and runs no layout. `thumbPosition` in the overlay props stays the exact, unsnapped offset. Rendering into a
|
|
302
|
+
* document without a window throws when the thumb attaches (there is no device-pixel ratio to snap to).
|
|
300
303
|
*
|
|
301
|
-
*
|
|
304
|
+
* カスタムスクロールバーコンポーネント。つまみの器はトラックの始端 (`top: 0; left: 0`) に置いたまま、描くウィンドウの装置の
|
|
305
|
+
* 画素の格子へ揃えた 2D の平行移動でトラックに沿って動くので、スクロールの 1 段が書き換えるのはその transform だけで、
|
|
306
|
+
* レイアウトは走らない。オーバーレイの props の `thumbPosition` は揃える前の厳密なオフセットのまま。ウィンドウを持たない
|
|
307
|
+
* 文書へ描くと、つまみの取り付けで例外を投げる (揃える装置の画素比が無い)。
|
|
302
308
|
*/
|
|
303
309
|
export declare const ScrollBar: ({ contentSize, viewportSize, scrollPosition, onScroll, enableThumbDrag, enableTrackClick, enableArrowButtons, enableArrowButtonTabStops, horizontal, enableHorizontalTapCircle, stretchMainSize, scrollBarWidth, className, ariaControls, tapScrollCircleOptions, itemCount, renderThumbOverlay, visibleStartIndex, visibleEndIndex, locale, labels, }: ScrollBarProps) => import("react").JSX.Element;
|
|
304
310
|
export {};
|
package/dist/ScrollBar.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScrollBar.d.ts","sourceRoot":"","sources":["../src/ScrollBar.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"ScrollBar.d.ts","sourceRoot":"","sources":["../src/ScrollBar.tsx"],"names":[],"mappings":"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"}
|
package/dist/VirtualScroll.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { default as React, ReactNode } from 'react';
|
|
2
|
+
import { DevicePixelSnapEdge } from './devicePixelGrid.cjs';
|
|
2
3
|
import { VirtualScrollLabelOverrides, VirtualScrollLocale } from './labels.cjs';
|
|
3
4
|
import { ScrollPaneProps } from './ScrollPane.cjs';
|
|
4
5
|
import { useFenwickMapTree } from './useFenwickMapTree.cjs';
|
|
@@ -21,6 +22,42 @@ export type VirtualScrollRange = {
|
|
|
21
22
|
/** Total height of the scroll content / スクロールコンテンツの総高さ */
|
|
22
23
|
totalHeight: number;
|
|
23
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Why VirtualScroll moved its scroll position on its own (see `VirtualScrollProps["onScrollAdjust"]`).
|
|
27
|
+
*
|
|
28
|
+
* - `"item-resize"`: `updateItemSize` changed the height of a row above the first visible row, and the position moved by
|
|
29
|
+
* the same delta, so the visible rows stay where they were.
|
|
30
|
+
* - `"reconciliation"`: a render found that `getItemHeight` returns a new height for a rendered row above the first
|
|
31
|
+
* visible row; the height reconciliation that follows the render (a microtask) moved the position by the same delta.
|
|
32
|
+
* - `"drift"`: after a size change (content, viewport, item count or insets) the pending alignment of the last
|
|
33
|
+
* `scrollToIndex` (or of `initialScrollAnchor`) was pinned again, in an effect after the commit.
|
|
34
|
+
* - `"re-issue"`: a compensation that the pane clamped against the previous content size was issued again once the new
|
|
35
|
+
* content size had committed, in an effect after the commit.
|
|
36
|
+
*
|
|
37
|
+
* VirtualScroll が自分でスクロール位置を動かした理由 (`VirtualScrollProps["onScrollAdjust"]` を参照)。
|
|
38
|
+
*
|
|
39
|
+
* - `"item-resize"`: `updateItemSize` が先頭の可視行より上の行の高さを変え、位置を同じ差だけ動かした (見えている行はその場に
|
|
40
|
+
* 留まる)。
|
|
41
|
+
* - `"reconciliation"`: 描画が、描いた行のうち先頭の可視行より上の行について `getItemHeight` の新しい高さを見つけ、描画の後の
|
|
42
|
+
* 高さの照合 (マイクロタスク) が位置を同じ差だけ動かした。
|
|
43
|
+
* - `"drift"`: 寸法の変化 (中身・ビューポート・件数・インセット) の後、最後の `scrollToIndex` (または `initialScrollAnchor`)
|
|
44
|
+
* の保留中の揃えを、確定の後の effect で留め直した。
|
|
45
|
+
* - `"re-issue"`: ペインが前の中身の寸法でクランプした補正を、新しい中身の寸法が確定した後の effect でもう一度発行した。
|
|
46
|
+
*/
|
|
47
|
+
export type VirtualScrollAdjustmentCause = "item-resize" | "reconciliation" | "drift" | "re-issue";
|
|
48
|
+
/**
|
|
49
|
+
* One position change VirtualScroll made on its own (the argument of `VirtualScrollProps["onScrollAdjust"]`).
|
|
50
|
+
*
|
|
51
|
+
* VirtualScroll が自分で行った 1 回の位置の変化 (`VirtualScrollProps["onScrollAdjust"]` の引数)。
|
|
52
|
+
*/
|
|
53
|
+
export type VirtualScrollAdjustment = {
|
|
54
|
+
/** The LOGICAL scroll position after the change — what `getScrollPosition()` returns at that moment / 変化の後の論理スクロール位置 (その時点の `getScrollPosition()` の値) */
|
|
55
|
+
readonly position: number;
|
|
56
|
+
/** The applied change in LOGICAL px: `position` minus the position before the change; never 0 / 適用した変化 (論理 px)。`position` から変化の前の位置を引いた値で、0 にはならない */
|
|
57
|
+
readonly delta: number;
|
|
58
|
+
/** Why the position moved / 位置が動いた理由 */
|
|
59
|
+
readonly cause: VirtualScrollAdjustmentCause;
|
|
60
|
+
};
|
|
24
61
|
/**
|
|
25
62
|
* Imperative handle of VirtualScroll. Every position it accepts or returns is in the
|
|
26
63
|
* LOGICAL coordinate space (content px, insets excluded) — the same space as onScroll /
|
|
@@ -94,7 +131,15 @@ export type VirtualScrollHandle = {
|
|
|
94
131
|
getContentSize: () => number;
|
|
95
132
|
/** Viewport size / ビューポートの高さ。未接続時は -1 */
|
|
96
133
|
getViewportSize: () => number;
|
|
97
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Scrolls to a specific item index, landing exactly on the aligned position (clamped to the content). A `"top"`
|
|
136
|
+
* (default) or `"bottom"` alignment is remembered at that position, so the device-pixel snap of the items wrapper
|
|
137
|
+
* keeps the aligned edge there (see the README's device-pixel snapping section).
|
|
138
|
+
*
|
|
139
|
+
* 指定したアイテムインデックスへスクロールする処理。揃えた位置 (中身の範囲へクランプ) へ厳密に着地する。`"top"` (既定) と
|
|
140
|
+
* `"bottom"` の揃えはその位置で覚えるので、そこでは行ラッパーの装置の画素への揃えが揃えた端を守る (README の装置の画素への
|
|
141
|
+
* 揃えの節を参照)。
|
|
142
|
+
*/
|
|
98
143
|
scrollToIndex: (index: number, options?: {
|
|
99
144
|
align?: "top" | "bottom" | "center";
|
|
100
145
|
offset?: number;
|
|
@@ -123,10 +168,15 @@ export type VirtualScrollHandle = {
|
|
|
123
168
|
* `getItemHeight(index)` must return the same `size`; `getItemHeight` is the source of truth,
|
|
124
169
|
* so if it keeps returning the old value, rows inside the current rendering window (including
|
|
125
170
|
* overscan) are reverted to the `getItemHeight` value by height reconciliation on the next render.
|
|
171
|
+
* When the item lies above the first visible row, the scroll position moves by the size change
|
|
172
|
+
* before this returns (layout-shift compensation), and `onScrollAdjust` reports it with the cause
|
|
173
|
+
* `"item-resize"`.
|
|
126
174
|
*
|
|
127
175
|
* 特定のアイテムのサイズを手動で更新。契約: 呼び出し後は `getItemHeight(index)` も同じ値を
|
|
128
176
|
* 返すこと。`getItemHeight` が正であるため、旧値を返し続けると描画ウィンドウ (オーバースキャン
|
|
129
|
-
* 含む) 内の行は次レンダーの高さ照合で `getItemHeight`
|
|
177
|
+
* 含む) 内の行は次レンダーの高さ照合で `getItemHeight` の値へ巻き戻る。アイテムが先頭の可視行より
|
|
178
|
+
* 上にあるときは、戻る前にスクロール位置をサイズの変化だけ動かし (レイアウトシフトの補正)、
|
|
179
|
+
* `onScrollAdjust` が理由 `"item-resize"` で知らせる。
|
|
130
180
|
*/
|
|
131
181
|
updateItemSize: (index: number, size: number) => void;
|
|
132
182
|
};
|
|
@@ -329,6 +379,33 @@ export type VirtualScrollProps<T> = {
|
|
|
329
379
|
testId?: string;
|
|
330
380
|
onScroll?: (scrollPosition: number, totalHeight: number) => void;
|
|
331
381
|
onRangeChange?: (range: VirtualScrollRange) => void;
|
|
382
|
+
/**
|
|
383
|
+
* Called synchronously, without throttling, each time VirtualScroll moves its scroll position on its own: a
|
|
384
|
+
* layout-shift compensation (`"item-resize"`, `"reconciliation"`), the re-pinning of a pending alignment
|
|
385
|
+
* (`"drift"`) or the second stage of a clamped compensation (`"re-issue"`) — see `VirtualScrollAdjustmentCause`.
|
|
386
|
+
* It runs after the change is complete, so `getScrollPosition()` and `getScrollAnchor()` read inside it already
|
|
387
|
+
* see the adjusted position and row heights. It never runs for scrolls that the user or the host start (wheel,
|
|
388
|
+
* drag, scrollbar, inertia, keyboard row navigation, `scrollTo` / `scrollBy` / `scrollToIndex` / `applyWheel`),
|
|
389
|
+
* nor for a change that leaves `getScrollPosition()` where it was.
|
|
390
|
+
*
|
|
391
|
+
* `onScroll` and `onRangeChange` report every position, but throttled and one frame later. A host that keeps its
|
|
392
|
+
* own scroll anchor by item identity (re-finding the first visible item by key after a list change) records that
|
|
393
|
+
* anchor from the range report and from its own scrolls; it must also record it from here, or a list change
|
|
394
|
+
* committed before the next range report restores a position VirtualScroll has already moved.
|
|
395
|
+
*
|
|
396
|
+
* VirtualScroll が自分でスクロール位置を動かすたびに、間引かず同期で呼ぶ関数。レイアウトシフトの補正
|
|
397
|
+
* (`"item-resize"`・`"reconciliation"`)、保留中の揃えの留め直し (`"drift"`)、クランプされた補正の二段目
|
|
398
|
+
* (`"re-issue"`) が対象 (`VirtualScrollAdjustmentCause` を参照)。変化を終えてから呼ぶので、中で読む
|
|
399
|
+
* `getScrollPosition()` と `getScrollAnchor()` は動かした後の位置と行の高さを返す。利用者やホストが始めた
|
|
400
|
+
* スクロール (ホイール・ドラッグ・スクロールバー・慣性・行のキーボード移動・`scrollTo` / `scrollBy` /
|
|
401
|
+
* `scrollToIndex` / `applyWheel`) と、`getScrollPosition()` を変えない変化では呼ばない。
|
|
402
|
+
*
|
|
403
|
+
* `onScroll` と `onRangeChange` はどの位置も知らせるが、間引いたうえで 1 フレーム遅れる。項目の同一性で自前の
|
|
404
|
+
* スクロールの錨を持つホスト (一覧の変化の後に先頭の可視項目をキーで探し直す) は、範囲の知らせと自分のスクロールで
|
|
405
|
+
* 錨を記録するが、ここでも記録すること。さもないと、次の範囲の知らせより前に確定した一覧の変化が、VirtualScroll が
|
|
406
|
+
* 既に動かした位置を巻き戻す。
|
|
407
|
+
*/
|
|
408
|
+
onScrollAdjust?: (adjustment: VirtualScrollAdjustment) => void;
|
|
332
409
|
/**
|
|
333
410
|
* Opt-in `aria-live` region announcing the visible range to assistive technology (default:
|
|
334
411
|
* none rendered). Virtualization removes off-screen rows from the DOM, so a screen-reader
|
|
@@ -534,26 +611,52 @@ export declare const MAX_RENDERED_ITEMS = 2000;
|
|
|
534
611
|
*/
|
|
535
612
|
export declare const ANCHOR_REBASE_DISTANCE = 1048576;
|
|
536
613
|
/**
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
|
|
614
|
+
* The viewport edge VirtualScroll aligned a row to, and the pane position at which that alignment holds. Module-level
|
|
615
|
+
* export (NOT in the package barrel), the parameter type of `resolveItemsWrapperSnapEdge`.
|
|
616
|
+
*
|
|
617
|
+
* VirtualScroll が行を揃えた表示域の端と、その揃えが成り立つペイン位置。モジュールレベル export (バレル非公開)。
|
|
618
|
+
* `resolveItemsWrapperSnapEdge` の引数の型。
|
|
619
|
+
*/
|
|
620
|
+
export type AlignedEdge = {
|
|
621
|
+
/** `"start"` for a top alignment, `"end"` for a bottom alignment / 上端揃えは `"start"`、下端揃えは `"end"` */
|
|
622
|
+
readonly edge: "start" | "end";
|
|
623
|
+
/** Pane position (PANE coordinates) where the aligned row sits at that edge / 揃えた行がその端にあるペイン位置 (ペイン座標) */
|
|
624
|
+
readonly panePosition: number;
|
|
625
|
+
};
|
|
626
|
+
/**
|
|
627
|
+
* Chooses the edge the items-wrapper translate keeps when it is snapped to the device-pixel grid
|
|
628
|
+
* (`snapToDevicePixelGrid`), so that aligned content never loses part of its edge gutter to the snap:
|
|
629
|
+
*
|
|
630
|
+
* - `"start"` while the pane rests at position 0: the first row and the top inset keep their place.
|
|
631
|
+
* - `"end"` while the pane rests at its maximum position: the last row and the bottom inset keep theirs.
|
|
632
|
+
* - The edge of the remembered alignment while the pane is at the position where it holds: a row revealed by
|
|
633
|
+
* `scrollToIndex` with `align: "top"` (or the default) or `align: "bottom"`, kept through layout-shift compensation and
|
|
634
|
+
* drift correction.
|
|
635
|
+
* - `"none"` (nearest) everywhere else.
|
|
636
|
+
*
|
|
637
|
+
* Position 0 wins over the maximum position (a list that does not scroll stays top-aligned), and both win over a
|
|
638
|
+
* remembered alignment, since a clamp means that alignment was not reached. A position within `EDGE_POSITION_TOLERANCE`
|
|
639
|
+
* of one of these counts as that position. Module-level export (NOT in the package barrel).
|
|
640
|
+
*
|
|
641
|
+
* 行ラッパーの平行移動を装置の画素の格子へ揃えるとき (`snapToDevicePixelGrid`) に守る端を選ぶ処理。揃えた中身の端の余白を
|
|
642
|
+
* 丸めが削らないようにする。
|
|
643
|
+
*
|
|
644
|
+
* - ペインが位置 0 に止まっている間は `"start"`。最初の行と上のインセットがその場に留まる。
|
|
645
|
+
* - ペインが最大位置に止まっている間は `"end"`。最後の行と下のインセットがその場に留まる。
|
|
646
|
+
* - 覚えた揃えが成り立つ位置にペインがある間は、その揃えの端。`scrollToIndex` が `align: "top"` (既定を含む) か
|
|
647
|
+
* `align: "bottom"` で見せた行で、レイアウトシフトの補正とドリフト補正を通して保つ。
|
|
648
|
+
* - それ以外は `"none"` (最も近い格子点)。
|
|
543
649
|
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
* (2px の輪郭・隙間・輪が隣の装置の行へ滲み、文字もぼける)、層の中の端数のレイアウト位置は描画が既に画素へ
|
|
548
|
-
* 揃えるため。冪等 (揃えた値はそのまま)。有限でない `cssPx` はそのまま伝わる (`NaN` は `NaN`)。
|
|
549
|
-
* モジュールレベル export (バレル非公開)。
|
|
650
|
+
* 位置 0 は最大位置に勝ち (スクロールしない一覧は上端揃えのまま)、どちらも覚えた揃えに勝つ (クランプは揃えが届かなかった
|
|
651
|
+
* ことを意味する)。これらの位置から `EDGE_POSITION_TOLERANCE` 以内の位置はその位置とみなす。モジュールレベル export
|
|
652
|
+
* (バレル非公開)。
|
|
550
653
|
*
|
|
551
|
-
* @param
|
|
552
|
-
* @param
|
|
553
|
-
* @
|
|
554
|
-
* @
|
|
654
|
+
* @param panePosition - The pane position the wrapper is rendered at (PANE coordinates) / ラッパーを描くペイン位置 (ペイン座標)
|
|
655
|
+
* @param maxPanePosition - The pane's maximum position: content plus insets minus the viewport / ペインの最大位置 (中身とインセットの和からビューポートを引いた値)
|
|
656
|
+
* @param alignedEdge - The remembered alignment, or `null` / 覚えた揃え (無ければ `null`)
|
|
657
|
+
* @returns The edge the snapped translate keeps / 揃えた平行移動が守る端
|
|
555
658
|
*/
|
|
556
|
-
export declare const
|
|
659
|
+
export declare const resolveItemsWrapperSnapEdge: (panePosition: number, maxPanePosition: number, alignedEdge: AlignedEdge | null) => DevicePixelSnapEdge;
|
|
557
660
|
/**
|
|
558
661
|
* Retrieves a high-resolution timestamp when available.
|
|
559
662
|
*
|