@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.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @module pointerCapture
3
+ * @description The single capability rule for the Pointer Capture API, shared by every pointer interaction in this package.
4
+ *
5
+ * @description 本パッケージのすべてのポインタ操作が共有する、Pointer Capture API の唯一の能力判定モジュール。
6
+ *
7
+ * `ScrollPane` (コンテンツのドラッグスクロール)・`ScrollBar` (サム / トラックのドラッグ)・
8
+ * `TapScrollCircle` (タップスクロールサークル) は、キャプチャの取得・解放・保持確認を
9
+ * 必ずここを通して行う。要素の `setPointerCapture` / `releasePointerCapture` /
10
+ * `hasPointerCapture` を直接呼んではならない。
11
+ *
12
+ * ❗ **3 つのメソッドが揃っている要素だけを「対応」とみなす。** jsdom (React のテストの既定環境) は
13
+ * このどれも実装しないため、直接呼ぶと利用側の `user.click` 1 回で `TypeError` になる。
14
+ * 一部だけを実装する環境で「取得はしたが解放も確認もできない」状態を作らないよう、判定は 1 本にしてある。
15
+ *
16
+ * 非対応の要素ではキャプチャ無しで操作を続ける (縮退)。コンテンツ領域とサムのドラッグは
17
+ * `window` / `document` のリスナーが追従を受け持つため動き続け、トラックとタップサークルは
18
+ * ポインタが要素上にある間だけ追従する。失われるのは、ウインドウ外や iframe 上で離したときの
19
+ * 終端イベントの受信と、`lostpointercapture` による強制解放の検知である。
20
+ */
21
+ /**
22
+ * Outcome of a pointer capture request.
23
+ * ポインタキャプチャ要求の結果。
24
+ *
25
+ * - `"captured"`: The element now holds the capture. / 要素がキャプチャを保持した。
26
+ * - `"unsupported"`: The element does not implement the Pointer Capture API; nothing was attempted. / 要素が API を実装しておらず、要求自体を行っていない。
27
+ * - `"inactive-pointer"`: The UA rejected the pointer as inactive (`NotFoundError`). / ポインタが非アクティブとして拒否された (`NotFoundError`)。
28
+ * - `"rejected"`: The UA rejected the request for any other reason. / それ以外の理由で拒否された。
29
+ */
30
+ export type PointerCaptureResult = "captured" | "unsupported" | "inactive-pointer" | "rejected";
31
+ /**
32
+ * Reports whether the element implements the whole Pointer Capture API.
33
+ * 要素が Pointer Capture API (取得・解放・保持確認の 3 メソッド) をすべて実装しているかの判定処理。
34
+ *
35
+ * @param element The element to inspect. 判定対象の要素。
36
+ * @returns True when set / release / has are all callable. 3 メソッドがすべて呼び出し可能な場合に true。
37
+ */
38
+ export declare const supportsPointerCapture: (element: Element) => boolean;
39
+ /**
40
+ * Reports whether the element holds (or has a pending) capture of the pointer.
41
+ * 要素が指定ポインタのキャプチャを保持 (または保留) しているかの判定処理。
42
+ *
43
+ * @param element The element to inspect. 判定対象の要素。
44
+ * @param pointerId The pointer to look up. 対象のポインタ ID。
45
+ * @returns True when the capture is held; always false when the API is unsupported. 保持している場合に true。API 非対応の要素では常に false。
46
+ */
47
+ export declare const isPointerCaptured: (element: Element, pointerId: number) => boolean;
48
+ /**
49
+ * Requests capture of the pointer for the element and classifies the outcome without throwing.
50
+ * 要素へ指定ポインタのキャプチャを要求し、例外を投げずに結果を分類する処理。
51
+ *
52
+ * @param element The element that should receive the capture. キャプチャを受け取る要素。
53
+ * @param pointerId The pointer to capture. キャプチャするポインタ ID。
54
+ * @returns The classified outcome of the request. 要求結果の分類。
55
+ */
56
+ export declare const capturePointer: (element: Element, pointerId: number) => PointerCaptureResult;
57
+ /**
58
+ * Releases the element's capture of the pointer when it holds one; does nothing otherwise.
59
+ * 要素が指定ポインタのキャプチャを保持していれば解放し、保持していなければ何もしない処理。
60
+ *
61
+ * 呼び出し側が状態を確定させてから呼ぶこと。`lostpointercapture` を同期発火するブラウザでは、
62
+ * この呼び出しの最中に喪失ハンドラーが走る。
63
+ *
64
+ * @param element The element that may hold the capture. キャプチャを保持している可能性のある要素。
65
+ * @param pointerId The pointer to release. 解放するポインタ ID。
66
+ * @returns Nothing. なし。
67
+ */
68
+ export declare const releaseCapturedPointer: (element: Element, pointerId: number) => void;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @module pointerCapture
3
+ * @description The single capability rule for the Pointer Capture API, shared by every pointer interaction in this package.
4
+ *
5
+ * @description 本パッケージのすべてのポインタ操作が共有する、Pointer Capture API の唯一の能力判定モジュール。
6
+ *
7
+ * `ScrollPane` (コンテンツのドラッグスクロール)・`ScrollBar` (サム / トラックのドラッグ)・
8
+ * `TapScrollCircle` (タップスクロールサークル) は、キャプチャの取得・解放・保持確認を
9
+ * 必ずここを通して行う。要素の `setPointerCapture` / `releasePointerCapture` /
10
+ * `hasPointerCapture` を直接呼んではならない。
11
+ *
12
+ * ❗ **3 つのメソッドが揃っている要素だけを「対応」とみなす。** jsdom (React のテストの既定環境) は
13
+ * このどれも実装しないため、直接呼ぶと利用側の `user.click` 1 回で `TypeError` になる。
14
+ * 一部だけを実装する環境で「取得はしたが解放も確認もできない」状態を作らないよう、判定は 1 本にしてある。
15
+ *
16
+ * 非対応の要素ではキャプチャ無しで操作を続ける (縮退)。コンテンツ領域とサムのドラッグは
17
+ * `window` / `document` のリスナーが追従を受け持つため動き続け、トラックとタップサークルは
18
+ * ポインタが要素上にある間だけ追従する。失われるのは、ウインドウ外や iframe 上で離したときの
19
+ * 終端イベントの受信と、`lostpointercapture` による強制解放の検知である。
20
+ */
21
+ /**
22
+ * Outcome of a pointer capture request.
23
+ * ポインタキャプチャ要求の結果。
24
+ *
25
+ * - `"captured"`: The element now holds the capture. / 要素がキャプチャを保持した。
26
+ * - `"unsupported"`: The element does not implement the Pointer Capture API; nothing was attempted. / 要素が API を実装しておらず、要求自体を行っていない。
27
+ * - `"inactive-pointer"`: The UA rejected the pointer as inactive (`NotFoundError`). / ポインタが非アクティブとして拒否された (`NotFoundError`)。
28
+ * - `"rejected"`: The UA rejected the request for any other reason. / それ以外の理由で拒否された。
29
+ */
30
+ export type PointerCaptureResult = "captured" | "unsupported" | "inactive-pointer" | "rejected";
31
+ /**
32
+ * Reports whether the element implements the whole Pointer Capture API.
33
+ * 要素が Pointer Capture API (取得・解放・保持確認の 3 メソッド) をすべて実装しているかの判定処理。
34
+ *
35
+ * @param element The element to inspect. 判定対象の要素。
36
+ * @returns True when set / release / has are all callable. 3 メソッドがすべて呼び出し可能な場合に true。
37
+ */
38
+ export declare const supportsPointerCapture: (element: Element) => boolean;
39
+ /**
40
+ * Reports whether the element holds (or has a pending) capture of the pointer.
41
+ * 要素が指定ポインタのキャプチャを保持 (または保留) しているかの判定処理。
42
+ *
43
+ * @param element The element to inspect. 判定対象の要素。
44
+ * @param pointerId The pointer to look up. 対象のポインタ ID。
45
+ * @returns True when the capture is held; always false when the API is unsupported. 保持している場合に true。API 非対応の要素では常に false。
46
+ */
47
+ export declare const isPointerCaptured: (element: Element, pointerId: number) => boolean;
48
+ /**
49
+ * Requests capture of the pointer for the element and classifies the outcome without throwing.
50
+ * 要素へ指定ポインタのキャプチャを要求し、例外を投げずに結果を分類する処理。
51
+ *
52
+ * @param element The element that should receive the capture. キャプチャを受け取る要素。
53
+ * @param pointerId The pointer to capture. キャプチャするポインタ ID。
54
+ * @returns The classified outcome of the request. 要求結果の分類。
55
+ */
56
+ export declare const capturePointer: (element: Element, pointerId: number) => PointerCaptureResult;
57
+ /**
58
+ * Releases the element's capture of the pointer when it holds one; does nothing otherwise.
59
+ * 要素が指定ポインタのキャプチャを保持していれば解放し、保持していなければ何もしない処理。
60
+ *
61
+ * 呼び出し側が状態を確定させてから呼ぶこと。`lostpointercapture` を同期発火するブラウザでは、
62
+ * この呼び出しの最中に喪失ハンドラーが走る。
63
+ *
64
+ * @param element The element that may hold the capture. キャプチャを保持している可能性のある要素。
65
+ * @param pointerId The pointer to release. 解放するポインタ ID。
66
+ * @returns Nothing. なし。
67
+ */
68
+ export declare const releaseCapturedPointer: (element: Element, pointerId: number) => void;
69
+ //# sourceMappingURL=pointerCapture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointerCapture.d.ts","sourceRoot":"","sources":["../src/pointerCapture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;GAQG;AACH,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG,aAAa,GAAG,kBAAkB,GAAG,UAAU,CAAA;AAE/F;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,OAAO,KAAG,OAAoK,CAAA;AAE9N;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,OAAO,EAAE,WAAW,MAAM,KAAG,OAAkF,CAAA;AAE1J;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,EAAE,WAAW,MAAM,KAAG,oBAYpE,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GAAI,SAAS,OAAO,EAAE,WAAW,MAAM,KAAG,IAI5E,CAAA"}
@@ -2,7 +2,7 @@ import { TapScrollAxisSpeedParams } from './computeTapScrollVelocity.cjs';
2
2
  import { TapScrollCircleDragState, TapScrollCircleHandle } from './TapScrollCircle.cjs';
