@aiquants/virtualscroll 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,116 @@
2
2
 
3
3
  All notable changes to `@aiquants/virtualscroll` are documented here.
4
4
 
5
+ ## 3.8.0 (2026-10-03)
6
+
7
+ ### Added
8
+
9
+ - **スクロールバー矢印を Tab 順から外す `enableArrowButtonTabStops`**: 矢印ボタン 2 個を Tab の止まり先に
10
+ するかどうか (既定 `true` = 従来どおり)。行のロービングフォーカス (矢印 / Page / Home / End) やグリッドの
11
+ キーボードモデルのように、ホストがキーボードスクロールを自前で持つ場合に `false` を指定すると両矢印が
12
+ `tabIndex={-1}` になり、バー 1 本あたり 2 個の冗長な Tab の止まり先が消える (ネイティブのスクロールバーも
13
+ Tab の止まり先にならない)。変わるのは `tabIndex` だけで、ポインタ押下と長押しリピート・アクセシブルネーム・
14
+ スクリプトからフォーカスした矢印の Enter / Space は維持する。矢印は `role="scrollbar"` の要素の子孫で、
15
+ ARIA 1.2 はその子を presentational とするため、支援技術が矢印を独立したボタンとして示すかどうかは
16
+ ブラウザ次第 (Chromium は示し、バー全体を 1 つの操作部品として示すブラウザもある) — パッケージはそこを
17
+ 約束しない。既定を `true` に据え置くのは、ホスト側のモデルが無ければ矢印が Tab で届く唯一のスクロール
18
+ 操作部品だから (ペインは transform で動きネイティブのスクロール領域を持たず、行も Tab の止まり先ではない)。
19
+ 矢印が無効な間 (`enableArrowButtons: false` またはスクロール不要) は効果なし。同じ名前と意味で全入口に
20
+ 置いた: `ScrollBar` / `ScrollPane` の props と、`VirtualScroll` / `VirtualGrid` の `scrollBarOptions`
21
+ (グリッドは縦・横の両バーへ適用)。
22
+ - **テスト**: 単体 `arrowButtonTabStops.spec.tsx` 24 件 (新規。4 入口それぞれで、未指定と `true` 明示の
23
+ `tabIndex=0` と Tab 経路、`false` の `tabIndex=-1` と Tab / Shift+Tab が止まらずに横切ること (user-event)、
24
+ 名前付きのボタンのままで操作可能なこと、長押しリピート、スクリプトからフォーカスした矢印の
25
+ Enter / Space)、`VirtualGrid.spec.tsx` +7 (下記 Changed の両バー適用 5 件と `RangeError` の文面 2 件)、
26
+ `labels.spec.ts` +1 (`VIRTUAL_SCROLL_LABEL_KEYS` の列そのもの — 7 キーの件数と順序 — の固定。合成する側が
27
+ 自分のキー表の先頭に連結して固定するため、キーの追加・並べ替えは破壊的変更になる)、
28
+ `shippedTextSelfContained.spec.ts` 51 件 (新規 — 下記 Changed)、`VirtualScroll.spec.ts` +12 (下記 Fixed の
29
+ 装置の画素への揃え: 揃える関数の 4 件 — 比 1・1.25・1.5・2 で負・小数の入力が最も近い格子点へ揃うこと、代表値、
30
+ 冪等、0 より大きい有限数でない比の `RangeError` — と、描画の 8 件 — 比 1 の 2440.5px が整数 px になること、
31
+ 行の上端は端数のまま残ること、比 1.25・1.5・2 の格子、比の変化で揃え直して監視を新しい比で張り直すこと、
32
+ ラッパーの取り外しで監視を外し再取り付けで比を読み直すこと、iframe に描いた一覧はそのウィンドウの比を読むこと、
33
+ ウィンドウの無い文書で `Error` を投げること、act を使わない `flushSync` の描画でも戻った時点で揃っていること)。
34
+ vitest 1,026 件 / 53 ファイル (単体 995 + 統合 31。3.7.1 の取り込み後、2026-10-03 の `vitest run --maxWorkers=1` 1 回の全数実測で、
35
+ すべて通りスキップ 0)。
36
+
37
+ ### Changed
38
+
39
+ - **`VirtualGrid` の横バーも `scrollBarOptions` のバー固有メンバーに従う**: `enableThumbDrag` /
40
+ `enableTrackClick` / `enableArrowButtons` (と新設の `enableArrowButtonTabStops`) を縦バーと同じく横バーへも
41
+ 適用する。従来は横バーへ効くのが `width` / `tapScrollCircleOptions` だけで、これらを指定しても縦バーしか
42
+ 変わらなかった (仕様書に既知の制約として記録していたもの)。1 つの設定がグリッドの片側だけに効く状態を
43
+ 無くすための変更で、`tapScrollCircleOptions` は統合 2 軸サークル、`renderThumbOverlay` (可視行
44
+ インデックスを受け取る) と `enableScrollToTopBottomButtons` は行軸の機能として内包 `VirtualScroll` 専用のまま。
45
+ - **公開の流れで tarball の漏洩検査を必ず通す**: `publish:patch` / `publish:minor` / `publish:major` は
46
+ typecheck → test → 漏洩検査 (`check-publish-leaks.mjs` — 自分で `pnpm pack` を行い、その tarball そのものを検査する)
47
+ → 版上げ → 公開 の順に、素の `pnpm publish` が起動する `prepublishOnly` は漏洩検査だけを走らせる。`pnpm pack` は
48
+ `prepare` (= ビルド) を走らせるため、検査する tarball は常にビルドしたての成果物で、公開の入口が二度ビルドすることはない。
49
+ 検出が 1 件でもあれば版上げの前に止まる。配線は単体 `publishLeakGuard.spec.ts` 7 件 (新規) が固定する。
50
+ あわせて、同梱の README・CHANGELOG・`src/VirtualGrid.tsx` の注釈が参照していた、tarball に含まれない
51
+ 設計文書への参照 (3.7.0 の tarball で検査が検出した 4 件) を、参照先が決めていた内容 (`VirtualGrid` の
52
+ 有界化契約と、バーごとのタップサークルを抑止する理由) の記述へ置き換えた。
53
+ - **同梱の文章を単独で読めるようにする**: `VirtualGrid` の `getRowHeight` / `getColWidth` が契約外の値を
54
+ 返したときの `RangeError` は、読めない設計文書の節を示す代わりに、許容範囲と従い方と上限の理由を
55
+ 自分で述べる: `[VirtualGrid] getColWidth(3) returned 262145 — must be an integer in [0, 262144] (0 = hidden).
56
+ Round fractional sizes to whole pixels and split content larger than 262144 px across several columns: a larger
57
+ track could push cell positions past the 2^25 px layout-coordinate limit of browsers.` (行は `rows`)。
58
+ あわせて README・CHANGELOG・型宣言 (`.d.ts` に載る docstring) から、tarball に含まれない文書への参照
59
+ (`docs/specs` などのパス、ADR 番号、設計文書の節番号 (§)、変異台帳の ID) を除き、参照先が決めて
60
+ いた内容の記述へ置き換えた (例: `VirtualGrid` が意図的に持たない行側メンバーの一覧、`MAX_RENDERED_CELLS`
61
+ が賄う画面の大きさ)。実装のコメントには設計文書の節への参照が素の節記号と題名の形で残る (ADR 番号、
62
+ 設計・実装プランに続く節記号、リポジトリの `docs` の下の階層と `.agents`・`.worktrees` へのパスは、下記の
63
+ spec が同梱の全ファイルで拒否するため、コメントにも残さない)。単体 `shippedTextSelfContained.spec.ts` (新規)
64
+ は同梱物を `package.json` の `files` から導出し (`dist`・spec を除く `src`・README・CHANGELOG・LICENSE と
65
+ `package.json`)、本パッケージ独自の文書参照の規則 (ADR 番号、設計・実装プランに続く節記号、URL の一部では
66
+ ない `docs` の下の階層と `.agents`・`.worktrees` へのパス。規則は spec の中で定義し、モノレポの漏洩ガードの
67
+ 規則ファイルは読まない) を、全パスと全ファイルの全文へコメントも含め、エスケープを復号した形にも当てる。
68
+ 読み手向けのより厳しい規則 (数字の続く節記号、設計文書を題名で指すこと、変異台帳の ID) は、README・
69
+ CHANGELOG・型宣言・sourcemap (対応表 `mappings` を除く)・スタイルシート (コメントを除く) と、コードの
70
+ 文字列・テンプレート・JSX の文字へ当てる。検出は `ファイル:行: [規則] "一致"` の一覧で示し、`files` の
71
+ 項目が何にも一致しなければ (ビルド前の `dist` など) 名前を示して失敗する。
72
+
73
+ ### Fixed
74
+
75
+ - **行ラッパーの平行移動を装置の画素へ揃える**: 行を包むラッパーは自前の合成層 (`will-change: transform`) を
76
+ `transform: translateY(…)` で動かす。その量がスクロール位置の端数 (トラックパッドや慣性の移動量・中央揃え・
77
+ 端数の行の高さやインセット) をそのまま持つと、ブラウザは層全体を再標本化する。実画面ではペイン位置 2440.5px で
78
+ 2px の輪郭・2px の隙間・2px の輪が 3+1+3 行に滲み (下辺では隙間が消え)、文字もぼけた。平行移動は一覧を描く
79
+ ウィンドウの装置の画素の格子へ揃える (`Math.round(量 × devicePixelRatio) / devicePixelRatio`。比 1 では整数 px、
80
+ 1.25 では 0.8px、1.5 では 2/3px、2 では 0.5px の刻み)。比はラッパーの文書のウィンドウ (iframe や別ウィンドウに
81
+ 描いた一覧ではそのウィンドウ) からラッパーの取り付け時に読み、ブラウザの拡大縮小や密度の違う画面への移動で
82
+ 変わると `(resolution: <比>dppx)` のメディアクエリで読み直す。揃えるのはラッパーの平行移動だけで、行の位置
83
+ (層の中の端数の位置は描画が画素へ揃える) と、`onScroll` / `onRangeChange` / `getScrollPosition()` /
84
+ `getScrollAnchor()` が示す位置は厳密値のまま (見える位置のずれは最大で半装置画素)。`VirtualGrid` の行も
85
+ 同じラッパーで描くので縦に揃う。サーバー描画の HTML は厳密値を持ち (サーバーに画面は無い)、ハイドレーションで
86
+ 揃えた値に置き換わる。クライアントでのマウントは最初の paint の前に比を確定する。ウィンドウを持たない文書
87
+ (`document.implementation.createHTMLDocument()` で作った文書など) へ描くと、ラッパーの取り付けで `Error` を
88
+ 投げる (描かれない文書には揃える比が無い)。
89
+
90
+ ## 3.7.1 (2026-10-01)
91
+
92
+ ### Fixed
93
+
94
+ - **Pointer Capture API の無い環境 (jsdom) で `TypeError`**: `ScrollPane` (コンテンツ領域のドラッグ・
95
+ ドラッグの打ち切り・ドラッグ無効化・アンマウントの後始末) と `TapScrollCircle` (解放・指の
96
+ ハンドオフ) が `hasPointerCapture` / `releasePointerCapture` を存在確認なしで呼んでいた。React の
97
+ 既定のテスト環境である jsdom は API を 1 つも実装しないため、利用側のテストで行を `user.click`
98
+ するだけで `TypeError: element.hasPointerCapture is not a function` が `window` の `error` へ
99
+ 報告されていた (ハンドラ内の例外なので `fireEvent` / `user.click` 自体は投げず、検出しにくい)。
100
+ - **能力判定を 1 本化**: 新モジュール `src/pointerCapture.ts` (非公開) が「取得・解放・保持確認の
101
+ 3 メソッドが揃った要素だけをキャプチャ対応とみなす」規則を持ち、`ScrollPane` / `ScrollBar` /
102
+ `TapScrollCircle` のすべての取得・解放・保持確認がここを通る (`ScrollBar` が個別に持っていた
103
+ `?.` / `if (element.setPointerCapture)` のガードも置き換えた)。非対応の要素ではキャプチャ無しで
104
+ 操作を続け、例外は投げない。API がある環境での挙動 (`lostpointercapture` との順序・
105
+ `TapScrollCircle` の生存プローブ・`NotFoundError` の扱い) は不変。縮退の内容は README の
106
+ 「Environments without the Pointer Capture API (jsdom)」に記載。公開 API の変更は無い。
107
+ - **テスト**: 単体 `pointerCapture.spec.ts` 22 件 (新規) と、`Element.prototype` /
108
+ `HTMLElement.prototype` から API を取り除いた素の jsdom で行の `user.click`・コンテンツ / サム /
109
+ トラック / タップサークルの押下・移動・解放・指のハンドオフ・途中終了経路を駆動し `window` の
110
+ `error` が 0 件であることを表明する `pointerCaptureUnsupported.spec.tsx` 14 件 (新規。修正前の
111
+ ソースでは 8 件が red)。`ScrollBar.spec.tsx` の 3 件は API を一式で差し込む形へ変更 (1 メソッド
112
+ だけの差し込みは非対応と判定され、キャプチャ要求そのものが行われず空砲になるため)。
113
+ vitest 924 / 924 (50 ファイル、`pnpm test` 1 回の実測)。
114
+
5
115
  ## 3.7.0 (2026-09-24)
