@aiquants/virtualscroll 3.9.2 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +149 -0
- package/README.md +135 -40
- package/dist/ScrollBar.d.cts +45 -38
- package/dist/ScrollBar.d.ts +45 -38
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.cts +12 -14
- package/dist/ScrollPane.d.ts +12 -14
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +6 -6
- package/dist/VirtualGrid.d.ts +6 -6
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +184 -30
- package/dist/VirtualScroll.d.ts +184 -30
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/cli.js +26 -47
- package/dist/index.cjs +1 -1
- package/dist/index.js +2385 -2413
- package/dist/labels.d.cts +38 -7
- package/dist/labels.d.ts +38 -7
- package/dist/labels.d.ts.map +1 -1
- package/dist/logger.d.cts +1 -16
- package/dist/logger.d.ts +1 -16
- package/dist/logger.d.ts.map +1 -1
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/dist/useFenwickMapTree.d.cts +21 -1
- package/dist/useFenwickMapTree.d.ts +21 -1
- package/dist/useFenwickMapTree.d.ts.map +1 -1
- package/dist/useLruCache.d.ts.map +1 -1
- package/dist/utils.d.cts +14 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/ScrollBar.tsx +115 -55
- package/src/ScrollPane.tsx +12 -14
- package/src/VirtualGrid.tsx +27 -25
- package/src/VirtualScroll.tsx +486 -234
- package/src/labels.ts +58 -7
- package/src/logger.ts +1 -25
- package/src/styles/virtualscroll.css +51 -3
- package/src/useFenwickMapTree.ts +60 -98
- package/src/useLruCache.ts +14 -8
- package/src/utils.ts +15 -0
package/src/ScrollBar.tsx
CHANGED
|
@@ -7,11 +7,11 @@ import type { CSSProperties, ReactNode } from "react"
|
|
|
7
7
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
8
8
|
import { twMerge } from "tailwind-merge"
|
|
9
9
|
import { snapToDevicePixelGrid, usePaintingDevicePixelRatio } from "./devicePixelGrid.ts"
|
|
10
|
-
import { resolveVirtualScrollLabels, type VirtualScrollLabelOverrides, type VirtualScrollLocale } from "./labels.ts"
|
|
10
|
+
import { resolveVirtualScrollLabels, resolveVirtualScrollLocale, type VirtualScrollLabelOverrides, type VirtualScrollLabels, type VirtualScrollLocale } from "./labels.ts"
|
|
11
11
|
import { Logger } from "./logger.ts"
|
|
12
12
|
import { capturePointer, releaseCapturedPointer } from "./pointerCapture.ts"
|
|
13
13
|
import { TapScrollCircle, type TapScrollCircleDragState, type TapScrollCircleHandle, type TapScrollCircleRenderProps } from "./TapScrollCircle.tsx"
|
|
14
|
-
import { getAxisScale, minmax } from "./utils.ts"
|
|
14
|
+
import { getAxisScale, keepFocusOnPress, minmax } from "./utils.ts"
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Configuration for scrollbar orientation (vertical or horizontal).
|
|
@@ -40,6 +40,13 @@ type ArrowAutoRepeatOptions = {
|
|
|
40
40
|
canUseArrowButtons: boolean
|
|
41
41
|
/** Whether arrow buttons are enabled in configuration / 設定で矢印ボタンが有効化されているか */
|
|
42
42
|
enableArrowButtons: boolean
|
|
43
|
+
/**
|
|
44
|
+
* Whether a focused arrow takes Enter / Space: `false` under the pointer-only signal (`enableArrowButtonTabStops: false`),
|
|
45
|
+
* where the bar is hidden from assistive technology and its arrows answer the pointer only, even when a script focuses one.
|
|
46
|
+
* フォーカスした矢印が Enter / Space を受けるか。ポインタ専用の合図 (`enableArrowButtonTabStops: false`) の下では `false` で、
|
|
47
|
+
* バーは支援技術から隠れ、矢印はスクリプトがフォーカスしてもポインタにだけ応える。
|
|
48
|
+
*/
|
|
49
|
+
keyboardOperable: boolean
|
|
43
50
|
/** Function to reset tap scroll state / タップスクロール状態をリセットする関数 */
|
|
44
51
|
resetTapScroll: () => void
|
|
45
52
|
/** Function to scroll by a step / ステップ単位でスクロールする関数 */
|
|
@@ -154,8 +161,8 @@ export type ScrollBarProps = {
|
|
|
154
161
|
viewportSize: number
|
|
155
162
|
/** The current scroll position. / 現在のスクロール位置。 */
|
|
156
163
|
scrollPosition: number
|
|
157
|
-
/** A callback function invoked to adjust scroll position. Accepts next position or updater. /
|
|
158
|
-
onScroll?: (scrollPosition: number | ((prevPosition: number) => number), prevPosition
|
|
164
|
+
/** A callback function invoked to adjust scroll position. Accepts next position or updater, and always receives the position the bar resolved the request from. / スクロール位置を調整するためのコールバック。次位置または更新関数と、バーが要求を解いた元の位置 (必ず渡す) を受け取る。 */
|
|
165
|
+
onScroll?: (scrollPosition: number | ((prevPosition: number) => number), prevPosition: number) => number | undefined
|
|
159
166
|
/** Whether grabbing the scrollbar thumb is allowed. / スクロールバーのつまみ操作を許可するかどうか。 */
|
|
160
167
|
enableThumbDrag?: boolean
|
|
161
168
|
/** Whether clicking on the track moves the scrollbar. / スクロールバーのトラッククリック操作を許可するかどうか。 */
|
|
@@ -163,37 +170,41 @@ export type ScrollBarProps = {
|
|
|
163
170
|
/** Whether arrow buttons control the scroll position. / 矢印ボタンによるスクロール操作を許可するかどうか。 */
|
|
164
171
|
enableArrowButtons?: boolean
|
|
165
172
|
/**
|
|
166
|
-
* Whether the
|
|
173
|
+
* Whether the bar is the keyboard user's scrolling control (default `true`), or a pointer-only control because the host
|
|
174
|
+
* scrolls this viewport by keyboard itself (`false`).
|
|
175
|
+
*
|
|
176
|
+
* `true`: the bar is exposed to assistive technology as a named `role="scrollbar"` (`labels.verticalScrollBar` /
|
|
177
|
+
* `labels.horizontalScrollBar`) holding a named `role="slider"` thumb (`labels.verticalScrollThumb` /
|
|
178
|
+
* `labels.horizontalScrollThumb`), and its two arrow buttons are Tab stops: `ScrollPane` / `VirtualScroll` move their
|
|
179
|
+
* content by transform, so there is no native scroller the browser could make focusable, and their rows are not Tab
|
|
180
|
+
* stops — without a host keyboard model the arrows are the only scrolling control Tab reaches. Focus an arrow and
|
|
181
|
+
* Enter / Space scrolls one step.
|
|
182
|
+
*
|
|
183
|
+
* `false` declares that the host owns keyboard scrolling of this viewport (roving row focus with Arrow / Page / Home /
|
|
184
|
+
* End, a grid keyboard model), so the bar only duplicates that path. The bar then becomes what a native scrollbar is:
|
|
185
|
+
* a pointer-only control. The bar carries `aria-hidden="true"`; its arrows get `tabIndex={-1}`, and the bar root and
|
|
186
|
+
* the thumb wrapper are not focusable; a press anywhere on the bar cancels its default action, so it never moves
|
|
187
|
+
* focus — neither onto a part of the bar (an arrow, also a disabled one) nor away from where the host put it — while
|
|
188
|
+
* the press itself still reaches the part. Pointer scrolling is unchanged: the arrows (with press-and-hold repeat),
|
|
189
|
+
* the track, the thumb and the tap circle work exactly as with `true`. The same signal reaches the scroll-to-edge pills
|
|
190
|
+
* of `VirtualScroll` (`VirtualScrollScrollBarOptions["enableArrowButtonTabStops"]`).
|
|
167
191
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* Enter / Space on an arrow focused from script all keep working. The arrows are descendants of
|
|
171
|
-
* the `role="scrollbar"` root, whose children ARIA 1.2 makes presentational, so whether assistive
|
|
172
|
-
* technology lists them as buttons of their own is the browser's choice (Chromium does); this
|
|
173
|
-
* option changes nothing about that. Set it to `false` when the host already owns keyboard
|
|
174
|
-
* scrolling of this viewport (roving row focus with Arrow / Page / Home / End, a grid keyboard
|
|
175
|
-
* model): the arrows then duplicate that path and cost every keyboard user two extra Tab presses
|
|
176
|
-
* per bar — native scrollbars are never Tab stops either. The default stays `true` because
|
|
177
|
-
* without such a host model the arrows are the only scrolling control a keyboard user can reach
|
|
178
|
-
* with Tab: `ScrollPane` / `VirtualScroll` move their content by transform, so there is no native
|
|
179
|
-
* scroller the browser could make focusable, and their rows are not Tab stops. No effect while
|
|
180
|
-
* the arrows are disabled (`enableArrowButtons: false`, or nothing to scroll) — a disabled button
|
|
181
|
-
* is never focusable.
|
|
192
|
+
* バーがキーボード利用者のスクロール操作部品か (既定 `true`)、ホストがこのビューポートのキーボードスクロールを自分で
|
|
193
|
+
* 持つためポインタ専用の部品か (`false`) の指定。
|
|
182
194
|
*
|
|
183
|
-
*
|
|
195
|
+
* `true`: バーは名前付きの `role="scrollbar"` (`labels.verticalScrollBar` / `labels.horizontalScrollBar`) として
|
|
196
|
+
* 支援技術へ見え、名前付きの `role="slider"` のつまみ (`labels.verticalScrollThumb` / `labels.horizontalScrollThumb`) を
|
|
197
|
+
* 持ち、矢印ボタン 2 個は Tab の止まり先。`ScrollPane` / `VirtualScroll` はコンテンツを transform で動かすため、
|
|
198
|
+
* ブラウザがフォーカス可能にできるネイティブのスクロール領域が無く、行も Tab の止まり先ではない — ホストのキーボード
|
|
199
|
+
* モデルが無ければ、矢印が Tab で届く唯一のスクロール操作部品。矢印にフォーカスして Enter / Space で 1 ステップ移動。
|
|
184
200
|
*
|
|
185
|
-
* `false`
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* (
|
|
190
|
-
*
|
|
191
|
-
* `
|
|
192
|
-
* 課すため (ネイティブのスクロールバーも Tab の止まり先にならない)。既定を `true` に据え置く理由は、
|
|
193
|
-
* ホスト側のモデルが無ければ矢印がキーボード利用者の Tab で届く唯一のスクロール操作部品であること
|
|
194
|
-
* (`ScrollPane` / `VirtualScroll` はコンテンツを transform で動かすため、ブラウザがフォーカス可能に
|
|
195
|
-
* できるネイティブのスクロール領域が無く、行も Tab の止まり先ではない)。矢印が無効な間
|
|
196
|
-
* (`enableArrowButtons: false` またはスクロール不要) は効果なし — disabled のボタンはそもそもフォーカス不能。
|
|
201
|
+
* `false` は、ホストがこのビューポートのキーボードスクロールを持つ (行のロービングフォーカスと矢印 / Page / Home / End、
|
|
202
|
+
* グリッドのキーボードモデル) ことの宣言で、バーはその経路の重複にすぎない。バーはネイティブのスクロールバーと同じく
|
|
203
|
+
* ポインタ専用の部品になる。バーは `aria-hidden="true"` を持ち、矢印は `tabIndex={-1}`、バーのルートとつまみの器は
|
|
204
|
+
* フォーカス不能。バーのどこを押しても押下の既定動作を取り消すので、押下はフォーカスを動かさない — バーの部品
|
|
205
|
+
* (矢印。無効の矢印も) へも、ホストが置いた場所の外へも — が、押下そのものは部品へ届く。ポインタのスクロールは
|
|
206
|
+
* 変わらない: 矢印 (長押しの連続を含む)・トラック・つまみ・タップのサークルは `true` と同じに動く。同じ合図は
|
|
207
|
+
* `VirtualScroll` の端へ戻るピルにも届く (`VirtualScrollScrollBarOptions["enableArrowButtonTabStops"]`)。
|
|
197
208
|
*/
|
|
198
209
|
enableArrowButtonTabStops?: boolean
|
|
199
210
|
/** Whether the scrollbar is horizontal. / スクロールバーが水平かどうか。 */
|
|
@@ -231,18 +242,21 @@ export type ScrollBarProps = {
|
|
|
231
242
|
/** The index of the last visible item. / 最後の可視アイテムのインデックス。 */
|
|
232
243
|
visibleEndIndex?: number
|
|
233
244
|
/**
|
|
234
|
-
* UI chrome locale of the built-in
|
|
235
|
-
* throws a RangeError at render; no
|
|
236
|
-
*
|
|
237
|
-
*
|
|
245
|
+
* UI chrome locale of the built-in accessible names of the bar, its thumb and its arrows, and of the percentage the bar
|
|
246
|
+
* and the thumb announce as their value text (default `"en"`). An unsupported value throws a RangeError at render; no
|
|
247
|
+
* language negotiation happens.
|
|
248
|
+
* バー・つまみ・矢印の内蔵アクセシブルネームと、バーとつまみが値の文字として知らせる百分率の UI クロームロケール (既定
|
|
249
|
+
* `"en"`)。非対応値は描画時に RangeError、言語ネゴシエーションなし。
|
|
238
250
|
*/
|
|
239
251
|
locale?: VirtualScrollLocale
|
|
240
252
|
/**
|
|
241
|
-
* Per-key overrides laid over the catalog of `locale`.
|
|
242
|
-
*
|
|
253
|
+
* Per-key overrides laid over the catalog of `locale`. A vertical bar reads `verticalScrollBar`,
|
|
254
|
+
* `verticalScrollThumb`, `scrollUp` and `scrollDown`; a horizontal bar reads `horizontalScrollBar`,
|
|
255
|
+
* `horizontalScrollThumb`, `scrollLeft` and `scrollRight`. Unknown keys and blank values throw a
|
|
243
256
|
* RangeError at render.
|
|
244
|
-
* `locale`
|
|
245
|
-
*
|
|
257
|
+
* `locale` のカタログへ重ねるキー単位の上書き。縦のバーが読むのは `verticalScrollBar`・`verticalScrollThumb`・
|
|
258
|
+
* `scrollUp`・`scrollDown`、横のバーは `horizontalScrollBar`・`horizontalScrollThumb`・`scrollLeft`・`scrollRight`。
|
|
259
|
+
* 未知キーと空白値は描画時に RangeError。
|
|
246
260
|
*/
|
|
247
261
|
labels?: VirtualScrollLabelOverrides
|
|
248
262
|
}
|
|
@@ -294,6 +308,33 @@ export const DEFAULT_TAP_SCROLL_CIRCLE_OPTIONS: ResolvedTapScrollCircleOptions =
|
|
|
294
308
|
maxSpeedCurve: undefined,
|
|
295
309
|
}
|
|
296
310
|
|
|
311
|
+
/**
|
|
312
|
+
* The accessible names one bar renders: the bar (`role="scrollbar"`), its thumb (`role="slider"`) and its two arrows
|
|
313
|
+
* (start, end).
|
|
314
|
+
*
|
|
315
|
+
* バー 1 本が描くアクセシブルネーム (バー `role="scrollbar"`・つまみ `role="slider"`・始端と終端の矢印)。
|
|
316
|
+
*/
|
|
317
|
+
type ScrollBarNames = {
|
|
318
|
+
/** Name of the bar / バーの名前 */
|
|
319
|
+
readonly bar: string
|
|
320
|
+
/** Name of the thumb / つまみの名前 */
|
|
321
|
+
readonly thumb: string
|
|
322
|
+
/** Names of the start and the end arrow / 始端と終端の矢印の名前 */
|
|
323
|
+
readonly arrows: readonly [string, string]
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Picks the names of one bar from the resolved labels by its orientation.
|
|
328
|
+
*
|
|
329
|
+
* 解決済みのラベルから、向きに応じてバー 1 本の名前を選ぶ処理。
|
|
330
|
+
*
|
|
331
|
+
* @param labels - Resolved labels / 解決済みのラベル
|
|
332
|
+
* @param horizontal - Whether the bar is horizontal / 横のバーか
|
|
333
|
+
* @returns The bar's names / バーの名前
|
|
334
|
+
*/
|
|
335
|
+
const pickScrollBarNames = (labels: VirtualScrollLabels, horizontal: boolean): ScrollBarNames =>
|
|
336
|
+
horizontal ? { bar: labels.horizontalScrollBar, thumb: labels.horizontalScrollThumb, arrows: [labels.scrollLeft, labels.scrollRight] } : { bar: labels.verticalScrollBar, thumb: labels.verticalScrollThumb, arrows: [labels.scrollUp, labels.scrollDown] }
|
|
337
|
+
|
|
297
338
|
/**
|
|
298
339
|
* Creates the orientation dependent configuration for the scrollbar.
|
|
299
340
|
*
|
|
@@ -374,7 +415,7 @@ const useThumbVisualFeedback = ({ isDragging, isThumbHovered, enableThumbDrag }:
|
|
|
374
415
|
*
|
|
375
416
|
* スクロールバー矢印の自動リピート操作向けハンドラーを提供。
|
|
376
417
|
*/
|
|
377
|
-
const useArrowAutoRepeat = ({ canUseArrowButtons, enableArrowButtons, resetTapScroll, scrollByStep }: ArrowAutoRepeatOptions): ArrowAutoRepeatHandlers => {
|
|
418
|
+
const useArrowAutoRepeat = ({ canUseArrowButtons, enableArrowButtons, keyboardOperable, resetTapScroll, scrollByStep }: ArrowAutoRepeatOptions): ArrowAutoRepeatHandlers => {
|
|
378
419
|
const arrowHoldIntervalRef = useRef<number | null>(null)
|
|
379
420
|
const arrowHoldTimeoutRef = useRef<number | null>(null)
|
|
380
421
|
// 長押し中に張るグローバル解除リスナ (disabled 化やウィンドウ blur でボタンに pointerup が届かない保険)。
|
|
@@ -430,7 +471,9 @@ const useArrowAutoRepeat = ({ canUseArrowButtons, enableArrowButtons, resetTapSc
|
|
|
430
471
|
|
|
431
472
|
const handleArrowKeyDown = useCallback(
|
|
432
473
|
(direction: 1 | -1) => (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
|
433
|
-
|
|
474
|
+
// ポインタ専用のバーは支援技術から隠れていて、キーボードのスクロールはホストが持つ。スクリプトがフォーカスした矢印でも
|
|
475
|
+
// キーを受けると、隠れた部品がホストのキー操作と重なって一覧を動かす
|
|
476
|
+
if (!(enableArrowButtons && keyboardOperable)) {
|
|
434
477
|
return
|
|
435
478
|
}
|
|
436
479
|
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
@@ -438,7 +481,7 @@ const useArrowAutoRepeat = ({ canUseArrowButtons, enableArrowButtons, resetTapSc
|
|
|
438
481
|
scrollByStep(direction)
|
|
439
482
|
}
|
|
440
483
|
},
|
|
441
|
-
[enableArrowButtons, scrollByStep],
|
|
484
|
+
[enableArrowButtons, keyboardOperable, scrollByStep],
|
|
442
485
|
)
|
|
443
486
|
|
|
444
487
|
// 矢印ボタンが操作不能 (canUseArrowButtons=false) になったら進行中のリピートを即停止する。
|
|
@@ -695,7 +738,11 @@ export const ScrollBar = ({
|
|
|
695
738
|
const resolvedTapScrollOptions = useMemo<ResolvedTapScrollCircleOptions>(() => resolveTapScrollCircleOptions(tapScrollCircleOptions, itemCount), [itemCount, tapScrollCircleOptions])
|
|
696
739
|
const orientationConfig = useMemo(() => createOrientationConfig(horizontal), [horizontal])
|
|
697
740
|
const resolvedLabels = useMemo(() => resolveVirtualScrollLabels(locale, labels), [locale, labels])
|
|
698
|
-
const
|
|
741
|
+
const names = pickScrollBarNames(resolvedLabels, horizontal)
|
|
742
|
+
// 値の文字はスクロールの割合。px の値 (aria-valuenow) は長い一覧で数千万になり、読み上げても位置が分からない
|
|
743
|
+
const valueTextFormat = useMemo(() => new Intl.NumberFormat(resolveVirtualScrollLocale(locale), { style: "percent", maximumFractionDigits: 0 }), [locale])
|
|
744
|
+
// ホストがキーボードのスクロールを持つと、バーはその経路の重複になる。支援技術にもキーボードにも同じ操作を 2 度見せない
|
|
745
|
+
const pointerOnly = !enableArrowButtonTabStops
|
|
699
746
|
const {
|
|
700
747
|
enabled: tapCircleEnabled,
|
|
701
748
|
size: tapCircleSize,
|
|
@@ -733,6 +780,9 @@ export const ScrollBar = ({
|
|
|
733
780
|
// 負値が aria-valuemax へ露出して ARIA 違反 (valuemax < valuemin) になる。644 行の
|
|
734
781
|
// getScrollBarMetrics と同一のクランプ規約)
|
|
735
782
|
const maxScrollPosition = Math.max(contentSize - viewportSize, 0)
|
|
783
|
+
// ポインタ専用のバーは支援技術から隠れるので値の文字を持たない。スクロールできないバーは 0 %、中身が縮んで一時的に最大位置を
|
|
784
|
+
// 越えた位置は 100 %
|
|
785
|
+
const valueText = pointerOnly ? undefined : valueTextFormat.format(maxScrollPosition > 0 ? minmax(scrollPosition / maxScrollPosition, 0, 1) : 0)
|
|
736
786
|
const effectiveTrackLength = Math.max(trackLength - thumbSize, 0)
|
|
737
787
|
// scrollPosition が範囲外 (コンテンツ縮小直後の未クランプ 1 フレームや iOS バウンス値) でも
|
|
738
788
|
// サムがトラック外へはみ出さないよう、レンダー時に [0, effectiveTrackLength] へクランプする。
|
|
@@ -1268,6 +1318,7 @@ export const ScrollBar = ({
|
|
|
1268
1318
|
const { handleArrowPointerDown, handleArrowPointerUp, handleArrowKeyDown } = useArrowAutoRepeat({
|
|
1269
1319
|
canUseArrowButtons,
|
|
1270
1320
|
enableArrowButtons,
|
|
1321
|
+
keyboardOperable: !pointerOnly,
|
|
1271
1322
|
resetTapScroll,
|
|
1272
1323
|
scrollByStep,
|
|
1273
1324
|
})
|
|
@@ -1484,13 +1535,14 @@ export const ScrollBar = ({
|
|
|
1484
1535
|
}, [tapCircleOffsetX, tapCircleOffsetY, tapCircleSize])
|
|
1485
1536
|
|
|
1486
1537
|
/**
|
|
1487
|
-
* Renders one arrow button of the bar
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1538
|
+
* Renders one arrow button of the bar: a named Tab stop (`tabIndex=0`) on which Enter / Space scrolls one step, or,
|
|
1539
|
+
* on a pointer-only bar (`enableArrowButtonTabStops={false}`), a pointer target out of the Tab order (`tabIndex=-1`)
|
|
1540
|
+
* inside the bar's `aria-hidden` subtree, whose presses never move focus. Pointer presses and press-and-hold repeat
|
|
1541
|
+
* work the same either way.
|
|
1490
1542
|
*
|
|
1491
|
-
* バーの矢印ボタン 1
|
|
1492
|
-
*
|
|
1493
|
-
*
|
|
1543
|
+
* バーの矢印ボタン 1 個の描画。名前付きの Tab の止まり先 (`tabIndex=0`) で Enter / Space により 1 ステップ移動するか、
|
|
1544
|
+
* ポインタ専用のバー (`enableArrowButtonTabStops={false}`) では Tab 順の外のポインタの的 (`tabIndex=-1`) で、
|
|
1545
|
+
* バーの `aria-hidden` の部分木の中にあり押下はフォーカスを動かさない。ポインタの押下と長押しの連続はどちらも同じ。
|
|
1494
1546
|
*
|
|
1495
1547
|
* @param direction - Step direction (-1 = toward the start, 1 = toward the end) / ステップ方向 (-1 = 始端側、1 = 終端側)
|
|
1496
1548
|
* @param label - Accessible name / アクセシブルネーム
|
|
@@ -1502,7 +1554,7 @@ export const ScrollBar = ({
|
|
|
1502
1554
|
<button
|
|
1503
1555
|
key={key}
|
|
1504
1556
|
type="button"
|
|
1505
|
-
tabIndex={
|
|
1557
|
+
tabIndex={pointerOnly ? -1 : 0}
|
|
1506
1558
|
className="aqvs-scrollbar-arrow-button"
|
|
1507
1559
|
style={{
|
|
1508
1560
|
[mainSizeKey]: scrollBarWidth,
|
|
@@ -1552,11 +1604,17 @@ export const ScrollBar = ({
|
|
|
1552
1604
|
// components-only アーティファクトには同梱されず、ホストのビルド内容任せになる
|
|
1553
1605
|
data-visible={scrollBarVisible ? "true" : "false"}
|
|
1554
1606
|
role="scrollbar"
|
|
1555
|
-
|
|
1607
|
+
aria-label={names.bar}
|
|
1608
|
+
aria-hidden={pointerOnly ? true : undefined}
|
|
1609
|
+
// ポインタ専用のバーはフォーカスを受ける部品を持たない。押下の既定動作も取り消す: Chromium は無効の矢印の押下でも
|
|
1610
|
+
// pointerdown を届けたうえで、最も近いフォーカス可能な祖先へフォーカスを移す (取り消すとどこへも移さない)
|
|
1611
|
+
tabIndex={pointerOnly ? undefined : -1}
|
|
1612
|
+
onPointerDown={pointerOnly ? keepFocusOnPress : undefined}
|
|
1556
1613
|
aria-controls={ariaControls}
|
|
1557
1614
|
aria-valuenow={scrollPosition}
|
|
1558
1615
|
aria-valuemin={0}
|
|
1559
1616
|
aria-valuemax={maxScrollPosition}
|
|
1617
|
+
aria-valuetext={valueText}
|
|
1560
1618
|
aria-orientation={horizontal ? "horizontal" : "vertical"}>
|
|
1561
1619
|
{(!horizontal || enableHorizontalTapCircle) && scrollBarVisible && tapCircleEnabled && (
|
|
1562
1620
|
<TapScrollCircle
|
|
@@ -1572,7 +1630,7 @@ export const ScrollBar = ({
|
|
|
1572
1630
|
onDragChange={handleTapCircleDragChange}
|
|
1573
1631
|
/>
|
|
1574
1632
|
)}
|
|
1575
|
-
{renderArrowButton(-1,
|
|
1633
|
+
{renderArrowButton(-1, names.arrows[0], arrowIcons[0], "arrow-start")}
|
|
1576
1634
|
<div
|
|
1577
1635
|
key="track"
|
|
1578
1636
|
className="aqvs-scrollbar-track"
|
|
@@ -1607,12 +1665,14 @@ export const ScrollBar = ({
|
|
|
1607
1665
|
data-visible={scrollBarVisible || isDragging ? "true" : "false"}
|
|
1608
1666
|
onPointerDown={handlePointerDownOnThumb}
|
|
1609
1667
|
role="slider"
|
|
1668
|
+
aria-label={names.thumb}
|
|
1610
1669
|
aria-orientation={horizontal ? "horizontal" : "vertical"}
|
|
1611
1670
|
aria-valuenow={scrollPosition}
|
|
1612
1671
|
aria-valuemin={0}
|
|
1613
1672
|
aria-valuemax={maxScrollPosition}
|
|
1673
|
+
aria-valuetext={valueText}
|
|
1614
1674
|
aria-disabled={!enableThumbDrag}
|
|
1615
|
-
tabIndex={-1}>
|
|
1675
|
+
tabIndex={pointerOnly ? undefined : -1}>
|
|
1616
1676
|
{/* スクロールバーのつまみ(可視部分) */}
|
|
1617
1677
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: スクロールバーのつまみは必要なインタラクションです */}
|
|
1618
1678
|
<div
|
|
@@ -1639,7 +1699,7 @@ export const ScrollBar = ({
|
|
|
1639
1699
|
/>
|
|
1640
1700
|
</div>
|
|
1641
1701
|
</div>
|
|
1642
|
-
{renderArrowButton(1,
|
|
1702
|
+
{renderArrowButton(1, names.arrows[1], arrowIcons[1], "arrow-end")}
|
|
1643
1703
|
</div>
|
|
1644
1704
|
)
|
|
1645
1705
|
}
|
package/src/ScrollPane.tsx
CHANGED
|
@@ -80,18 +80,16 @@ export type ScrollPaneProps = {
|
|
|
80
80
|
/** Whether arrow buttons control the scroll position. / 矢印ボタンのスクロール操作を許可するかどうか。 */
|
|
81
81
|
enableArrowButtons?: boolean
|
|
82
82
|
/**
|
|
83
|
-
* Whether the
|
|
84
|
-
* pane
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* 既定が `true` なのは、それ以外ではペインで Tab の届く唯一のスクロール操作部品だから。契約の全文は
|
|
94
|
-
* `ScrollBarProps["enableArrowButtonTabStops"]`。
|
|
83
|
+
* Whether the pane's scrollbar is the keyboard user's scrolling control (default `true`) or a pointer-only control
|
|
84
|
+
* because the host scrolls this pane by keyboard itself (`false`), forwarded to the pane's ScrollBar. With `false`
|
|
85
|
+
* the bar carries `aria-hidden="true"`, has no Tab stop and no focusable part, and a press on it never moves focus;
|
|
86
|
+
* pointer scrolling is unchanged. The default stays `true` because otherwise the arrows are the pane's only scrolling
|
|
87
|
+
* control that Tab reaches. Full contract: `ScrollBarProps["enableArrowButtonTabStops"]`.
|
|
88
|
+
* ペインのスクロールバーがキーボード利用者のスクロール操作部品か (既定 `true`)、ホストがこのペインのキーボード
|
|
89
|
+
* スクロールを自分で持つためポインタ専用の部品か (`false`) の指定。ペインの ScrollBar へ転送。`false` ではバーが
|
|
90
|
+
* `aria-hidden="true"` を持ち、Tab の止まり先もフォーカス可能な部品も無く、押下はフォーカスを動かさない。ポインタの
|
|
91
|
+
* スクロールは変わらない。既定が `true` なのは、それ以外ではペインで Tab の届く唯一のスクロール操作部品だから。
|
|
92
|
+
* 契約の全文は `ScrollBarProps["enableArrowButtonTabStops"]`。
|
|
95
93
|
*/
|
|
96
94
|
enableArrowButtonTabStops?: boolean
|
|
97
95
|
/** Whether dragging the content area scrolls the pane. / コンテンツ領域のドラッグでスクロールさせるかどうか。 */
|
|
@@ -185,9 +183,9 @@ export type ScrollPaneProps = {
|
|
|
185
183
|
*/
|
|
186
184
|
contentProps?: React.AriaAttributes & { id?: string; role?: React.AriaRole }
|
|
187
185
|
/**
|
|
188
|
-
* UI chrome locale forwarded to the pane's ScrollBar (
|
|
186
|
+
* UI chrome locale forwarded to the pane's ScrollBar (the names of the bar, its thumb and its arrows; default `"en"`).
|
|
189
187
|
* An unsupported value throws a RangeError at render.
|
|
190
|
-
* ペインの ScrollBar へ転送する UI クロームロケール (
|
|
188
|
+
* ペインの ScrollBar へ転送する UI クロームロケール (バー・つまみ・矢印の名前、既定 `"en"`)。
|
|
191
189
|
* 非対応値は描画時に RangeError。
|
|
192
190
|
*/
|
|
193
191
|
locale?: VirtualScrollLocale
|
package/src/VirtualGrid.tsx
CHANGED
|
@@ -229,7 +229,7 @@ export type VirtualGridRange = {
|
|
|
229
229
|
totalHeight: number
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
/** Live-region options: the consumer owns announcement wording/locale; the built-in catalog (`locale` / `labels`) covers only the
|
|
232
|
+
/** Live-region options: the consumer owns announcement wording/locale; the built-in catalog (`locale` / `labels`) covers only the eleven chrome strings (the scrollbar names, pills, empty state) — row-parity. / liveRegion オプション: 読み上げの文言とロケールは消費側所有。内蔵カタログ (`locale` / `labels`) はクローム 11 文言 (スクロールバーの名前・ピル・空状態) のみ対象 — 行側と同一方針。 */
|
|
233
233
|
export type VirtualGridLiveRegionOptions = {
|
|
234
234
|
/** Builds the announcement (same string = no re-announce, "" clears). / 読み上げ文言の組み立て (同一文字列 = 再読み上げなし、"" でクリア)。 */
|
|
235
235
|
buildMessage: (range: VirtualGridRange) => string
|
|
@@ -419,11 +419,11 @@ export type VirtualGridProps<T> = {
|
|
|
419
419
|
scrollBarOptions?: VirtualScrollScrollBarOptions
|
|
420
420
|
liveRegion?: VirtualGridLiveRegionOptions
|
|
421
421
|
/**
|
|
422
|
-
* UI chrome locale (default `"en"`), forwarded to the embedded vertical VirtualScroll (its
|
|
423
|
-
*
|
|
424
|
-
* arrows. An unsupported value throws a RangeError at render.
|
|
425
|
-
* UI クロームロケール (既定 `"en"`)。内蔵の縦 VirtualScroll (
|
|
426
|
-
* 空状態)
|
|
422
|
+
* UI chrome locale (default `"en"`), forwarded to the embedded vertical VirtualScroll (its bar's
|
|
423
|
+
* names and the 0-row / all-frozen / degenerate-band empty state) and the horizontal bar (the
|
|
424
|
+
* names of the bar, its thumb and its arrows). An unsupported value throws a RangeError at render.
|
|
425
|
+
* UI クロームロケール (既定 `"en"`)。内蔵の縦 VirtualScroll (バーの名前と 0 行・全行固定・退化帯の
|
|
426
|
+
* 空状態) と横バー (バー・つまみ・矢印の名前) へ転送。非対応値は描画時に RangeError。
|
|
427
427
|
*/
|
|
428
428
|
locale?: VirtualScrollLocale
|
|
429
429
|
/**
|
|
@@ -605,9 +605,7 @@ const EMPTY_PLACED_COLUMNS: PlacedColumn[] = []
|
|
|
605
605
|
const collectVisibleColumns = (startCol: number, endCol: number, getColWidth: (col: number) => number, colTree: ReturnType<typeof useFenwickMapTree>, maxColumns: number): { columns: PlacedColumn[]; widthUpdates: Array<{ index: number; value: number }>; truncated: number } => {
|
|
606
606
|
const columns: PlacedColumn[] = []
|
|
607
607
|
const widthUpdates: Array<{ index: number; value: number }> = []
|
|
608
|
-
|
|
609
|
-
return { columns, widthUpdates, truncated: 0 }
|
|
610
|
-
}
|
|
608
|
+
// 呼び出し元は空窓 (start > end) を先に除くので、ここへ来る窓は 1 列以上
|
|
611
609
|
const startPrefix = colTree.prefixSum(startCol, { materializeOption: { materialize: false } })
|
|
612
610
|
// 窓先頭の絶対 left から幅を積む O(k) 走査 (列ごとの prefixSum O(k·log n) を回避 — 行 memo と同型)
|
|
613
611
|
let runningLeft = startPrefix.cumulative - startPrefix.currentValue
|
|
@@ -850,8 +848,8 @@ const VirtualGridInner = <T,>(
|
|
|
850
848
|
const colTree = useFenwickMapTree(colCount, validatedGetColWidth, colTreeOptions)
|
|
851
849
|
|
|
852
850
|
// ---- 幅エポック (木の内容バージョン): updateColSize / self-heal 適用で繰上げ、窓キー不変でも
|
|
853
|
-
// 配置 memo
|
|
854
|
-
//
|
|
851
|
+
// 配置 memo と列窓再計算を失効させる。総幅は相殺バッチリサイズ (総和不変) で変わらないため
|
|
852
|
+
// 合図にならない (行側は VirtualScroll の木の版数が同じ役目を持つ) ----
|
|
855
853
|
const [widthEpoch, setWidthEpoch] = useState(0)
|
|
856
854
|
|
|
857
855
|
// ---- 凍結帯 (設計 §8) — 形状検証は fail-fast、colCount 超過だけは動的クランプ (シート
|
|
@@ -982,7 +980,7 @@ const VirtualGridInner = <T,>(
|
|
|
982
980
|
/** Anchor the DOM currently shows (updated post-commit in the layout effect). / DOM が現在表示中のアンカー (commit 後の layout effect で更新)。 */
|
|
983
981
|
const committedAnchorRef = useRef(0)
|
|
984
982
|
|
|
985
|
-
const [totalWidth, setTotalWidth] = useState(() => colTree.getTotal()
|
|
983
|
+
const [totalWidth, setTotalWidth] = useState(() => colTree.getTotal())
|
|
986
984
|
const totalWidthRef = useRef(totalWidth)
|
|
987
985
|
totalWidthRef.current = totalWidth
|
|
988
986
|
/** Mirrors the tree total into state+ref immediately (same tick — drag loops clamp fresh). / 木総幅を state + ref へ即時反映 (同 tick — ドラッグループが新鮮にクランプ)。 */
|
|
@@ -1715,11 +1713,14 @@ const VirtualGridInner = <T,>(
|
|
|
1715
1713
|
// ---- liveRegion (2D — 文言は消費側所有、静定デバウンス) ----
|
|
1716
1714
|
const [liveMessage, setLiveMessage] = useState("")
|
|
1717
1715
|
const liveTimerRef = useRef<number | null>(null)
|
|
1716
|
+
// 埋め込みの一覧はアンマウントで待っていた範囲の通知を同期で配る (取りこぼさないため)。それはグリッド自身の後始末の後に
|
|
1717
|
+
// 届くので、アンマウントの後は読み上げを予約しない (予約すると消費側の buildMessage がアンマウントの後に走る)
|
|
1718
|
+
const isLiveRegionClosedRef = useRef(false)
|
|
1718
1719
|
const liveRegionRef = useRef(liveRegion)
|
|
1719
1720
|
liveRegionRef.current = liveRegion
|
|
1720
1721
|
const scheduleLiveMessage = useCallback(() => {
|
|
1721
1722
|
const options = liveRegionRef.current
|
|
1722
|
-
if (options === undefined) {
|
|
1723
|
+
if (options === undefined || isLiveRegionClosedRef.current) {
|
|
1723
1724
|
return
|
|
1724
1725
|
}
|
|
1725
1726
|
// 不正な debounceMs は黙って既定へ読み替えず読み上げを無効化する (警告は下の effect が 1 回だけ出す)
|
|
@@ -1734,10 +1735,8 @@ const VirtualGridInner = <T,>(
|
|
|
1734
1735
|
liveTimerRef.current = null
|
|
1735
1736
|
const range = buildRange()
|
|
1736
1737
|
if (range !== null) {
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
return next === prev ? prev : next
|
|
1740
|
-
})
|
|
1738
|
+
// 同じ文言の再設定は React が Object.is で捨てる (再描画しない) ので、比べずに書く (行側のライブリージョンと同じ)
|
|
1739
|
+
setLiveMessage(options.buildMessage(range))
|
|
1741
1740
|
}
|
|
1742
1741
|
}, debounce)
|
|
1743
1742
|
}, [buildRange])
|
|
@@ -1750,14 +1749,16 @@ const VirtualGridInner = <T,>(
|
|
|
1750
1749
|
Logger.warn(`[VirtualGrid] liveRegion.debounceMs must be a finite number >= 0, received ${liveDebounceMs}. Announcements are disabled.`)
|
|
1751
1750
|
}
|
|
1752
1751
|
}, [hasLiveRegion, liveDebounceValid, liveDebounceMs])
|
|
1753
|
-
useEffect(
|
|
1754
|
-
|
|
1752
|
+
useEffect(() => {
|
|
1753
|
+
// StrictMode の二重マウント (後始末の後に再び effect) でも読み上げを再開できるよう、本体で開き直す
|
|
1754
|
+
isLiveRegionClosedRef.current = false
|
|
1755
|
+
return () => {
|
|
1756
|
+
isLiveRegionClosedRef.current = true
|
|
1755
1757
|
if (liveTimerRef.current !== null) {
|
|
1756
1758
|
window.clearTimeout(liveTimerRef.current)
|
|
1757
1759
|
}
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
)
|
|
1760
|
+
}
|
|
1761
|
+
}, [])
|
|
1761
1762
|
useEffect(() => {
|
|
1762
1763
|
scheduleLiveMessage()
|
|
1763
1764
|
}, [columnWindow, scheduleLiveMessage])
|
|
@@ -1849,9 +1850,10 @@ const VirtualGridInner = <T,>(
|
|
|
1849
1850
|
if (found.cumulative === bandStart) {
|
|
1850
1851
|
return { row: rowAnchor.index + effectiveFrozenRows, col: minmax(found.index + 1, 0, colCount - 1), offsetX: 0, offsetY: rowAnchor.offsetPx }
|
|
1851
1852
|
}
|
|
1853
|
+
// 見つけた列の左端は木から読み直す (探索の結果の型は列が無い場合の undefined を含むため)
|
|
1854
|
+
const column = colTree.prefixSum(found.index, { materializeOption: { materialize: false } })
|
|
1852
1855
|
// 行はシフト空間 → フル行空間へ +R 翻訳 (行凍結設計 §3 — 復元側は −R で往復恒等)
|
|
1853
|
-
|
|
1854
|
-
return { row: rowAnchor.index + effectiveFrozenRows, col: found.index, offsetX: Math.max(0, bandStart - colLeft), offsetY: rowAnchor.offsetPx }
|
|
1856
|
+
return { row: rowAnchor.index + effectiveFrozenRows, col: found.index, offsetX: Math.max(0, bandStart - (column.cumulative - column.currentValue)), offsetY: rowAnchor.offsetPx }
|
|
1855
1857
|
},
|
|
1856
1858
|
getViewportSize: () => ({ width: viewportRef.current.width, height: viewportRef.current.height }),
|
|
1857
1859
|
// 縦はインセット込みの行側 getContentSize を単一情報源にする (range.totalHeight は木総高のみ)。
|
|
@@ -2003,7 +2005,7 @@ const VirtualGridInner = <T,>(
|
|
|
2003
2005
|
onScroll={(request, previous) => {
|
|
2004
2006
|
// バー操作は手動横入力 — 列アンカーの張りを解除する
|
|
2005
2007
|
pendingColAnchorRef.current = null
|
|
2006
|
-
return applyHxRef.current(typeof request === "function" ? request(previous
|
|
2008
|
+
return applyHxRef.current(typeof request === "function" ? request(previous) : request)
|
|
2007
2009
|
}}
|
|
2008
2010
|
// バー固有の設定は縦バー (内包 VirtualScroll へ scrollBarOptions を全量透過) と同じ値を
|
|
2009
2011
|
// 横バーへも渡す — 片方のバーにだけ効く設定面を作らない
|