3
3
  /**
4
4
  * Default corner-relative offset (px) for BOTH grid tap-circle axes — the reachability arm (R)
5
- * of the three-arm placement law (plan §5). Derivation: full speed needs
5
+ * of the three-arm placement law. Derivation: full speed needs
6
6
  * `maxVisualDistance = 240 px` of advancing pull toward the far screen edge, which is a hard
7
7
  * stop on touch (`touch-action: none`). With `off = −200` the disc center sits
8
8
  * `sbw − off − size/2 = 12 + 200 − 20 = 192 px` inboard of each far root edge, giving a
@@ -12,11 +12,11 @@ import { TapScrollCircleDragState, TapScrollCircleHandle } from './TapScrollCirc
12
12
  * nav-zone envelope ≥ ~2/3. Deliberately a LITERAL: it must NOT track a consumer-supplied
13
13
  * `maxVisualDistance` (coupling two knobs would make one silently move the other), and
14
14
  * shrinking `maxVisualDistance` instead is forbidden (it changes the speed law's `maxDistance`
15
- * input and breaks the T1 identity anchor).
15
+ * input, so a pure-axis drag would no longer reproduce the bar's own speed law).
16
16
  * グリッドタップサークル両軸のコーナー相対既定オフセット (px) — 3 アーム配置則の到達性アーム
17
17
  * (R)。導出: 全速には前進方向へ 240 px の引きが要り、タッチではスクリーン端がハードストップ。
18
18
  * −200 で中心は各遠端から 192 px 内側 = 最悪ケース引き分率 0.80 (ナビ帯込み 0.66)。意図的な
19
- * リテラル — 消費側 `maxVisualDistance` へ連動させない (ノブ連動の登記済み禁止)。
19
+ * リテラル — 消費側 `maxVisualDistance` へ連動させない (2 つのノブを連動させると一方が他方を黙って動かす)。
20
20
  */