6
116
 
7
117
  - **新機能 — 内蔵 UI 文言の多言語化 (`locale` / `labels`)**: コンポーネント自身が描画する 7 文言
@@ -67,13 +177,13 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
67
177
  単調時の 56%。x は `hxRef` 単一書き手のため無事)。修正は**軸別位置権威の x 側への整列**:
68
178
  `getVy` を埋め込みハンドル `getScrollPosition()` (= `applyVy` の `scrollBy` が updater を解決する
69
179
  ペイン内部 ref と同一権威) の読みへ変更 — 「各軸の `getPos` は適用シームと同一権威を読む」を
70
- ドライバ契約として登記 (グリッド仕様 §3.6 ADR-31 行 / MV-XY38-39)。`applyVy`・x 経路・
180
+ ドライバ契約とした。`applyVy`・x 経路・
71
181
  `computeTapScrollVelocity`・フックのループ意味論はバイト不変。
72
182
  - **デモ / ゲート**: エンジンデモに `/grid-tap` ルートを新設 (VirtualGrid + 統合サークル)。
73
183
  実ブラウザ E2E `grid-tap-scroll.spec.ts` (ページ内 rAF サンプラーで純 x / 純 y 1 秒保持の
74
184
  フレーム単調性 + 前進下限) と、決定的 rAF ハーネスの配線単体ゲート
75
185
  `gridTapAxisAuthority.spec.tsx` 3 件 (純 y / 斜め xy / 純 x の毎フレーム一様前進 —
76
- 鏡像退行変異 MV-XY38/39 で red 実証) を追加。単体 819 / 819 (46 ファイル)。
186
+ y の読みを鏡像へ戻す変異 2 本で red を実証) を追加。単体 819 / 819 (46 ファイル)。
77
187
 
78
188
  ## 3.6.0 (2026-09-18)
79
189
 
@@ -85,15 +195,15 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
85
195
  帯幅非依存・凍結トグル不変)、**既定オフセットは両軸 −200** (到達性導出 — バー既定 −80/0 では
86
196
  ない)。opt-out は従来どおり `tapScrollCircleOptions: { enabled: false }` — 今後は**唯一の
87
197
  サークルを殺す**。封鎖コンテンツ面積は半減 (40 px ディスク 1 枚)。既定 ON の挙動置換を
88
- **minor** とする判断は ADR-32 登記: 置換対象が出荷済み欠陥・再解釈フィールド
198
+ **minor** とした理由: 置換対象が出荷済み欠陥・再解釈フィールド
89
199
  (`offsetX`/`offsetY` — バー相対 → コーナー相対) への in-repo 依存ゼロ・型削除ゼロ (いずれか
90
200
  1 つでも崩れていれば 4.0.0 だった)。
91
201
  - **速度則**: 方向余弦分解 `v = (s_x(r)·ox/r, s_y(r)·oy/r)` — `s_a` は無改造
92
- `computeTapScrollSpeed` の軸別評価 (純軸ドラッグは各バー則と画素法則恒等 = T1)。新規純関数
202
+ `computeTapScrollSpeed` の軸別評価 (純軸ドラッグは各バーの速度則と画素単位で恒等)。新規純関数
93
203
  `computeTapScrollVelocity` + 型 `TapScrollVelocityInput` / `TapScrollAxisSpeedParams` を
94
204
  **バレル export ×3**。`TapScrollCircle.axis` は `"x" | "y" | "xy"` の射影部分空間へ一般化
95
205
  (既定 `"y"` 不変・state 型不変 — `"xy"` の `direction` は径方向係合 `{0, 1}`)。