21
21
  export declare const GRID_TAP_CIRCLE_DEFAULT_OFFSET = -200;
22
22
  /** Parameters for `useGridTapScroll` (read live through a params ref — identity-stability is not required of callers). / `useGridTapScroll` のパラメータ (params ref 経由で毎フレーム最新を読む — 呼出し側に identity 安定を要求しない)。 */
@@ -25,9 +25,9 @@ export type UseGridTapScrollParams = {
25
25
  enabled: boolean;
26
26
  /** Shared pull range (= `max(maxVisualDistance, 1)`). / 共有引き範囲 (= `max(maxVisualDistance, 1)`)。 */
27
27
  maxDistance: number;
28
- /** Horizontal speed parameters (§4.1 Pₓ). / 横軸速度パラメータ (§4.1 Pₓ)。 */
28
+ /** Horizontal speed parameters (Pₓ, built from the scroll-band width and the column count). / 横軸速度パラメータ (Pₓ。スクロール帯幅と列数から構成)。 */
29
29
  xSpeedParams: TapScrollAxisSpeedParams;
30
- /** Vertical speed parameters (§4.1 P_y). / 縦軸速度パラメータ (§4.1 P_y)。 */
30
+ /** Vertical speed parameters (P_y, built from the embedded pane height and the scroll-row count). / 縦軸速度パラメータ (P_y。埋め込みペイン高とスクロール行数から構成)。 */
31
31
  ySpeedParams: TapScrollAxisSpeedParams;
32
32
  /** The grid's x apply seam (absolute position in, clamped applied position out). / グリッドの x 適用シーム (絶対位置 → クランプ後位置)。 */
33
33
  applyHxRef: {
@@ -35,21 +35,21 @@ export type UseGridTapScrollParams = {
35
35
  };
36
36
  /** Synchronous-fresh hx read. / hx の同期・最新読み。 */
37
37
  getHx: () => number;
38
- /** The ONE extracted x clamp law (`getMaxHx` SSOT — §3.5-7). / 唯一抽出の x クランプ則 (`getMaxHx` SSOT — §3.5-7)。 */
38
+ /** The ONE extracted x clamp law (`getMaxHx` SSOT — never re-derived from the grid's tree). / 唯一抽出の x クランプ則 (`getMaxHx` SSOT — グリッドの木から再導出しない)。 */
39
39
  getMaxHx: () => number;
40
- /** y apply seam: wraps `scrollBy(delta)` AND syncs `vyRef.current = applied` before returning (§3.5-8 — NOT a bare handle passthrough). / y 適用シーム: `scrollBy(delta)` をラップし `vyRef.current = applied` を同期してから返す (§3.5-8 — 素のハンドル素通しではない)。 */
40
+ /** y apply seam: wraps `scrollBy(delta)` AND syncs `vyRef.current = applied` before returning (NOT a bare handle passthrough — without the sync, x-frame notifications during a diagonal drag carry a stale y). / y 適用シーム: `scrollBy(delta)` をラップし `vyRef.current = applied` を同期してから返す (素のハンドル素通しではない — 同期が無いと対角ドラッグ中の x フレーム通知が stale な y を運ぶ)。 */
41
41
  applyVy: (delta: number) => number;
42
42
  /** Synchronous-fresh vy read — MUST read the same authority `applyVy` resolves against (the embedded pane's position), never a notification mirror with a second writer (v3.6.1 — a lagging mirror base overstates `actualDelta` and drives the residual negative = yo-yo). / vy の同期・最新読み — `applyVy` が解決するのと同一権威 (埋め込みペイン位置) を読むこと。第 2 の書き手を持つ通知鏡像は禁止 (v3.6.1 — 遅延鏡像基準は `actualDelta` を過大化し残差を負へ落とすヨーヨー)。 */
43
43
  getVy: () => number;
44
44
  /** y extent read through the embedded handle's freshness channel (never re-derived). / 埋め込みハンドルの鮮度チャネル経由の y 延長 (再導出禁止)。 */
45
45
  getMaxVy: () => number;
46
- /** Pending column anchor — cleared ONLY on frames whose APPLIED hx delta ≠ 0 (§3.5-5). / 保留列アンカー — 適用済み hx デルタ ≠ 0 のフレームのみ解除 (§3.5-5)。 */
46
+ /** Pending column anchor — cleared ONLY on frames whose APPLIED hx delta ≠ 0 (a request the clamp swallows keeps it). / 保留列アンカー — 適用済み hx デルタ ≠ 0 のフレームのみ解除 (クランプに吸収された要求では残す)。 */
47
47
  pendingColAnchorRef: {
48
48
  current: unknown;
49
49
  };
50
- /** x extent-growth freshness key (columnWindow / total width — §3.5-3). / x 延長成長の鮮度キー (columnWindow / 総幅 — §3.5-3)。 */
50
+ /** x extent-growth freshness key (columnWindow / total width — a change re-arms a loop parked at the edge). / x 延長成長の鮮度キー (columnWindow / 総幅 — 変化で端に止まったループを再武装)。 */
51
51
  xExtentFreshness: unknown;
52
- /** y extent-growth freshness key (the vertical range state — §3.5-3). / y 延長成長の鮮度キー (縦レンジ state — §3.5-3)。 */
52
+ /** y extent-growth freshness key (the vertical range state — a change re-arms a loop parked at the edge). / y 延長成長の鮮度キー (縦レンジ state — 変化で端に止まったループを再武装)。 */
53
53
  yExtentFreshness: unknown;
54
54
  };
55
55
  /** Result of `useGridTapScroll`. / `useGridTapScroll` の返り値。 */
@@ -65,7 +65,7 @@ export type UseGridTapScrollResult = {
65
65
  * Owns the unified tap circle's drag state and the two-axis rAF integration loop.
66
66
  * 統合タップサークルのドラッグ状態と 2 軸 rAF 積分ループを所有するフック。
67
67
  *
68
- * Per-frame semantics (plan §8.3 — implemented verbatim):
68
+ * Per-frame semantics:
69
69
  *
70
70
  * ```text
71
71
  * dt = min(max((t − last)/1000, 0), TAP_SCROLL_MAX_FRAME_DELTA_SECONDS); skip if dt ≤ 0
@@ -2,7 +2,7 @@ import { TapScrollAxisSpeedParams } from './computeTapScrollVelocity.ts';
2
2
  import { TapScrollCircleDragState, TapScrollCircleHandle } from './TapScrollCircle.tsx';
3
3
  /**
4
4
  * Default corner-relative offset (px) for BOTH grid tap-circle axes — the reachability arm (R)
5
- * of the three-arm placement law (plan §5). Derivation: full speed needs
5
+ * of the three-arm placement law. Derivation: full speed needs
6
6
  * `maxVisualDistance = 240 px` of advancing pull toward the far screen edge, which is a hard
7
7
  * stop on touch (`touch-action: none`). With `off = −200` the disc center sits
8
8
  * `sbw − off − size/2 = 12 + 200 − 20 = 192 px` inboard of each far root edge, giving a
@@ -12,11 +12,11 @@ import { TapScrollCircleDragState, TapScrollCircleHandle } from './TapScrollCirc
12
12
  * nav-zone envelope ≥ ~2/3. Deliberately a LITERAL: it must NOT track a consumer-supplied
13
13
  * `maxVisualDistance` (coupling two knobs would make one silently move the other), and
14
14
  * shrinking `maxVisualDistance` instead is forbidden (it changes the speed law's `maxDistance`
15
- * input and breaks the T1 identity anchor).
15
+ * input, so a pure-axis drag would no longer reproduce the bar's own speed law).
16
16
  * グリッドタップサークル両軸のコーナー相対既定オフセット (px) — 3 アーム配置則の到達性アーム
17
17
  * (R)。導出: 全速には前進方向へ 240 px の引きが要り、タッチではスクリーン端がハードストップ。
18
18
  * −200 で中心は各遠端から 192 px 内側 = 最悪ケース引き分率 0.80 (ナビ帯込み 0.66)。意図的な
19
- * リテラル — 消費側 `maxVisualDistance` へ連動させない (ノブ連動の登記済み禁止)。
19
+ * リテラル — 消費側 `maxVisualDistance` へ連動させない (2 つのノブを連動させると一方が他方を黙って動かす)。
20
20
  */
21
21
  export declare const GRID_TAP_CIRCLE_DEFAULT_OFFSET = -200;
22
22
  /** Parameters for `useGridTapScroll` (read live through a params ref — identity-stability is not required of callers). / `useGridTapScroll` のパラメータ (params ref 経由で毎フレーム最新を読む — 呼出し側に identity 安定を要求しない)。 */
@@ -25,9 +25,9 @@ export type UseGridTapScrollParams = {
25
25
  enabled: boolean;
26
26
  /** Shared pull range (= `max(maxVisualDistance, 1)`). / 共有引き範囲 (= `max(maxVisualDistance, 1)`)。 */
27
27
  maxDistance: number;
28
- /** Horizontal speed parameters (§4.1 Pₓ). / 横軸速度パラメータ (§4.1 Pₓ)。 */
28
+ /** Horizontal speed parameters (Pₓ, built from the scroll-band width and the column count). / 横軸速度パラメータ (Pₓ。スクロール帯幅と列数から構成)。 */
29
29
  xSpeedParams: TapScrollAxisSpeedParams;
30
- /** Vertical speed parameters (§4.1 P_y). / 縦軸速度パラメータ (§4.1 P_y)。 */
30
+ /** Vertical speed parameters (P_y, built from the embedded pane height and the scroll-row count). / 縦軸速度パラメータ (P_y。埋め込みペイン高とスクロール行数から構成)。 */
31
31
  ySpeedParams: TapScrollAxisSpeedParams;
32
32
  /** The grid's x apply seam (absolute position in, clamped applied position out). / グリッドの x 適用シーム (絶対位置 → クランプ後位置)。 */
33
33
  applyHxRef: {
@@ -35,21 +35,21 @@ export type UseGridTapScrollParams = {
35
35
  };
36
36
  /** Synchronous-fresh hx read. / hx の同期・最新読み。 */
37
37
  getHx: () => number;
38
- /** The ONE extracted x clamp law (`getMaxHx` SSOT — §3.5-7). / 唯一抽出の x クランプ則 (`getMaxHx` SSOT — §3.5-7)。 */
38
+ /** The ONE extracted x clamp law (`getMaxHx` SSOT — never re-derived from the grid's tree). / 唯一抽出の x クランプ則 (`getMaxHx` SSOT — グリッドの木から再導出しない)。 */
39
39
  getMaxHx: () => number;
40
- /** y apply seam: wraps `scrollBy(delta)` AND syncs `vyRef.current = applied` before returning (§3.5-8 — NOT a bare handle passthrough). / y 適用シーム: `scrollBy(delta)` をラップし `vyRef.current = applied` を同期してから返す (§3.5-8 — 素のハンドル素通しではない)。 */
40
+ /** y apply seam: wraps `scrollBy(delta)` AND syncs `vyRef.current = applied` before returning (NOT a bare handle passthrough — without the sync, x-frame notifications during a diagonal drag carry a stale y). / y 適用シーム: `scrollBy(delta)` をラップし `vyRef.current = applied` を同期してから返す (素のハンドル素通しではない — 同期が無いと対角ドラッグ中の x フレーム通知が stale な y を運ぶ)。 */
41
41
  applyVy: (delta: number) => number;
42
42
  /** Synchronous-fresh vy read — MUST read the same authority `applyVy` resolves against (the embedded pane's position), never a notification mirror with a second writer (v3.6.1 — a lagging mirror base overstates `actualDelta` and drives the residual negative = yo-yo). / vy の同期・最新読み — `applyVy` が解決するのと同一権威 (埋め込みペイン位置) を読むこと。第 2 の書き手を持つ通知鏡像は禁止 (v3.6.1 — 遅延鏡像基準は `actualDelta` を過大化し残差を負へ落とすヨーヨー)。 */
43
43
  getVy: () => number;
44
44
  /** y extent read through the embedded handle's freshness channel (never re-derived). / 埋め込みハンドルの鮮度チャネル経由の y 延長 (再導出禁止)。 */
45
45
  getMaxVy: () => number;
46
- /** Pending column anchor — cleared ONLY on frames whose APPLIED hx delta ≠ 0 (§3.5-5). / 保留列アンカー — 適用済み hx デルタ ≠ 0 のフレームのみ解除 (§3.5-5)。 */
46
+ /** Pending column anchor — cleared ONLY on frames whose APPLIED hx delta ≠ 0 (a request the clamp swallows keeps it). / 保留列アンカー — 適用済み hx デルタ ≠ 0 のフレームのみ解除 (クランプに吸収された要求では残す)。 */
47
47
  pendingColAnchorRef: {
48
48
  current: unknown;
49
49
  };
50
- /** x extent-growth freshness key (columnWindow / total width — §3.5-3). / x 延長成長の鮮度キー (columnWindow / 総幅 — §3.5-3)。 */
50
+ /** x extent-growth freshness key (columnWindow / total width — a change re-arms a loop parked at the edge). / x 延長成長の鮮度キー (columnWindow / 総幅 — 変化で端に止まったループを再武装)。 */
51
51
  xExtentFreshness: unknown;
52
- /** y extent-growth freshness key (the vertical range state — §3.5-3). / y 延長成長の鮮度キー (縦レンジ state — §3.5-3)。 */
52
+ /** y extent-growth freshness key (the vertical range state — a change re-arms a loop parked at the edge). / y 延長成長の鮮度キー (縦レンジ state — 変化で端に止まったループを再武装)。 */
53
53
  yExtentFreshness: unknown;
54
54
  };
55
55
  /** Result of `useGridTapScroll`. / `useGridTapScroll` の返り値。 */
@@ -65,7 +65,7 @@ export type UseGridTapScrollResult = {
65
65
  * Owns the unified tap circle's drag state and the two-axis rAF integration loop.
66
66
  * 統合タップサークルのドラッグ状態と 2 軸 rAF 積分ループを所有するフック。
67
67
  *
68
- * Per-frame semantics (plan §8.3 — implemented verbatim):
68
+ * Per-frame semantics:
69
69
  *
70
70
  * ```text
71
71
  * dt = min(max((t − last)/1000, 0), TAP_SCROLL_MAX_FRAME_DELTA_SECONDS); skip if dt ≤ 0
@@ -1 +1 @@
1
- {"version":3,"file":"useGridTapScroll.d.ts","sourceRoot":"","sources":["../src/useGridTapScroll.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAA4B,KAAK,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAEvG,OAAO,KAAK,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAG5F;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,8BAA8B,OAAO,CAAA;AAalD,4MAA4M;AAC5M,MAAM,MAAM,sBAAsB,GAAG;IACjC,uIAAuI;IACvI,OAAO,EAAE,OAAO,CAAA;IAChB,mGAAmG;IACnG,WAAW,EAAE,MAAM,CAAA;IACnB,oEAAoE;IACpE,YAAY,EAAE,wBAAwB,CAAA;IACtC,oEAAoE;IACpE,YAAY,EAAE,wBAAwB,CAAA;IACtC,sHAAsH;IACtH,UAAU,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;KAAE,CAAA;IAC1D,gDAAgD;IAChD,KAAK,EAAE,MAAM,MAAM,CAAA;IACnB,4GAA4G;IAC5G,QAAQ,EAAE,MAAM,MAAM,CAAA;IACtB,4OAA4O;IAC5O,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAClC,8YAA8Y;IAC9Y,KAAK,EAAE,MAAM,MAAM,CAAA;IACnB,2HAA2H;IAC3H,QAAQ,EAAE,MAAM,MAAM,CAAA;IACtB,0IAA0I;IAC1I,mBAAmB,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAA;IACzC,uHAAuH;IACvH,gBAAgB,EAAE,OAAO,CAAA;IACzB,8GAA8G;IAC9G,gBAAgB,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,+DAA+D;AAC/D,MAAM,MAAM,sBAAsB,GAAG;IACjC,4FAA4F;IAC5F,WAAW,EAAE,OAAO,CAAA;IACpB,qGAAqG;IACrG,yBAAyB,EAAE,CAAC,KAAK,EAAE,wBAAwB,KAAK,IAAI,CAAA;IACpE,4HAA4H;IAC5H,kBAAkB,EAAE,KAAK,CAAC,SAAS,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAA;CACpE,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,sBAAsB,KAAG,sBAuQjE,CAAA"}
1
+ {"version":3,"file":"useGridTapScroll.d.ts","sourceRoot":"","sources":["../src/useGridTapScroll.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAA4B,KAAK,wBAAwB,EAAE,MAAM,+BAA+B,CAAA;AAEvG,OAAO,KAAK,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAG5F;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,8BAA8B,OAAO,CAAA;AAalD,4MAA4M;AAC5M,MAAM,MAAM,sBAAsB,GAAG;IACjC,uIAAuI;IACvI,OAAO,EAAE,OAAO,CAAA;IAChB,mGAAmG;IACnG,WAAW,EAAE,MAAM,CAAA;IACnB,gIAAgI;IAChI,YAAY,EAAE,wBAAwB,CAAA;IACtC,6IAA6I;IAC7I,YAAY,EAAE,wBAAwB,CAAA;IACtC,sHAAsH;IACtH,UAAU,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;KAAE,CAAA;IAC1D,gDAAgD;IAChD,KAAK,EAAE,MAAM,MAAM,CAAA;IACnB,mJAAmJ;IACnJ,QAAQ,EAAE,MAAM,MAAM,CAAA;IACtB,oVAAoV;IACpV,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IAClC,8YAA8Y;IAC9Y,KAAK,EAAE,MAAM,MAAM,CAAA;IACnB,2HAA2H;IAC3H,QAAQ,EAAE,MAAM,MAAM,CAAA;IACtB,mLAAmL;IACnL,mBAAmB,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAA;IACzC,qKAAqK;IACrK,gBAAgB,EAAE,OAAO,CAAA;IACzB,4JAA4J;IAC5J,gBAAgB,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,+DAA+D;AAC/D,MAAM,MAAM,sBAAsB,GAAG;IACjC,4FAA4F;IAC5F,WAAW,EAAE,OAAO,CAAA;IACpB,qGAAqG;IACrG,yBAAyB,EAAE,CAAC,KAAK,EAAE,wBAAwB,KAAK,IAAI,CAAA;IACpE,4HAA4H;IAC5H,kBAAkB,EAAE,KAAK,CAAC,SAAS,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAA;CACpE,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,sBAAsB,KAAG,sBAuQjE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/virtualscroll",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "High-performance virtual scrolling component for React with variable item heights",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -107,8 +107,8 @@
107
107
  "license-check": "pnpm dlx license-checker --production --onlyAllow \"MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;Unlicense\"",
108
108
  "license-check:json": "pnpm dlx license-checker --production --onlyAllow \"MIT;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;Unlicense\" --json",
109
109
  "clean": "rimraf dist",
110
- "publish:patch": "pnpm run typecheck && pnpm run --if-present test && pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
111
- "publish:minor": "pnpm run typecheck && pnpm run --if-present test && pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
112
- "publish:major": "pnpm run typecheck && pnpm run --if-present test && pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks"
110
+ "publish:patch": "pnpm run typecheck && pnpm run --if-present test && node ../../.config/scripts/check-publish-leaks.mjs && pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
111
+ "publish:minor": "pnpm run typecheck && pnpm run --if-present test && node ../../.config/scripts/check-publish-leaks.mjs && pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
112
+ "publish:major": "pnpm run typecheck && pnpm run --if-present test && node ../../.config/scripts/check-publish-leaks.mjs && pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks"
113
113
  }
114
114
  }
package/src/ScrollBar.tsx CHANGED
@@ -8,6 +8,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
8
8
  import { twMerge } from "tailwind-merge"
9
9
  import { resolveVirtualScrollLabels, type VirtualScrollLabelOverrides, type VirtualScrollLocale } from "./labels.ts"
10
10
  import { Logger } from "./logger.ts"
11
+ import { capturePointer, releaseCapturedPointer } from "./pointerCapture.ts"
11
12
  import { TapScrollCircle, type TapScrollCircleDragState, type TapScrollCircleHandle, type TapScrollCircleRenderProps } from "./TapScrollCircle.tsx"
12
13
  import { getAxisScale, minmax } from "./utils.ts"
13
14
 
@@ -159,6 +160,40 @@ export type ScrollBarProps = {
159
160
  enableTrackClick?: boolean
160
161
  /** Whether arrow buttons control the scroll position. / 矢印ボタンによるスクロール操作を許可するかどうか。 */
161
162
  enableArrowButtons?: boolean
163
+ /**
164
+ * Whether the two arrow buttons are Tab stops (default `true`).
165
+ *
166
+ * `false` renders both with `tabIndex={-1}`, which takes them out of the sequential focus order
167
+ * and changes nothing else: pointer presses and press-and-hold repeat, the accessible names and
168
+ * Enter / Space on an arrow focused from script all keep working. The arrows are descendants of
169
+ * the `role="scrollbar"` root, whose children ARIA 1.2 makes presentational, so whether assistive
170
+ * technology lists them as buttons of their own is the browser's choice (Chromium does); this
171
+ * option changes nothing about that. Set it to `false` when the host already owns keyboard
172
+ * scrolling of this viewport (roving row focus with Arrow / Page / Home / End, a grid keyboard
173
+ * model): the arrows then duplicate that path and cost every keyboard user two extra Tab presses
174
+ * per bar — native scrollbars are never Tab stops either. The default stays `true` because
175
+ * without such a host model the arrows are the only scrolling control a keyboard user can reach
176
+ * with Tab: `ScrollPane` / `VirtualScroll` move their content by transform, so there is no native
177
+ * scroller the browser could make focusable, and their rows are not Tab stops. No effect while
178
+ * the arrows are disabled (`enableArrowButtons: false`, or nothing to scroll) — a disabled button
179
+ * is never focusable.
180
+ *
181
+ * 矢印ボタン 2 個を Tab の止まり先にするかどうか (既定 `true`)。
182
+ *
183
+ * `false` は両方を `tabIndex={-1}` で描画し、順次フォーカス移動の順序から外すだけの指定。
184
+ * ポインタ押下と長押しリピート、アクセシブルネーム、スクリプトからフォーカスした矢印での
185
+ * Enter / Space はすべて維持。矢印は `role="scrollbar"` のルートの子孫で、ARIA 1.2 はその子を
186
+ * presentational とするため、支援技術が矢印を独立したボタンとして示すかどうかはブラウザの選択
187
+ * (Chromium は示す) であり、本オプションはそこに関与しない。ホストがこのビューポートのキーボード
188
+ * スクロールを既に持つ場合 (行のロービングフォーカスと矢印 / Page / Home / End、グリッドのキーボードモデル) に
189
+ * `false` を指定 — 矢印はその経路の重複となり、キーボード利用者にバー 1 本あたり 2 回の余分な Tab を
190
+ * 課すため (ネイティブのスクロールバーも Tab の止まり先にならない)。既定を `true` に据え置く理由は、
191
+ * ホスト側のモデルが無ければ矢印がキーボード利用者の Tab で届く唯一のスクロール操作部品であること
192
+ * (`ScrollPane` / `VirtualScroll` はコンテンツを transform で動かすため、ブラウザがフォーカス可能に
193
+ * できるネイティブのスクロール領域が無く、行も Tab の止まり先ではない)。矢印が無効な間
194
+ * (`enableArrowButtons: false` またはスクロール不要) は効果なし — disabled のボタンはそもそもフォーカス不能。
195
+ */
196
+ enableArrowButtonTabStops?: boolean
162
197
  /** Whether the scrollbar is horizontal. / スクロールバーが水平かどうか。 */
163
198
  horizontal?: boolean
164
199
  /**
@@ -610,6 +645,7 @@ export const ScrollBar = ({
610
645
  enableThumbDrag = true,
611
646
  enableTrackClick = true,
612
647
  enableArrowButtons = true,
648
+ enableArrowButtonTabStops = true,
613
649
  horizontal = false,
614
650
  enableHorizontalTapCircle = false,
615
651
  stretchMainSize = false,
@@ -859,7 +895,7 @@ export const ScrollBar = ({
859
895
  */
860
896
  const stepAutoScroll = useCallback(
861
897
  (timestamp: number) => {
862
- // この rAF ループ意味論は useGridTapScroll (グリッド 2 軸ドライバ) と双子 — 片方を直したらもう片方も直すこと (ADR-27)
898
+ // この rAF ループ意味論は useGridTapScroll (グリッド 2 軸ドライバ) と双子 — 片方を直したらもう片方も直すこと
863
899
  const state = tapDragStateRef.current
864
900
  const metrics = latestMetricsRef.current
865
901
  if (!state.active || state.direction === 0) {
@@ -1136,10 +1172,10 @@ export const ScrollBar = ({
1136
1172
 
1137
1173
  /**
1138
1174
  * Pointer up handler for thumb dragging.
1139
- * Cleans up drag state and visual feedback.
1175
+ * Cleans up drag state and visual feedback, releasing the capture only when the press acquired one.
1140
1176
  *
1141
1177
  * つまみドラッグ用のポインタ上げハンドラー。
1142
- * ドラッグ状態と視覚的フィードバックをクリーンアップします。
1178
+ * ドラッグ状態と視覚的フィードバックをクリーンアップし、押下時にキャプチャを取得できていた場合だけ解放します。
1143
1179
  */
1144
1180
  const handleThumbPointerUp = useCallback(
1145
1181
  (event: PointerEvent) => {
@@ -1152,9 +1188,8 @@ export const ScrollBar = ({
1152
1188
  document.removeEventListener("pointerup", handleThumbPointerUp)
1153
1189
  document.removeEventListener("pointercancel", handleThumbPointerUp)
1154
1190
 
1155
- const captureTarget = state.captureTarget
1156
- if (captureTarget?.hasPointerCapture?.(event.pointerId)) {
1157
- captureTarget.releasePointerCapture(event.pointerId)
1191
+ if (state.captureTarget !== null) {
1192
+ releaseCapturedPointer(state.captureTarget, event.pointerId)
1158
1193
  }
1159
1194
 
1160
1195
  thumbDragStateRef.current = { pointerId: null, startThumbPosition: 0, startClientX: 0, startClientY: 0, scale: 1, captureTarget: null }
@@ -1227,8 +1262,10 @@ export const ScrollBar = ({
1227
1262
  // スクロールバーのつまみをポインタイベントで処理
1228
1263
  /**
1229
1264
  * Handles pointer down interaction on the thumb.
1265
+ * Captures the pointer when the element supports it; otherwise the drag continues on the document listeners alone.
1230
1266
  *
1231
1267
  * つまみ押下時のインタラクションを処理。
1268
+ * 要素がキャプチャに対応していれば取得し、取得できなければ document のリスナーだけでドラッグを続ける。
1232
1269
  */
1233
1270
  const handlePointerDownOnThumb = (event: React.PointerEvent<HTMLDivElement>) => {
1234
1271
  if (!scrollBarVisible) {
@@ -1247,18 +1284,10 @@ export const ScrollBar = ({
1247
1284
 
1248
1285
  // ポインターキャプチャを取得し、iframe/ウィンドウ外での pointerup 取りこぼしによる
1249
1286
  // ドラッグ固着を防ぐ。キャプチャ後もイベントは document までバブリングするため既存の
1250
- // document リスナはそのまま機能する。取得不能なポインターでは例外を握り潰す。
1287
+ // document リスナはそのまま機能する。取得できなかったときは解放対象を持たない。
1251
1288
  const element = event.currentTarget
1252
1289
  const scale = getMainAxisScale(element)
1253
- let captureTarget: HTMLElement | null = null
1254
- if (element.setPointerCapture) {
1255
- try {
1256
- element.setPointerCapture(event.pointerId)
1257
- captureTarget = element
1258
- } catch {
1259
- captureTarget = null
1260
- }
1261
- }
1290
+ const captureTarget = capturePointer(element, event.pointerId) === "captured" ? element : null
1262
1291
 
1263
1292
  thumbDragStateRef.current = {
1264
1293
  pointerId: event.pointerId,
@@ -1282,8 +1311,10 @@ export const ScrollBar = ({
1282
1311
 
1283
1312
  /**
1284
1313
  * Handles pointer down interaction on the track.
1314
+ * Jumps to the pressed position and captures the pointer when the element supports it.
1285
1315
  *
1286
1316
  * トラック押下時のインタラクションを処理。
1317
+ * 押下位置へ移動し、要素がキャプチャに対応していれば取得する。
1287
1318
  */
1288
1319
  const handlePointerDownOnTrack = (event: React.PointerEvent<HTMLDivElement>) => {
1289
1320
  if (!scrollBarVisible) {
@@ -1311,15 +1342,9 @@ export const ScrollBar = ({
1311
1342
  const initialPosition = translateToScrollPosition(startThumbPosition)
1312
1343
  resolveScrollRequest(initialPosition)
1313
1344
 
1314
- // サム側と同様、取得不能なポインター (非アクティブ pointerId 等) では例外を握り潰し、
1315
- // キャプチャなしのフォールバックでドラッグ状態の設定と preventDefault を継続する。
1316
- if (element.setPointerCapture) {
1317
- try {
1318
- element.setPointerCapture(event.pointerId)
1319
- } catch {
1320
- // キャプチャ取得失敗時はキャプチャなしで続行 (要素上の pointermove では追従する)。
1321
- }
1322
- }
1345
+ // 取得できなくても (非アクティブ pointerId・API 非対応) ドラッグ状態の設定と preventDefault は続ける。
1346
+ // キャプチャ無しでも要素上の pointermove では追従する。
1347
+ capturePointer(element, event.pointerId)
1323
1348
 
1324
1349
  trackDragStateRef.current = {
1325
1350
  pointerId: event.pointerId,
@@ -1374,19 +1399,17 @@ export const ScrollBar = ({
1374
1399
 
1375
1400
  /**
1376
1401
  * Handles pointer up interaction on the track.
1402
+ * Releases the capture when held and ends the track drag.
1377
1403
  *
1378
1404
  * トラックドラッグ終了時の処理。
1405
+ * キャプチャを保持していれば解放し、トラックドラッグを終える。
1379
1406
  */
1380
1407
  const handlePointerUpOnTrack = (event: React.PointerEvent<HTMLDivElement>) => {
1381
1408
  if (trackDragStateRef.current.pointerId !== event.pointerId) {
1382
1409
  return
1383
1410
  }
1384
1411
 
1385
- // hasPointerCapture は API 非実装環境 (jsdom 等) を考慮してサム側と同様に存在ガード付きで呼ぶ。
1386
- const element = event.currentTarget
1387
- if (element.hasPointerCapture?.(event.pointerId)) {
1388
- element.releasePointerCapture(event.pointerId)
1389
- }
1412
+ releaseCapturedPointer(event.currentTarget, event.pointerId)
1390
1413
 
1391
1414
  resetTrackDragState()
1392
1415
 
@@ -1396,19 +1419,17 @@ export const ScrollBar = ({
1396
1419
 
1397
1420
  /**
1398
1421
  * Handles pointer cancel interaction on the track.
1422
+ * Releases the capture when held and discards the track drag.
1399
1423
  *
1400
1424
  * トラックドラッグキャンセル時の処理。
1425
+ * キャプチャを保持していれば解放し、トラックドラッグを破棄する。
1401
1426
  */
1402
1427
  const handlePointerCancelOnTrack = (event: React.PointerEvent<HTMLDivElement>) => {
1403
1428
  if (trackDragStateRef.current.pointerId !== event.pointerId) {
1404
1429
  return
1405
1430
  }
1406
1431
 
1407
- // hasPointerCapture は API 非実装環境 (jsdom 等) を考慮してサム側と同様に存在ガード付きで呼ぶ。
1408
- const element = event.currentTarget
1409
- if (element.hasPointerCapture?.(event.pointerId)) {
1410
- element.releasePointerCapture(event.pointerId)
1411
- }
1432
+ releaseCapturedPointer(event.currentTarget, event.pointerId)
1412
1433
 
1413
1434
  resetTrackDragState()
1414
1435
  }
@@ -1450,18 +1471,25 @@ export const ScrollBar = ({
1450
1471
  }, [tapCircleOffsetX, tapCircleOffsetY, tapCircleSize])
1451
1472
 
1452
1473
  /**
1453
- * Renders an arrow button for scrollbar controls.
1454
- * The buttons stay in the tab order (tabIndex=0) so keyboard users can reach
1455
- * the Enter/Space handlers, which are the only keyboard scrolling entry point.
1474
+ * Renders one arrow button of the bar. It is a Tab stop (`tabIndex=0`) unless the host opted out
1475
+ * with `enableArrowButtonTabStops={false}` (`tabIndex=-1`); either way it stays focusable from
1476
+ * script, pointer-operable and named, and Enter / Space scrolls one step while it has focus.
1477
+ *
1478
+ * バーの矢印ボタン 1 個の描画。ホストが `enableArrowButtonTabStops={false}` で外さない限り Tab の
1479
+ * 止まり先 (`tabIndex=0`)、外した場合は `tabIndex=-1`。いずれの場合もスクリプトからのフォーカス・
1480
+ * ポインタ操作・アクセシブルネームは維持し、フォーカス中の Enter / Space で 1 ステップ移動。
1456
1481
  *
1457
- * スクロールバー制御用矢印ボタンを描画。キーボードで Enter/Space ハンドラへ
1458
- * 到達できるよう tabIndex=0 でタブ順序に残す。
1482
+ * @param direction - Step direction (-1 = toward the start, 1 = toward the end) / ステップ方向 (-1 = 始端側、1 = 終端側)
1483
+ * @param label - Accessible name / アクセシブルネーム
1484
+ * @param icon - Decorative glyph, hidden from assistive technology / 支援技術から隠す装飾グリフ
1485
+ * @param key - React key / React キー
1486
+ * @returns The arrow button element / 矢印ボタン要素
1459
1487
  */
1460
1488
  const renderArrowButton = (direction: 1 | -1, label: string, icon: string, key: string) => (
1461
1489
  <button
1462
1490
  key={key}
1463
1491
  type="button"
1464
- tabIndex={0}
1492
+ tabIndex={enableArrowButtonTabStops ? 0 : -1}
1465
1493
  className="aqvs-scrollbar-arrow-button"
1466
1494
  style={{
1467
1495
  [mainSizeKey]: scrollBarWidth,