96
- - **登記デルタ (グリッド仕様 §3.6 の一覧が正典)**: キャンセルイベント無条件リセット (旧: 縦は
206
+ - **バーのタップループとの挙動差**: キャンセルイベント無条件リセット (旧: 縦は
97
207
  paneId フィルタ → 一様に無条件 = 厳密 fail-closed) / 片軸境界はループ非停止・境界フラグ毎
98
208
  フレーム (反転で同一保持中に端から退却) / 軸上ピンの純軸ドラッグはループをパーク (角度付き
99
209
  pointermove で再開) / pendingColAnchor は**適用済み** hx デルタのみで解除 (バーの要求時規則
@@ -103,8 +213,7 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
103
213
  意図縦 ⇒ 交差 829 px/s (2 s で ≈ 8+ 列 @200 px 列)、意図横 ⇒ 交差 1,533 px/s (2 s で
104
214
  ≈ **96 行** @32 px 行)。受入閾値 (タッチ実機・クローズドループ): 中央値 ≤ 1 列 (意図縦) /
105
215
  ≤ 6 行 (意図横) — 超過時は連続リマップ `φ_ε(θ) = θ − ε·sin 4θ` を 3.6.0 既定として出荷する
106
- 事前コミット。異方性下ではコンテンツ速度角 ≠ ドラッグ角 (全手掛かりはドラッグ角を表示 —
107
- 登記)。遮蔽 / 到達性の閾値表はグリッド仕様 §3.6。
216
+ 事前コミット。異方性下ではコンテンツ速度角 ≠ ドラッグ角 (全手掛かりはドラッグ角を表示)。
108
217
  - **`axis="x"` の引き transform 修正**: `translateY` → `translateX` (「右ドラッグで下へ動く」
109
218
  出荷済み quirk — 裸バー opt-in 経由でのみ到達・in-repo 消費者ゼロ)。`"y"` はバイト恒等。
110
219
  - **forced-colors**: `.aqvs-tap-scroll-circle` へパッケージ初の `forced-colors: active` 輪郭
@@ -119,26 +228,26 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
119
228
  `stretchMainSize` (縦バー限定でインライン主軸寸をスキップ — 横バーは stretch が主軸を寸法
120
229
  決めできないため書込み維持) を `ScrollPane` が `isSelfMeasuringViewport` で結線。幾何は
121
230
  `viewportSize` の数値由来で無傷。回帰ゲート = 実 Chromium ラダー E2E
122
- (800→1300→800→600→1000 双方向回復)。ScrollPane 仕様 §2.5 に機構を登記。
123
- - 単体 815 / 815 (45 ファイル) 全緑・変異台帳 MV-XY 37 行 + MV-SM 3 行 (物理 43 変異) 全 red 実測。
231
+ (800→1300→800→600→1000 双方向回復)。
232
+ - 単体 815 / 815 (45 ファイル) 全緑・変異 43 本すべての red を実測。
124
233
 
125
234
  ## 3.5.0 (2026-09-14)
126
235
 
127
236
  - **新機能 — `VirtualGrid` の両軸末尾凍結 (`frozenTrailingCols` / `frozenTrailingRows`)**:
128
- 末尾凍結はエンジン機能 (ADR-18 — route A)。列側は幅減算 + 右クリップ — スクロール帯幅を
237
+ 末尾凍結はエンジン側の機能。列側は幅減算 + 右クリップ — スクロール帯幅を
129
238
  `viewport − W_F − W_T` へ一般化し (`scrollBandWidthRef` の単一 ref 源)、末尾セルは右アンカー
130
239
  クリップ `.aqvs-grid-row-trailing` / `-inner` の**帯ローカル left** で描画する (アンカー機構の
131
240
  外 — 先頭帯の鏡像)。行側は `.aqvs-grid-main` の**下**の第 2 帯外バンド `.aqvs-grid-trailing-rows`
132
241
  (クリップ高 = H_T_vis インライン、inner は木高 H_T の bottom 定着 — 帯上端 = extent − H_T_vis
133
242
  が全構成で成立)。帯行は `renderRow` 逐語再利用で 4 象限コーナー全種が無償成立。複合帯の
134
- 優先順位は LEADING > TRAILING > MIDDLE (ADR-19): 中帯が最初に潰れ (`bandWidth ≤ 0` は正準空窓
243
+ 優先順位は LEADING > TRAILING > MIDDLE: 中帯が最初に潰れ (`bandWidth ≤ 0` は正準空窓
135
244
  start > end)、末尾が 2 番目 (CSS `max()` 左端 / bottom 定着 inner)、可視クリップの単一情報源は
136
245
  `trailingVisibleSize` (消費先はテンプレート高 / hbar `paddingRight` / `getFrozenSize` の vis
137
246
  export の 3 点ちょうど)。
138
247
  - **契約**: 両 props は [0, `MAX_FROZEN_TRAILING_COLS` / `MAX_FROZEN_TRAILING_ROWS` = 128] の整数
139
- fail-fast (RangeError)・動的クランプは `min(T, count − 先頭実効)` で**先頭がカウント空間で勝つ**
140
- (ADR-19-1)。**登記済み帰結**: 先頭 + 末尾の帯寸がビューポートを超えるとき、隠れた末尾セルは
141
- どのスクロール位置でも可視化できない (文書化されたクランプ — `scrollToCell` の末尾 no-op は
248
+ fail-fast (RangeError)・動的クランプは `min(T, count − 先頭実効)` で**先頭がカウント空間で勝つ**。
249
+ **文書化した帰結**: 先頭 + 末尾の帯寸がビューポートを超えるとき、隠れた末尾セルはどの
250
+ スクロール位置でも可視化できない (文書化されたクランプ — `scrollToCell` の末尾 no-op は
142
251
  そこでも正確)。T = 0 は DOM 構造・インライン属性までバイト恒等 (F = 0 / R = 0 恒等の双子)。
143
252
  `scrollToCell` / `initialScrollAnchor` の末尾狙いは当該軸 no-op (列は既存アンカー残置)、
144
253
  `updateRowSize` / `focusRowAtIndex` は 3 分岐 (末尾帯行は `trailingRowEpoch` 繰上げ / 帯行 DOM
@@ -150,15 +259,15 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
150
259
  - **`--aqvs-grid-trailing-width`**: 新設のパッケージスコープ CSS 変数 (0px フォールバック)。
151
260
  T > 0 のときのみ書かれ T = 0 で除去される**単一書き手**規律。`.aqvs-grid-row-scroll` の
152
261
  `right` は `var(--aqvs-grid-trailing-width, 0px)` へ差替え (T = 0 は計算結果までバイト恒等)。
153
- - **ADR-25 — `data-aqvs-frozen-row` の付与を F_eff > 0 へゲート分離**: 従来は凍結分岐と結合して
262
+ - **`data-aqvs-frozen-row` の付与を F_eff > 0 へゲート分離**: 従来は凍結分岐と結合して
154
263
  いたため、末尾のみ (F = 0 ∧ T > 0) のグリッドが凍結列ゼロで全行に属性を出す嘘になるところ
155
264
  だった。class `aqvs-grid-row-frozen-host` は「横帯モード」の描画トレイトとして残る。既存構成
156
265
  (F > 0 / T = 0) への挙動変化ゼロ。
157
- - **ADR-23 — T の実行時変更は埋め込み VirtualScroll を再マウントしない**: `key` は R 専用のまま、
266
+ - **T の実行時変更は埋め込み VirtualScroll を再マウントしない**: `key` は R 専用のまま、
158
267
  itemCount が**末尾**で伸縮するだけ (getItem / getItemKey / getItemHeight = i + R 不変)。視点は
159
268
  トグルを跨いで無償保存される。**保留アンカーの帰結 (文書化)**: T 増加で末尾帯入りする行を
160
269
  狙った保留行アンカーは最終スクロール行へクランプされる (itemCount 変化ごとの再ピン留め +
161
- `sanitizeIndex` — 「保留アンカーは R 再マウントで死ぬ」の鏡像)。MV-TR-A1..A4 ゲートで実測確定
270
+ `sanitizeIndex` — 「保留アンカーは R 再マウントで死ぬ」の鏡像)。専用ゲート 4 件で実測確定
162
271
  (740/740 全緑)。
163
272
 
164
273
  ## 3.4.0 (2026-09-03)
@@ -171,7 +280,7 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
171
280
  ゲートを `ScrollBar.spec.tsx` に追加 (695/695 全緑)。
172
281
 
173
282
  - **新機能 — `VirtualGrid` の凍結先頭行 (`frozenLeadingRows`)**: 帯外バンド + インデックス
174
- シフト方式 (行凍結設計プラン §2)。凍結行 [0, R) はグリッド所有の `.aqvs-grid-frozen-rows`
283
+ シフト方式。凍結行 [0, R) はグリッド所有の `.aqvs-grid-frozen-rows`
175
284
  バンドとして pane の外 (上) に描画し、行本体は `renderRow` をそのまま再利用 — F > 0 の
176
285
  凍結セル構造 (静的セル / row-scroll クリップ / hx 残差 var) が帯行内でも成立し**凍結行 ×
177
286
  凍結列の 4 象限コーナーが無償で成立**。埋め込み VirtualScroll にはシフト済み行空間
@@ -208,14 +317,14 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
208
317
  全行を再レンダーさせていた perf 欠陥の是正 (+R 挙動は不変)。(3) getScrollAnchor ハンドル
209
318
  docstring に縮退帯 null (H_F がビューポートを埋め itemCount 0 → null) を明記。
210
319
  - **検証**: 専用 spec `VirtualGridFrozenRows.spec.tsx` 19 (パッケージ 694/694、42 ファイル)
211
- と変異 MV-FR30〜FR45 red 実測 (FR40 は ×2 変異とも red。getItemKey +R シフト撤去のみ
212
- GT-2 として ⚠️ 等価登記 — 現状仕様 §5 台帳、全 102 変異 = 101 red + 1 ⚠️ 等価。
320
+ と変異 16 本の red 実測 (うち 1 本は 2 通りの変異とも red。getItemKey の +R シフト撤去だけは
321
+ 観測上の等価変異として記録 — 全 102 変異 = 101 red + 1 等価。
213
322
  `VirtualGridFrozen.spec.tsx` の `getFrozenSize` deep-equal ピン 4 件も rows / height 込みへ
214
323
  更新)。
215
324
 
216
325
  ## 3.3.0 (単独未公開 — 3.4.0 に同梱して公開)
217
326
 
218
- - **新機能 — `VirtualGrid` の凍結先頭列 (`frozenLeadingCols`)**: 設計 §8 の 3 形状要件を実装。
327
+ - **新機能 — `VirtualGrid` の凍結先頭列 (`frozenLeadingCols`)**: 次の 3 形状要件を実装。
219
328
  (a) スクロール帯の窓探索は木座標 W_F + hx 始まり (幅 0 凍結列の床クランプ込み)、(b) 残差は
220
329
  静的クリップ (`.aqvs-grid-row-scroll`) 内の inner が `calc(残差 − W_F)` で運ぶ (クリップと
221
330
  transform の分離は構造要件)、(c) 凍結帯はアンカー機構の外の静的 subtree (木絶対 left 直接
@@ -224,11 +333,10 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
224
333
  **F = 0 は DOM 構造まで従来と恒等** (既存 648 テスト無変更 green)。契約: [0, 128] の整数
225
334
  fail-fast (`MAX_FROZEN_LEADING_COLS` 新 export)・colCount 超過は動的クランプ (全列凍結)・
226
335
  凍結列狙いの scrollToCell / initialScrollAnchor の x は no-op・`getFrozenSize()` ハンドル新設。
227
- 検証: 専用 spec 27 (パッケージ 675/675) + 変異 MV-FR1〜FR29 全 red 実測 (現状仕様 §5 台帳 —
228
- R1 敵対レビューの実証欠陥 2 件 (全列凍結の両帯重複 / ヒール共有バッチの上書き非対称) と
229
- 実証穴 2 件 (帯ガード無ゲート / CSS 特異度) を封鎖済み)。publish 振付: 3.2.0 と 3.3.0 は
230
- 積層未リリースのため、版ラベル整合には凍結列コミットの親で 3.2.0 → HEAD で 3.3.0 の
231
- 2 段 publish が必要 (現状仕様 §8 の正準記録)。
336
+ 検証: 専用 spec 27 (パッケージ 675/675) + 変異 29 本すべての red 実測 (R1 敵対レビューの
337
+ 実証欠陥 2 件 (全列凍結の両帯重複 / ヒール共有バッチの上書き非対称) と実証穴 2 件 (帯ガード
338
+ 無ゲート / CSS 特異度) を封鎖済み)。publish 振付: 3.2.0 と 3.3.0 は積層未リリースのため、
339
+ 版ラベル整合には凍結列コミットの親で 3.2.0 → HEAD で 3.3.0 の 2 段 publish が必要。
232
340
 
233
341
  ## 3.2.0 (単独未公開 — 3.4.0 に同梱して公開)
234
342
 
@@ -244,14 +352,19 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
244
352
  の消費者はグリッド所有 `.aqvs-grid-row` の translateX (ペイン構造クラスを侵さない)。ヘッダー
245
353
  同期は範囲通知 (`VirtualGridRange`) + 消費側の窓原点相対方式。行側パリティ引継ぎ:
246
354
  `horizontalKeyInputs` / `horizontalKeyStep` / `contentInsets` / `background` / `onRowFocus` /
247
- `focusRowAtIndex` / `behaviorOptions.defaultColWidth` (全メンバー分類表は
248
- `docs/specs/2026.09.01 [AI] virtualscroll-virtual-grid.md` §3.1 / §3.2)。設計正典:
249
- `docs/plans/2026.09.01 [AI] virtualgrid-horizontal-axis-design-plan.md` (v6 認証済み)。
355
+ `focusRowAtIndex` / `behaviorOptions.defaultColWidth`。意図的に持たない行側メンバーは
356
+ `initialScrollIndex` / `initialScrollOffset` (`initialScrollAnchor` が唯一の口)・`onWheelHorizontal` /
357
+ `onPanHorizontal` (横軸はグリッドの所有)・ハンドルの `scrollToIndex` (`scrollToCell` が上位互換) /
358
+ `getFenwickTreeTotalHeight` / `getFenwickSize`。有界化契約 (両軸):
359
+ 祖先スケール z と描画窓スパン windowSpan (≤ viewport + (2 × overscan + 2) × `MAX_TRACK_SIZE`) に
360
+ ついて `z × (ANCHOR_REBASE_DISTANCE + windowSpan) ≤ 2^24` である限りアンカー基準の DOM 座標
361
+ (セル / 行の位置と残差 transform) は完全精度帯に収まり、overscan が既定の 3 で viewport が
362
+ 2^13 px 以下なら z = 5.31 まで成り立つ。
250
363
  - **シーム — `ScrollPane` / `VirtualScroll` に `onPanHorizontal`**: ポインタパン横成分の委譲
251
364
  (onWheelHorizontal のパン対・同符号・per-move 増分・押下時横 scale 除算)。指定時は横移動でも
252
365
  ドラッグが発動する。未指定の既存消費者への影響ゼロ。❗ 横軸を `Omit<>` で封じるラッパー
253
366
  (directory-tree) は 3.2.0 更新時に本シームを封印リストへ追加し (pass-through review)、
254
- **select-box / daily-report の影響なし確認**も同時に行うこと (設計 §11)。
367
+ **select-box / daily-report の影響なし確認**も同時に行うこと。
255
368
  - **改修 — `ScrollBar` 横タップサークル**: `enableHorizontalTapCircle` (opt-in — 単体既定不変)
256
369
  で横バーにもタップスクロールサークルを描画。`TapScrollCircle` の方向・距離・デッドゾーン
257
370
  導出を軸パラメータ化 (`axis` prop)。
@@ -268,17 +381,17 @@ All notable changes to `@aiquants/virtualscroll` are documented here.
268
381
  アクセサ identity を再構築キーへ届くよう修正 (従来は構造的に不発)。`defaultColWidth` は実行時
269
382
  変更にも追随。liveRegion は行側契約へ整列 (既定 400ms・不正値は警告 + 無効化 — 黙示代替も変異で
270
383
  封止)。横タップサークルへ `itemCount = colCount` を自動配線しタップ速度の log10(colCount) 則
271
- (設計 B7 対策) を実現、`tapScrollCircleOptions` も横バーへ透過。self-heal バーストは窓非依存の
272
- 「連続する非収束」計数 (解消は収束パスのみ — 窓端を動かす振幅の振動アクセサにも回避されず、
273
- 停止後はどの窓かでの収束 1 回で再武装。逐次の正当な治癒は誤認しない)。
384
+ を実現 (10^13 列ではサム 1 px ≈ 200 億列で、サムでは列を狙えないため)、`tapScrollCircleOptions` も
385
+ 横バーへ透過。self-heal バーストは窓非依存の「連続する非収束」計数 (解消は収束パスのみ —
386
+ 窓端を動かす振幅の振動アクセサにも回避されず、停止後はどの窓かでの収束 1 回で再武装。逐次の
387
+ 正当な治癒は誤認しない)。
274
388
  - **品質**: vitest 648 / 648 = 単体 618 + 統合 30 (VirtualGrid 59 + 横シーム 10 + Fenwick baseValue 4
275
- の新設ゲート込み)・変異 56 本 (MV-A〜MV-BD — アンカー / 残差 / 上限 / 打切り / clamp / パン符号・発動・scale /
389
+ の新設ゲート込み)・変異 56 本 (アンカー / 残差 / 上限 / 打切り / clamp / パン符号・発動・scale /
276
390
  タップ軸 / バー追従 / 幅エポック / 分母見積り / self-heal / 境界規約 / baseValue / contentProps
277
391
  持込 / 総幅同期 / 具現化計数 / リサイズ無効化 / 実測引継ぎ / identity 追随 / 暴走ガード /
278
392
  再ピン留め / 手動解除 / 同 tick 鮮度 / debounce 検証・黙示代替 / keyStep 透過 / overscan 15 化 /
279
393
  見積り基点 / 解除 3 サイト / 速度則配線 / 収束解消 / 蓄積撤去 / 停止恒久化 / tap オプション透過 /
280
- 非表示行スキップ / gBCR 計測復帰) 全 red 実測。台帳は
281
- `docs/specs/2026.09.01 [AI] virtualscroll-virtual-grid.md` §5。
394
+ 非表示行スキップ / gBCR 計測復帰) 全 red 実測。
282
395
 
283
396
  ## 3.1.1 (2026-08-28)
284
397
 
@@ -378,7 +491,7 @@ A-5 のうち**固定リピートタイマは実装を見送り**ました: 長
378
491
  ### Notes
379
492
 
380
493
  - デモに `/overscroll` (ページに縦あふれを持つ連鎖検証ルート) を追加。
381
- - ❗ E2E 作者向けの罠 (テスト仕様書 §6 に詳細): 離散的な合成ホイールを**ペインより上**のページ要素で
494
+ - ❗ E2E 作者向けの罠: 離散的な合成ホイールを**ペインより上**のページ要素で
382
495
  打つと、compositor が先にページをスクロールし、イベント配送時の hit-test が「ずり上がってきた行」に
383
496
  当たって 1 ノッチがページとペインの両方へ入る (実測: listener 内の rect が移動済みの座標を示す)。
384
497
  実ジェスチャはスクロールラッチで保護される。陽性コントロールの打点は**ペインより下**に取ること。
@@ -421,7 +534,7 @@ A-5 のうち**固定リピートタイマは実装を見送り**ました: 長
421
534
  - 2.5.0 の既知の限界に「両 ref を layout effect へ移すとタップスクロールの走破が短くなる退行が実機
422
535
  E2E で出る」と記録していましたが、**8 回 x 3 系列の A/B/A 対照実験で反証されました**。当時観測した
423
536
  失敗は書き込み位置と無関係のコールドスタート負荷フレーク (rAF が ~10fps へ落ち、`deltaSeconds` が
424
- 100ms キャップに張り付いてサンプル数の前提が崩れる — テスト仕様書 §6.1 に記載済みの既知モード) で、
537
+ 100ms キャップに張り付いてサンプル数の前提が崩れる既知のモード) で、
425
538
  render 本体書き込みの有無に関わらず同率 (6-7/8) で失敗します。「3 回失敗 → revert → 3 回成功」は
426
539
  負荷の時間的クラスタリングが生んだ錯覚でした。全 1,893 回のクランプ監査で committed 値と
427
540
  render 新値の食い違いは 0 件です。
@@ -524,7 +637,7 @@ A-5 のうち**固定リピートタイマは実装を見送り**ました: 長
524
637
  - 行は依然としてタブ順に入りません (`tabIndex={-1}`)。キーボードのみで横スクロールへ到達させたい消費側は
525
638
  `handle.focusItemAtIndex(0)` などでフォーカスの入口を用意してください (ロービングタブインデックスの
526
639
  導入は全消費側のタブ順を変えるため見送り)。デモ (`/horizontal`) にその実演ボタンを追加しました。
527
- - 既知の限界 (いずれも仕様書 §11.4 に記載): 行内でドラッグ選択/ダブルクリックすると選択が残り、それを
640
+ - 既知の限界: 行内でドラッグ選択/ダブルクリックすると選択が残り、それを
528
641
  畳むキーボード手段が無いため `Shift + ←/→` が効かないままになる / Shadow DOM 内の選択は検出できない /
529
642
  横スクロールは支援技術へ何も伝わらない / 縦スクロールでフォーカス行が仮想化で消えるとフォーカスが
530
643
  `<body>` へ落ちる / 位置を量子化する消費側では `horizontalKeyStep` が量子未満だと 1px も動かない /
package/README.md CHANGED
@@ -13,7 +13,7 @@ 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**: Escape row-return (`enableEscapeRowReturn`) and a screen-reader live region (`liveRegion`) announcing the visible range with your own wording
16
+ - ♿ **Accessibility opt-ins**: Escape row-return (`enableEscapeRowReturn`), a screen-reader live region (`liveRegion`) announcing the visible range with your own wording, and scrollbar arrows out of the Tab order for apps with their own keyboard scrolling (`enableArrowButtonTabStops`)
17
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)
18
18
  - 🔧 **TypeScript**: Full TypeScript support with comprehensive type definitions
19
19
 
@@ -135,6 +135,31 @@ the rendered row count and scrollability all derive from it, so all five would b
135
135
  spilling over the next element, a dead band at the bottom, an end you can never reach, a thumb that
136
136
  leaves the viewport, or a list that ignores the wheel entirely).
137
137
 
138
+ ## Device-pixel snapping
139
+
140
+ The rows move inside one wrapper that is translated with `transform: translateY(…)` on its own compositor
141
+ layer (`will-change: transform`). That translate is snapped to the device-pixel grid of the window the list
142
+ is painted in, `Math.round(offset × devicePixelRatio) / devicePixelRatio`: whole CSS pixels at a ratio of 1,
143
+ steps of 0.8 px at 1.25, 2/3 px at 1.5 and 0.5 px at 2. Fractional scroll positions (trackpad and inertia
144
+ deltas, centred alignment, fractional row heights or insets) therefore never move the layer by a fraction of
145
+ a device pixel. A layer moved by a fraction is resampled as a whole: 2 px borders, outlines, focus rings and
146
+ the gaps between them smear across neighbouring pixel rows, and text blurs.
147
+
148
+ - The ratio comes from the window of the list's own document (a list rendered into an iframe or a second
149
+ window uses that window's ratio). It is read when the wrapper attaches and read again whenever it
150
+ changes (browser zoom, or moving the window to a screen of another density), through a
151
+ `(resolution: <ratio>dppx)` media query.
152
+ - Only the wrapper translate is snapped. Row positions inside it keep their exact values (the browser paints
153
+ fractional positions inside a layer on whole pixels already), and every position the component reports
154
+ (`onScroll`, `onRangeChange`, `getScrollPosition()`, `getScrollAnchor()`) stays exact. The rows are drawn
155
+ at most half a device pixel away from the exact scroll position. `VirtualGrid` rows are drawn by the same
156
+ wrapper, so they are snapped vertically as well.
157
+ - Server-rendered HTML carries the exact offset, since there is no screen on the server; the snapped offset
158
+ replaces it when the list hydrates. A client-side mount commits the ratio before its first paint.
159
+ - Rendering into a document without a window (for example one made with
160
+ `document.implementation.createHTMLDocument()`) throws when the wrapper attaches: nothing is painted there,
161
+ so there is no ratio to snap to.
162
+
138
163
  ## Horizontal Scrolling
139
164
 
140
165
  `VirtualScroll` virtualizes and scrolls the **vertical** axis only. To add a horizontal axis — e.g. a
@@ -327,6 +352,35 @@ itself has focus (an `Escape` on the row means whatever *you* decide — e.g. de
327
352
  does act it calls `preventDefault()` only and does not stop propagation, the same contract as the
328
353
  arrow keys.
329
354
 
355
+ ## Scrollbar arrows and the Tab order
356
+
357
+ The scrollbar's two arrow buttons are Tab stops by default, because they are the only scrolling
358
+ control a keyboard user can reach with Tab: the pane moves its content by transform (there is no
359
+ native scroller for the browser to make focusable) and the rows are not Tab stops. Focus an arrow
360
+ and Enter / Space scrolls one step.
361
+
362
+ When your app already provides keyboard scrolling — roving focus with Arrow / Page / Home / End on
363
+ the rows, a grid keyboard model — the arrows only add two redundant stops per bar. Take them out of
364
+ the Tab order:
365
+
366
+ ```tsx
367
+ <VirtualScroll
368
+ scrollBarOptions={{ enableArrowButtonTabStops: false }} // the rows own keyboard scrolling
369
+ ...
370
+ />
371
+ ```
372
+
373
+ `false` sets `tabIndex={-1}` and changes nothing else: pointer presses and press-and-hold repeat keep
374
+ working, the arrows keep their accessible names, and Enter / Space still scroll an arrow you focus
375
+ from script — the position native scrollbars are in, which are never Tab stops either. What the
376
+ package does not promise is that assistive technology lists the arrows as buttons of their own:
377
+ they are descendants of the `role="scrollbar"` element, whose children ARIA 1.2 makes
378
+ presentational. Chromium exposes them anyway; other browsers may present the whole bar as one
379
+ control. The option has one name and one meaning everywhere: `ScrollBar` and `ScrollPane` props,
380
+ and `scrollBarOptions` of `VirtualScroll` and `VirtualGrid` (the grid applies it to both of its
381
+ bars). While the arrows are disabled (`enableArrowButtons: false`, or nothing to scroll) it has no
382
+ effect — a disabled button is never focusable.
383
+
330
384
  ## Screen-reader live region
331
385
 
332
386
  Virtualization removes off-screen rows from the DOM, so a screen-reader user scrolling the list
@@ -440,8 +494,14 @@ trillion columns sit comfortably inside).
440
494
  </VirtualGrid>
441
495
  ```
442
496
 
443
- Contracts (see the design plan under `docs/plans/` for the full bounding theorem):
497
+ Contracts:
444
498
 
499
+ - Bounding contract (both axes): the anchored coordinates the grid writes to the DOM — cell and
500
+ row offsets and the residual transforms — stay in the full-precision band while
501
+ `z × (ANCHOR_REBASE_DISTANCE + windowSpan) <= 2^24`, where `z` is the ancestor `scale(z)`,
502
+ `ANCHOR_REBASE_DISTANCE` is 2^20 px and `windowSpan <= viewport + (2 × overscan + 2) × MAX_TRACK_SIZE`
503
+ is the rendered window. The next two contracts are its premises (the z ≈ 5.3 they yield assumes
504
+ a viewport of at most 2^13 px).
445
505
  - `getRowHeight` / `getColWidth` return integer px, `0` = hidden, `<= MAX_TRACK_SIZE`
446
506
  (2^18 px) — oversized tracks fail fast with a `RangeError` (a single unbounded track would
447
507
  pierce the measured browser layout wall at 2^25 px).
@@ -459,11 +519,18 @@ Contracts (see the design plan under `docs/plans/` for the full bounding theorem
459
519
  - Row-side parity carries: `horizontalKeyInputs` / `horizontalKeyStep` (arrow keys reach the
460
520
  grid-owned horizontal axis), `contentInsets`, `background`, `onRowFocus` /
461
521
  `focusRowAtIndex`, and `behaviorOptions.defaultColWidth` (explicit Fenwick baseValue — skips
462
- width sampling). The full member-by-member classification lives in
463
- `docs/specs/2026.09.01 [AI] virtualscroll-virtual-grid.md`.
522
+ width sampling). Deliberately not offered: `initialScrollIndex` / `initialScrollOffset` (use
523
+ `initialScrollAnchor`), `onWheelHorizontal` / `onPanHorizontal` (the grid owns its horizontal
524
+ axis and consumes those deltas itself), and on the handle `scrollToIndex` (use `scrollToCell`)
525
+ and `getFenwickTreeTotalHeight` / `getFenwickSize` (use `getContentSize` and the counts you pass).
464
526
  - `locale` / `labels` (see [Localization](#localization)) are forwarded to BOTH the embedded
465
527
  `VirtualScroll` (vertical arrows and the "No items" empty state shown for 0 rows, an all-frozen row
466
528
  set or a degenerate band) and the horizontal `ScrollBar` (left / right arrows).
529
+ - `scrollBarOptions`: the bar-local members — `width`, `enableThumbDrag`, `enableTrackClick`,
530
+ `enableArrowButtons` and `enableArrowButtonTabStops` — apply to BOTH bars alike, so no setting
531
+ covers only half of the grid. `tapScrollCircleOptions` configures the single two-axis circle;
532
+ `renderThumbOverlay` (fed with visible row indices) and `enableScrollToTopBottomButtons` are
533
+ row-axis features of the embedded `VirtualScroll`.
467
534
  - Frozen leading columns (v3.3.0): `frozenLeadingCols` pins the first F columns as a static
468
535
  band OUTSIDE the anchor machinery (direct tree-absolute lefts; scroll cells live inside a
469
536
  static clip whose inner carries the `residual - W_F` origin-shift transform). Integer in
@@ -503,7 +570,7 @@ Contracts (see the design plan under `docs/plans/` for the full bounding theorem
503
570
  the tree height H_T). Band rows reuse the normal row renderer, so every corner of the
504
571
  3 x 3 region grid comes for free. Integers in `[0, MAX_FROZEN_TRAILING_COLS]` /
505
572
  `[0, MAX_FROZEN_TRAILING_ROWS]` (128) — `RangeError` otherwise; the dynamic clamp is
506
- `min(T, count - effectiveLeading)`: the LEADING band wins the count space (ADR-19-1), and
573
+ `min(T, count - effectiveLeading)`: the LEADING band wins the count space, and
507
574
  when the leading + trailing extents exceed the viewport, occluded trailing cells cannot be
508
575
  scrolled into view (a documented clamp — `scrollToCell` / `initialScrollAnchor` targeting a
509
576
  trailing track stay no-ops on that axis, the column no-op leaving any armed column anchor in
@@ -629,6 +696,7 @@ exists in the DOM.
629
696
  | `enableThumbDrag` | `boolean` | Enable dragging the scrollbar thumb (default: true) |
630
697
  | `enableTrackClick` | `boolean` | Enable clicking the scrollbar track (default: true) |
631
698
  | `enableArrowButtons` | `boolean` | Enable arrow buttons on the scrollbar (default: true) |
699
+ | `enableArrowButtonTabStops` | `boolean` | Keep the two arrow buttons in the Tab order (default: true). Set `false` when your app already owns keyboard scrolling of the list: the arrows then get `tabIndex={-1}` and stay pointer-operable and named. See [Scrollbar arrows and the Tab order](#scrollbar-arrows-and-the-tab-order) |
632
700
  | `enableScrollToTopBottomButtons` | `boolean` | Enable the auto-hiding Top/Bottom pills (texts from `labels.scrollToTop` / `labels.scrollToBottom`; default: false) |
633
701
  | `renderThumbOverlay` | `(props: ScrollBarThumbOverlayRenderProps) => ReactNode` | Render prop to anchor custom UI near the scrollbar thumb |
634
702
  | `tapScrollCircleOptions` | `ScrollBarTapCircleOptions` | Customization for the auxiliary tap scroll circle |
@@ -917,6 +985,25 @@ export function UltraFastExample() {
917
985
  - Safari 14+
918
986
  - Edge 88+
919
987
 
988
+ ### Environments without the Pointer Capture API (jsdom)
989
+
990
+ The components also run where the Pointer Capture API (`setPointerCapture` / `releasePointerCapture` /
991
+ `hasPointerCapture`) is missing — notably jsdom, the default environment of React unit tests — so
992
+ consumers need no shim. A Testing Library `user.click` on a row, or a press / move / release on the
993
+ content area, the scrollbar thumb / track or the tap-scroll circle, never throws.
994
+
995
+ An element counts as capture-capable only when it implements all three methods. On any other element
996
+ every interaction runs without capture, and only the guarantees that capture provides degrade:
997
+
998
+ - **Still works**: content and thumb drags follow the pointer through `window` / `document` listeners;
999
+ the track and the tap-scroll circle follow it while it stays over them; the click after a content
1000
+ drag is still suppressed.
1001
+ - **Degrades**: a release outside the window or over an iframe is not received, so a drag can outlive
1002
+ the press (the content area ends a mouse / pen drag, and the track a mouse drag, on the next move
1003
+ with no button pressed); track and circle drags stop following once the
1004
+ pointer leaves them; a forced capture loss (`lostpointercapture`) is not observed; and the tap
1005
+ circle's finger hand-off cannot tell whether the waiting pointer is still down, so it always hands off.
1006
+
920
1007
  ## License
921
1008
 
922
1009
  MIT
@@ -80,6 +80,40 @@ export type ScrollBarProps = {
80
80
  enableTrackClick?: boolean;
81
81
  /** Whether arrow buttons control the scroll position. / 矢印ボタンによるスクロール操作を許可するかどうか。 */
82
82
  enableArrowButtons?: boolean;
83
+ /**
84
+ * Whether the two arrow buttons are Tab stops (default `true`).
85
+ *
86
+ * `false` renders both with `tabIndex={-1}`, which takes them out of the sequential focus order
87
+ * and changes nothing else: pointer presses and press-and-hold repeat, the accessible names and
88
+ * Enter / Space on an arrow focused from script all keep working. The arrows are descendants of
89
+ * the `role="scrollbar"` root, whose children ARIA 1.2 makes presentational, so whether assistive
90
+ * technology lists them as buttons of their own is the browser's choice (Chromium does); this
91
+ * option changes nothing about that. Set it to `false` when the host already owns keyboard
92
+ * scrolling of this viewport (roving row focus with Arrow / Page / Home / End, a grid keyboard
93
+ * model): the arrows then duplicate that path and cost every keyboard user two extra Tab presses
94
+ * per bar — native scrollbars are never Tab stops either. The default stays `true` because
95
+ * without such a host model the arrows are the only scrolling control a keyboard user can reach
96
+ * with Tab: `ScrollPane` / `VirtualScroll` move their content by transform, so there is no native
97
+ * scroller the browser could make focusable, and their rows are not Tab stops. No effect while
98
+ * the arrows are disabled (`enableArrowButtons: false`, or nothing to scroll) — a disabled button
99
+ * is never focusable.
100
+ *
101
+ * 矢印ボタン 2 個を Tab の止まり先にするかどうか (既定 `true`)。
102
+ *
103
+ * `false` は両方を `tabIndex={-1}` で描画し、順次フォーカス移動の順序から外すだけの指定。
104
+ * ポインタ押下と長押しリピート、アクセシブルネーム、スクリプトからフォーカスした矢印での
105
+ * Enter / Space はすべて維持。矢印は `role="scrollbar"` のルートの子孫で、ARIA 1.2 はその子を
106
+ * presentational とするため、支援技術が矢印を独立したボタンとして示すかどうかはブラウザの選択
107
+ * (Chromium は示す) であり、本オプションはそこに関与しない。ホストがこのビューポートのキーボード
108
+ * スクロールを既に持つ場合 (行のロービングフォーカスと矢印 / Page / Home / End、グリッドのキーボードモデル) に
109
+ * `false` を指定 — 矢印はその経路の重複となり、キーボード利用者にバー 1 本あたり 2 回の余分な Tab を
110
+ * 課すため (ネイティブのスクロールバーも Tab の止まり先にならない)。既定を `true` に据え置く理由は、
111
+ * ホスト側のモデルが無ければ矢印がキーボード利用者の Tab で届く唯一のスクロール操作部品であること
112
+ * (`ScrollPane` / `VirtualScroll` はコンテンツを transform で動かすため、ブラウザがフォーカス可能に
113
+ * できるネイティブのスクロール領域が無く、行も Tab の止まり先ではない)。矢印が無効な間
114
+ * (`enableArrowButtons: false` またはスクロール不要) は効果なし — disabled のボタンはそもそもフォーカス不能。
115
+ */
116
+ enableArrowButtonTabStops?: boolean;
83
117
  /** Whether the scrollbar is horizontal. / スクロールバーが水平かどうか。 */
84
118
  horizontal?: boolean;
85
119
  /**
@@ -266,5 +300,5 @@ export declare const computeAutoTapScrollMaxSpeedMultiplier: (itemCount?: number
266
300
  *
267
301
  * カスタムスクロールバーコンポーネント。
268
302
  */
269
- export declare const ScrollBar: ({ contentSize, viewportSize, scrollPosition, onScroll, enableThumbDrag, enableTrackClick, enableArrowButtons, horizontal, enableHorizontalTapCircle, stretchMainSize, scrollBarWidth, className, ariaControls, tapScrollCircleOptions, itemCount, renderThumbOverlay, visibleStartIndex, visibleEndIndex, locale, labels, }: ScrollBarProps) => import("react").JSX.Element;
303
+ 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;
270
304
  export {};
@@ -80,6 +80,40 @@ export type ScrollBarProps = {
80
80
  enableTrackClick?: boolean;
81
81
  /** Whether arrow buttons control the scroll position. / 矢印ボタンによるスクロール操作を許可するかどうか。 */
82
82
  enableArrowButtons?: boolean;
83
+ /**
84
+ * Whether the two arrow buttons are Tab stops (default `true`).
85
+ *
86
+ * `false` renders both with `tabIndex={-1}`, which takes them out of the sequential focus order
87
+ * and changes nothing else: pointer presses and press-and-hold repeat, the accessible names and
88
+ * Enter / Space on an arrow focused from script all keep working. The arrows are descendants of
89
+ * the `role="scrollbar"` root, whose children ARIA 1.2 makes presentational, so whether assistive
90
+ * technology lists them as buttons of their own is the browser's choice (Chromium does); this
91
+ * option changes nothing about that. Set it to `false` when the host already owns keyboard
92
+ * scrolling of this viewport (roving row focus with Arrow / Page / Home / End, a grid keyboard
93
+ * model): the arrows then duplicate that path and cost every keyboard user two extra Tab presses
94
+ * per bar — native scrollbars are never Tab stops either. The default stays `true` because
95
+ * without such a host model the arrows are the only scrolling control a keyboard user can reach
96
+ * with Tab: `ScrollPane` / `VirtualScroll` move their content by transform, so there is no native
97
+ * scroller the browser could make focusable, and their rows are not Tab stops. No effect while
98
+ * the arrows are disabled (`enableArrowButtons: false`, or nothing to scroll) — a disabled button
99
+ * is never focusable.
100
+ *
101
+ * 矢印ボタン 2 個を Tab の止まり先にするかどうか (既定 `true`)。
102
+ *
103
+ * `false` は両方を `tabIndex={-1}` で描画し、順次フォーカス移動の順序から外すだけの指定。
104
+ * ポインタ押下と長押しリピート、アクセシブルネーム、スクリプトからフォーカスした矢印での
105
+ * Enter / Space はすべて維持。矢印は `role="scrollbar"` のルートの子孫で、ARIA 1.2 はその子を
106
+ * presentational とするため、支援技術が矢印を独立したボタンとして示すかどうかはブラウザの選択
107
+ * (Chromium は示す) であり、本オプションはそこに関与しない。ホストがこのビューポートのキーボード
108
+ * スクロールを既に持つ場合 (行のロービングフォーカスと矢印 / Page / Home / End、グリッドのキーボードモデル) に
109
+ * `false` を指定 — 矢印はその経路の重複となり、キーボード利用者にバー 1 本あたり 2 回の余分な Tab を
110
+ * 課すため (ネイティブのスクロールバーも Tab の止まり先にならない)。既定を `true` に据え置く理由は、
111
+ * ホスト側のモデルが無ければ矢印がキーボード利用者の Tab で届く唯一のスクロール操作部品であること
112
+ * (`ScrollPane` / `VirtualScroll` はコンテンツを transform で動かすため、ブラウザがフォーカス可能に
113
+ * できるネイティブのスクロール領域が無く、行も Tab の止まり先ではない)。矢印が無効な間
114
+ * (`enableArrowButtons: false` またはスクロール不要) は効果なし — disabled のボタンはそもそもフォーカス不能。
115
+ */
116
+ enableArrowButtonTabStops?: boolean;
83
117
  /** Whether the scrollbar is horizontal. / スクロールバーが水平かどうか。 */
84
118
  horizontal?: boolean;
85
119
  /**
@@ -266,6 +300,6 @@ export declare const computeAutoTapScrollMaxSpeedMultiplier: (itemCount?: number
266
300
  *
267
301
  * カスタムスクロールバーコンポーネント。
268
302
  */
269
- export declare const ScrollBar: ({ contentSize, viewportSize, scrollPosition, onScroll, enableThumbDrag, enableTrackClick, enableArrowButtons, horizontal, enableHorizontalTapCircle, stretchMainSize, scrollBarWidth, className, ariaControls, tapScrollCircleOptions, itemCount, renderThumbOverlay, visibleStartIndex, visibleEndIndex, locale, labels, }: ScrollBarProps) => import("react").JSX.Element;
303
+ 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;
270
304
  export {};
271
305
  //# sourceMappingURL=ScrollBar.d.ts.map