@aiquants/virtualscroll 3.6.1 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +103 -11
- package/dist/ScrollBar.d.cts +17 -1
- package/dist/ScrollBar.d.ts +17 -1
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.cts +15 -0
- package/dist/ScrollPane.d.ts +15 -0
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/TapScrollCircle.d.ts.map +1 -1
- package/dist/VirtualGrid.d.cts +17 -1
- package/dist/VirtualGrid.d.ts +17 -1
- package/dist/VirtualGrid.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +24 -5
- package/dist/VirtualScroll.d.ts +24 -5
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2007 -1967
- package/dist/labels.d.cts +125 -0
- package/dist/labels.d.ts +126 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/pointerCapture.d.cts +68 -0
- package/dist/pointerCapture.d.ts +69 -0
- package/dist/pointerCapture.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ScrollBar.tsx +41 -38
- package/src/ScrollPane.tsx +64 -18
- package/src/TapScrollCircle.tsx +35 -25
- package/src/VirtualGrid.tsx +23 -1
- package/src/VirtualScroll.tsx +39 -12
- package/src/index.ts +1 -0
- package/src/labels.ts +236 -0
- package/src/pointerCapture.ts +89 -0
package/src/ScrollPane.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
2
2
|
import { twMerge } from "tailwind-merge"
|
|
3
|
+
import type { VirtualScrollLabelOverrides, VirtualScrollLocale } from "./labels.ts"
|
|
3
4
|
import { Logger } from "./logger.ts"
|
|
5
|
+
import { capturePointer, isPointerCaptured, releaseCapturedPointer } from "./pointerCapture.ts"
|
|
4
6
|
import { ScrollBar, type ScrollBarTapCircleOptions, type ScrollBarThumbOverlayRenderProps, TAP_SCROLL_CANCEL_EVENT } from "./ScrollBar.tsx"
|
|
5
7
|
import { getAxisScale, minmax } from "./utils.ts"
|
|
6
8
|
import { resolveWheelAxes } from "./wheelAxes.ts"
|
|
@@ -167,6 +169,20 @@ export type ScrollPaneProps = {
|
|
|
167
169
|
* `className` / `style` / `ref` を表現できない型にしてあり、この口からレイアウトを壊せない。
|
|
168
170
|
*/
|
|
169
171
|
contentProps?: React.AriaAttributes & { id?: string; role?: React.AriaRole }
|
|
172
|
+
/**
|
|
173
|
+
* UI chrome locale forwarded to the pane's ScrollBar (arrow aria-labels; default `"en"`).
|
|
174
|
+
* An unsupported value throws a RangeError at render.
|
|
175
|
+
* ペインの ScrollBar へ転送する UI クロームロケール (矢印の aria-label、既定 `"en"`)。
|
|
176
|
+
* 非対応値は描画時に RangeError。
|
|
177
|
+
*/
|
|
178
|
+
locale?: VirtualScrollLocale
|
|
179
|
+
/**
|
|
180
|
+
* Per-key label overrides forwarded to the pane's ScrollBar, laid over the catalog of `locale`.
|
|
181
|
+
* Unknown keys and blank values throw a RangeError at render.
|
|
182
|
+
* ペインの ScrollBar へ転送するキー単位のラベル上書き (`locale` のカタログへ重ねる)。
|
|
183
|
+
* 未知キーと空白値は描画時に RangeError。
|
|
184
|
+
*/
|
|
185
|
+
labels?: VirtualScrollLabelOverrides
|
|
170
186
|
}
|
|
171
187
|
|
|
172
188
|
export type ScrollPaneInertiaOptions = {
|
|
@@ -332,6 +348,8 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
332
348
|
renderOverlay,
|
|
333
349
|
initialScrollPosition = 0,
|
|
334
350
|
contentProps,
|
|
351
|
+
locale,
|
|
352
|
+
labels,
|
|
335
353
|
},
|
|
336
354
|
ref,
|
|
337
355
|
) => {
|
|
@@ -1116,11 +1134,22 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1116
1134
|
state.velocitySamples = []
|
|
1117
1135
|
state.scale = 1
|
|
1118
1136
|
// 状態を確定させてから解放する (同期 lostpointercapture に引き継ぎを壊させない)
|
|
1119
|
-
if (element
|
|
1120
|
-
element
|
|
1137
|
+
if (element !== null) {
|
|
1138
|
+
releaseCapturedPointer(element, releasedPointerId)
|
|
1121
1139
|
}
|
|
1122
1140
|
}, [enablePointerDrag, pointerDragInputs, isScrollable])
|
|
1123
1141
|
|
|
1142
|
+
/**
|
|
1143
|
+
* Wires the pointer drag-scroll of the content area and tears it down on unmount.
|
|
1144
|
+
* コンテンツ領域のポインタドラッグスクロールを結線し、アンマウント時に解除する処理。
|
|
1145
|
+
*
|
|
1146
|
+
* キャプチャの取得・解放・保持確認はすべて `pointerCapture.ts` の能力判定を通す。
|
|
1147
|
+
* Pointer Capture API の無い環境 (jsdom 等) でもドラッグは `window` のリスナーで追従し、例外は投げない。
|
|
1148
|
+
*
|
|
1149
|
+
* 目的: ドラッグ・慣性・click 抑止のリスナーをコンテンツ要素と `window` へ登録する。
|
|
1150
|
+
* 依存関係: [id]
|
|
1151
|
+
* クリーンアップ: リスナーを外し、保持中のキャプチャを解放してドラッグ状態を初期化する。
|
|
1152
|
+
*/
|
|
1124
1153
|
useEffect(() => {
|
|
1125
1154
|
const element = contentAreaRef.current
|
|
1126
1155
|
if (!element) {
|
|
@@ -1169,6 +1198,16 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1169
1198
|
state.shouldCancelNextClick = false
|
|
1170
1199
|
}
|
|
1171
1200
|
|
|
1201
|
+
/**
|
|
1202
|
+
* Confirms the drag once the activation threshold is crossed and moves the capture onto the content area.
|
|
1203
|
+
* 発動閾値を跨いだ時点でドラッグを確定し、キャプチャをコンテンツ領域へ移す処理。
|
|
1204
|
+
*
|
|
1205
|
+
* キャプチャを取れない場合 (非アクティブなポインタ・Pointer Capture API の非対応環境) も
|
|
1206
|
+
* ドラッグは確定させる。追従は `window` の `pointermove` が受け持つため、キャプチャが無くても止まらない。
|
|
1207
|
+
*
|
|
1208
|
+
* @param event The pointer move that crossed the threshold. 閾値を跨いだ pointermove。
|
|
1209
|
+
* @returns Nothing. なし。
|
|
1210
|
+
*/
|
|
1172
1211
|
const startDragging = (event: PointerEvent) => {
|
|
1173
1212
|
const state = dragStateRef.current
|
|
1174
1213
|
if (state.isDragging) {
|
|
@@ -1176,16 +1215,13 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1176
1215
|
}
|
|
1177
1216
|
state.isDragging = true
|
|
1178
1217
|
state.shouldCancelNextClick = true
|
|
1179
|
-
if (!element
|
|
1218
|
+
if (!isPointerCaptured(element, event.pointerId)) {
|
|
1180
1219
|
// 注意: キャプチャは pointerdown 時ではなくドラッグ確定時に取得する。
|
|
1181
1220
|
// pointerdown で即キャプチャすると click がキャプチャ要素へリターゲットされ、
|
|
1182
1221
|
// ペイン内の子要素 (行など) の click ハンドラが一切発火しなくなる (Chromium 実測)。
|
|
1183
1222
|
// ドラッグ確定後は shouldCancelNextClick で click を意図的に抑止するため副作用がない。
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
} catch {
|
|
1187
|
-
// ポインタが既に非アクティブだと NotFoundError になり得る。キャプチャ無しでもドラッグは継続可能。
|
|
1188
|
-
}
|
|
1223
|
+
// 取得に失敗してもキャプチャ無しでドラッグは継続できるため、結果は見ない。
|
|
1224
|
+
capturePointer(element, event.pointerId)
|
|
1189
1225
|
}
|
|
1190
1226
|
pushVelocitySample(event.clientY)
|
|
1191
1227
|
}
|
|
@@ -1227,13 +1263,16 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1227
1263
|
*/
|
|
1228
1264
|
const isAuthoritativeForDrag = (event: PointerEvent) => event.isTrusted || event.target === element
|
|
1229
1265
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1266
|
+
/**
|
|
1267
|
+
* Shared teardown for cases where the drag can no longer continue without receiving a pointerup.
|
|
1268
|
+
* pointerup を受け取れないままドラッグ継続が不可能になったときの共通終了処理。
|
|
1269
|
+
*
|
|
1270
|
+
* @param pointerId The pointer whose drag is aborted. ドラッグを打ち切るポインタ ID。
|
|
1271
|
+
* @returns Nothing. なし。
|
|
1272
|
+
*/
|
|
1232
1273
|
const abortDrag = (pointerId: number) => {
|
|
1233
1274
|
const state = dragStateRef.current
|
|
1234
|
-
|
|
1235
|
-
element.releasePointerCapture(pointerId)
|
|
1236
|
-
}
|
|
1275
|
+
releaseCapturedPointer(element, pointerId)
|
|
1237
1276
|
state.shouldCancelNextClick = false
|
|
1238
1277
|
clearClickTimer()
|
|
1239
1278
|
resetDragState()
|
|
@@ -1320,6 +1359,13 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1320
1359
|
return true
|
|
1321
1360
|
}
|
|
1322
1361
|
|
|
1362
|
+
/**
|
|
1363
|
+
* Ends the drag on pointerup, arms the click suppression and hands the fling velocity to inertia.
|
|
1364
|
+
* pointerup でドラッグを終え、click 抑止を張り、フリック速度を慣性へ渡す処理。
|
|
1365
|
+
*
|
|
1366
|
+
* @param event The terminal pointerup. 終端の pointerup。
|
|
1367
|
+
* @returns Nothing. なし。
|
|
1368
|
+
*/
|
|
1323
1369
|
const handlePointerUp = (event: PointerEvent) => {
|
|
1324
1370
|
const state = dragStateRef.current
|
|
1325
1371
|
if (state.pointerId !== event.pointerId) {
|
|
@@ -1377,9 +1423,7 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1377
1423
|
// handleLostPointerCapture は pointerId 不一致で無視されるため、abortDrag による
|
|
1378
1424
|
// shouldCancelNextClick / clickResetTimer の破棄 (click 抑止の喪失) が起きない。
|
|
1379
1425
|
resetDragState()
|
|
1380
|
-
|
|
1381
|
-
element.releasePointerCapture(event.pointerId)
|
|
1382
|
-
}
|
|
1426
|
+
releaseCapturedPointer(element, event.pointerId)
|
|
1383
1427
|
|
|
1384
1428
|
if (Math.abs(inertiaVelocity) >= threshold) {
|
|
1385
1429
|
startInertiaRef.current?.(inertiaVelocity)
|
|
@@ -1541,8 +1585,8 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1541
1585
|
toggleListeners(element, elementEntries, "remove")
|
|
1542
1586
|
toggleListeners(window, windowEntries, "remove")
|
|
1543
1587
|
const state = dragStateRef.current
|
|
1544
|
-
if (state.pointerId !== null
|
|
1545
|
-
element
|
|
1588
|
+
if (state.pointerId !== null) {
|
|
1589
|
+
releaseCapturedPointer(element, state.pointerId)
|
|
1546
1590
|
}
|
|
1547
1591
|
clearClickTimer()
|
|
1548
1592
|
resetDragState()
|
|
@@ -1638,6 +1682,8 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
|
|
|
1638
1682
|
renderThumbOverlay={renderThumbOverlay}
|
|
1639
1683
|
visibleStartIndex={visibleStartIndex}
|
|
1640
1684
|
visibleEndIndex={visibleEndIndex}
|
|
1685
|
+
locale={locale}
|
|
1686
|
+
labels={labels}
|
|
1641
1687
|
/>
|
|
1642
1688
|
{renderOverlay?.()}
|
|
1643
1689
|
</div>
|
package/src/TapScrollCircle.tsx
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { CSSProperties, ReactNode, PointerEvent as ReactPointerEvent } from "react"
|
|
7
7
|
import { forwardRef, memo, useCallback, useImperativeHandle, useRef, useState } from "react"
|
|
8
8
|
import { twMerge } from "tailwind-merge"
|
|
9
|
+
import { capturePointer, releaseCapturedPointer } from "./pointerCapture.ts"
|
|
9
10
|
|
|
10
11
|
export type TapScrollCircleDragState = {
|
|
11
12
|
/** Whether the pointer is actively dragging. / ポインターがドラッグ中かどうか。 */
|
|
@@ -205,13 +206,20 @@ export const TapScrollCircle = memo(
|
|
|
205
206
|
[applyDragState, axis],
|
|
206
207
|
)
|
|
207
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Releases the root's capture of the pointer when it holds one.
|
|
211
|
+
* ルートが指定ポインタのキャプチャを保持していれば解放する処理。
|
|
212
|
+
*
|
|
213
|
+
* @param pointerId The pointer to release, or null when no pointer is tracked. 解放するポインタ ID。追跡中のポインタが無ければ null。
|
|
214
|
+
* @returns Nothing. なし。
|
|
215
|
+
*/
|
|
208
216
|
const releasePointerCapture = useCallback((pointerId: number | null) => {
|
|
209
217
|
if (pointerId === null) {
|
|
210
218
|
return
|
|
211
219
|
}
|
|
212
220
|
const element = rootRef.current
|
|
213
|
-
if (element
|
|
214
|
-
element
|
|
221
|
+
if (element !== null) {
|
|
222
|
+
releaseCapturedPointer(element, pointerId)
|
|
215
223
|
}
|
|
216
224
|
}, [])
|
|
217
225
|
|
|
@@ -228,6 +236,15 @@ export const TapScrollCircle = memo(
|
|
|
228
236
|
[applyDragState, releasePointerCapture],
|
|
229
237
|
)
|
|
230
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Starts a circle drag, or records a second pointer as the hand-off candidate during a drag.
|
|
241
|
+
* サークルのドラッグを開始する処理。ドラッグ中に押された別ポインタはハンドオフ候補として記録する。
|
|
242
|
+
*
|
|
243
|
+
* キャプチャを取得できなくても (非アクティブなポインタ・Pointer Capture API の非対応環境) ドラッグは開始する。
|
|
244
|
+
*
|
|
245
|
+
* @param event The pointerdown on the circle. サークル上の pointerdown。
|
|
246
|
+
* @returns Nothing. なし。
|
|
247
|
+
*/
|
|
231
248
|
const handlePointerDown = useCallback(
|
|
232
249
|
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
233
250
|
// ドラッグ中に発生した別ポインター (2 本目のタッチ) は待機候補として記録し、
|
|
@@ -246,14 +263,9 @@ export const TapScrollCircle = memo(
|
|
|
246
263
|
const { left, top, width, height } = element.getBoundingClientRect()
|
|
247
264
|
centerRef.current = { x: left + width / 2, y: top + height / 2 }
|
|
248
265
|
pointerIdRef.current = event.pointerId
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// ポインタが既に非アクティブ (ウインドウ外へ出た等) だと setPointerCapture は
|
|
253
|
-
// NotFoundError を投げ得る。キャプチャ無しでもドラッグ処理は継続できるため握り潰す。
|
|
254
|
-
// setPointerCapture can throw NotFoundError if the pointer is already inactive
|
|
255
|
-
// (e.g. moved outside the browser window); dragging still works without capture.
|
|
256
|
-
}
|
|
266
|
+
// ポインタが既に非アクティブ (ウインドウ外へ出た等) でも API 非対応でも、
|
|
267
|
+
// キャプチャ無しでドラッグ処理は継続できるため結果は見ない。
|
|
268
|
+
capturePointer(element, event.pointerId)
|
|
257
269
|
updateDragState(event.clientX, event.clientY, true)
|
|
258
270
|
},
|
|
259
271
|
[updateDragState],
|
|
@@ -274,6 +286,16 @@ export const TapScrollCircle = memo(
|
|
|
274
286
|
[updateDragState],
|
|
275
287
|
)
|
|
276
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Ends the circle drag on pointerup, handing control to a live pending pointer when one exists.
|
|
291
|
+
* pointerup でサークルのドラッグを終える処理。生きている待機候補ポインタがあれば制御を移譲する。
|
|
292
|
+
*
|
|
293
|
+
* 候補の生死は `capturePointer` の結果で判定する。`"inactive-pointer"` だけを死とみなし、
|
|
294
|
+
* API 非対応・それ以外の拒否は生死不明として移譲を続ける。
|
|
295
|
+
*
|
|
296
|
+
* @param event The pointerup or pointercancel on the circle. サークル上の pointerup / pointercancel。
|
|
297
|
+
* @returns Nothing. なし。
|
|
298
|
+
*/
|
|
277
299
|
const handlePointerUp = useCallback(
|
|
278
300
|
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
279
301
|
if (pointerIdRef.current !== event.pointerId) {
|
|
@@ -296,21 +318,9 @@ export const TapScrollCircle = memo(
|
|
|
296
318
|
// pointers, so a dead candidate resets the state instead of getting promoted.
|
|
297
319
|
releasePointerCapture(event.pointerId)
|
|
298
320
|
const element = rootRef.current
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
element.setPointerCapture(pending.pointerId)
|
|
303
|
-
} catch (error) {
|
|
304
|
-
// NotFoundError = 候補ポインタが既に非アクティブ (サークル外で解放済み等)。
|
|
305
|
-
// クロスレルム (iframe への portal 等) では DOMException の instanceof が
|
|
306
|
-
// false になるため、name ベースで判定する (DOMException も name を持つので包含)。
|
|
307
|
-
// それ以外 (キャプチャ未実装環境等) は生死不明のため移譲を継続する。
|
|
308
|
-
const errorName = typeof error === "object" && error !== null && "name" in error ? (error as { name?: unknown }).name : undefined
|
|
309
|
-
if (errorName === "NotFoundError") {
|
|
310
|
-
candidateAlive = false
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
321
|
+
// NotFoundError (= サークル外で解放済み等) だけが死の証拠。API 非対応やそれ以外の拒否は
|
|
322
|
+
// 生死不明のため移譲を継続する (候補を捨てると 2 本目の指が効かなくなる)。
|
|
323
|
+
const candidateAlive = element === null || capturePointer(element, pending.pointerId) !== "inactive-pointer"
|
|
314
324
|
if (candidateAlive) {
|
|
315
325
|
pointerIdRef.current = pending.pointerId
|
|
316
326
|
updateDragState(pending.clientX, pending.clientY, true)
|
package/src/VirtualGrid.tsx
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import { type CSSProperties, forwardRef, type HTMLAttributes, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
25
25
|
import { twMerge } from "tailwind-merge"
|
|
26
26
|
import type { TapScrollAxisSpeedParams } from "./computeTapScrollVelocity.ts"
|
|
27
|
+
import type { VirtualScrollLabelOverrides, VirtualScrollLocale } from "./labels.ts"
|
|
27
28
|
import { Logger } from "./logger.ts"
|
|
28
29
|
import { resolveTapScrollCircleOptions, ScrollBar, type ScrollBarTapCircleOptions } from "./ScrollBar.tsx"
|
|
29
30
|
import type { ScrollPaneProps } from "./ScrollPane.tsx"
|
|
@@ -216,7 +217,7 @@ export type VirtualGridRange = {
|
|
|
216
217
|
totalHeight: number
|
|
217
218
|
}
|
|
218
219
|
|
|
219
|
-
/** Live-region options: the consumer owns wording/locale
|
|
220
|
+
/** Live-region options: the consumer owns announcement wording/locale; the built-in catalog (`locale` / `labels`) covers only the seven chrome strings (arrows, pills, empty state) — row-parity. / liveRegion オプション: 読み上げの文言とロケールは消費側所有。内蔵カタログ (`locale` / `labels`) はクローム 7 文言 (矢印・ピル・空状態) のみ対象 — 行側と同一方針。 */
|
|
220
221
|
export type VirtualGridLiveRegionOptions = {
|
|
221
222
|
/** Builds the announcement (same string = no re-announce, "" clears). / 読み上げ文言の組み立て (同一文字列 = 再読み上げなし、"" でクリア)。 */
|
|
222
223
|
buildMessage: (range: VirtualGridRange) => string
|
|
@@ -392,6 +393,21 @@ export type VirtualGridProps<T> = {
|
|
|
392
393
|
behaviorOptions?: VirtualGridBehaviorOptions
|
|
393
394
|
scrollBarOptions?: VirtualScrollScrollBarOptions
|
|
394
395
|
liveRegion?: VirtualGridLiveRegionOptions
|
|
396
|
+
/**
|
|
397
|
+
* UI chrome locale (default `"en"`), forwarded to the embedded vertical VirtualScroll (its
|
|
398
|
+
* arrows and the 0-row / all-frozen / degenerate-band empty state) and the horizontal bar
|
|
399
|
+
* arrows. An unsupported value throws a RangeError at render.
|
|
400
|
+
* UI クロームロケール (既定 `"en"`)。内蔵の縦 VirtualScroll (矢印と 0 行・全行固定・退化帯の
|
|
401
|
+
* 空状態) と横バーの矢印へ転送。非対応値は描画時に RangeError。
|
|
402
|
+
*/
|
|
403
|
+
locale?: VirtualScrollLocale
|
|
404
|
+
/**
|
|
405
|
+
* Per-key label overrides laid over the catalog of `locale`, forwarded to the same two
|
|
406
|
+
* surfaces as `locale`. Unknown keys and blank values throw a RangeError at render.
|
|
407
|
+
* `locale` のカタログへ重ねるキー単位のラベル上書き (`locale` と同じ 2 面へ転送)。
|
|
408
|
+
* 未知キーと空白値は描画時に RangeError。
|
|
409
|
+
*/
|
|
410
|
+
labels?: VirtualScrollLabelOverrides
|
|
395
411
|
className?: string
|
|
396
412
|
testId?: string
|
|
397
413
|
}
|
|
@@ -749,6 +765,8 @@ const VirtualGridInner = <T,>(
|
|
|
749
765
|
behaviorOptions,
|
|
750
766
|
scrollBarOptions,
|
|
751
767
|
liveRegion,
|
|
768
|
+
locale,
|
|
769
|
+
labels,
|
|
752
770
|
className,
|
|
753
771
|
testId,
|
|
754
772
|
}: VirtualGridProps<T>,
|
|
@@ -1910,6 +1928,8 @@ const VirtualGridInner = <T,>(
|
|
|
1910
1928
|
onRangeChange={handleRangeChangeWithLive}
|
|
1911
1929
|
onWheelHorizontal={consumeHorizontalDelta}
|
|
1912
1930
|
onPanHorizontal={consumeHorizontalDelta}
|
|
1931
|
+
locale={locale}
|
|
1932
|
+
labels={labels}
|
|
1913
1933
|
testId={testId ? `${testId}-rows` : undefined}>
|
|
1914
1934
|
{renderRow}
|
|
1915
1935
|
</VirtualScroll>
|
|
@@ -1951,6 +1971,8 @@ const VirtualGridInner = <T,>(
|
|
|
1951
1971
|
return applyHxRef.current(typeof request === "function" ? request(previous ?? hxRef.current) : request)
|
|
1952
1972
|
}}
|
|
1953
1973
|
scrollBarWidth={scrollBarWidth}
|
|
1974
|
+
locale={locale}
|
|
1975
|
+
labels={labels}
|
|
1954
1976
|
/>
|
|
1955
1977
|
</div>
|
|
1956
1978
|
{tapCircleEnabled && gridScrollable && (
|
package/src/VirtualScroll.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { forwardRef, type ReactNode, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import { resolveVirtualScrollLabels, type VirtualScrollLabelOverrides, type VirtualScrollLocale } from "./labels.ts"
|
|
2
3
|
import { Logger } from "./logger.ts"
|
|
3
4
|
import { ScrollPane, type ScrollPaneContentInsets, type ScrollPaneHandle, type ScrollPaneProps } from "./ScrollPane.tsx"
|
|
4
5
|
import { useFenwickMapTree } from "./useFenwickMapTree.ts"
|
|
@@ -224,12 +225,13 @@ export type VirtualScrollLiveRegionOptions = {
|
|
|
224
225
|
/**
|
|
225
226
|
* Formats the announcement text. Called after the visible range settles; returning the same
|
|
226
227
|
* string as last time leaves the DOM untouched (no re-announcement), returning `""` clears
|
|
227
|
-
* the region. The
|
|
228
|
-
*
|
|
228
|
+
* the region. The live-region wording and its language are consumer-owned: the package's
|
|
229
|
+
* built-in catalog (`locale` / `labels`) covers only the seven chrome strings (scrollbar arrow
|
|
230
|
+
* aria-labels, the scroll-to-edge pills and the empty state), never announcements.
|
|
229
231
|
* 読み上げ文言を組み立てる。可視範囲が静定した後に呼ばれ、前回と同じ文字列なら DOM を
|
|
230
|
-
* 触らない (再読み上げしない)。`""`
|
|
231
|
-
*
|
|
232
|
-
*
|
|
232
|
+
* 触らない (再読み上げしない)。`""` を返すとリージョンを空にする。読み上げの文面と言語は
|
|
233
|
+
* 消費側の所有物 — パッケージの内蔵カタログ (`locale` / `labels`) が扱うのはクローム 7 文言
|
|
234
|
+
* (スクロールバー矢印の aria-label、端スクロールピル、空状態) だけで、読み上げは対象外。
|
|
233
235
|
*
|
|
234
236
|
* @param range - The settled visible range / 静定した可視範囲
|
|
235
237
|
* @returns Announcement text / 読み上げ文言
|
|
@@ -310,6 +312,23 @@ export type VirtualScrollProps<T> = {
|
|
|
310
312
|
* その場合は `onRangeChange` から自前のリージョンを更新する従来手段を使う。
|
|
311
313
|
*/
|
|
312
314
|
liveRegion?: VirtualScrollLiveRegionOptions
|
|
315
|
+
/**
|
|
316
|
+
* UI chrome locale of the built-in strings (default `"en"`): the scrollbar arrow aria-labels,
|
|
317
|
+
* the scroll-to-edge pills and the empty-state text. Live-region wording is not affected (see
|
|
318
|
+
* {@link VirtualScrollLiveRegionOptions.format}). An unsupported value throws a RangeError at
|
|
319
|
+
* render; no language negotiation happens.
|
|
320
|
+
* 内蔵文言 (スクロールバー矢印の aria-label、端スクロールピル、空状態文言) の UI クロームロケール
|
|
321
|
+
* (既定 `"en"`)。ライブリージョンの文言には影響しない ({@link VirtualScrollLiveRegionOptions.format}
|
|
322
|
+
* 参照)。非対応値は描画時に RangeError、言語ネゴシエーションなし。
|
|
323
|
+
*/
|
|
324
|
+
locale?: VirtualScrollLocale
|
|
325
|
+
/**
|
|
326
|
+
* Per-key overrides laid over the catalog of `locale`; forwarded to the pane's scrollbar as-is.
|
|
327
|
+
* Unknown keys and blank values throw a RangeError at render.
|
|
328
|
+
* `locale` のカタログへ重ねるキー単位の上書き。ペインのスクロールバーへもそのまま転送。
|
|
329
|
+
* 未知キーと空白値は描画時に RangeError。
|
|
330
|
+
*/
|
|
331
|
+
labels?: VirtualScrollLabelOverrides
|
|
313
332
|
background?: ReactNode
|
|
314
333
|
children: (item: T, index: number) => ReactNode
|
|
315
334
|
initialScrollIndex?: number
|
|
@@ -1023,10 +1042,13 @@ const VirtualScrollInner = <T,>(
|
|
|
1023
1042
|
horizontalKeyStep = DEFAULT_HORIZONTAL_KEY_STEP,
|
|
1024
1043
|
contentProps,
|
|
1025
1044
|
liveRegion,
|
|
1045
|
+
locale,
|
|
1046
|
+
labels,
|
|
1026
1047
|
}: VirtualScrollProps<T>,
|
|
1027
1048
|
ref: React.Ref<VirtualScrollHandle>,
|
|
1028
1049
|
) => {
|
|
1029
1050
|
const { width: scrollBarWidth, enableThumbDrag, enableTrackClick, enableArrowButtons, enableScrollToTopBottomButtons, renderThumbOverlay, tapScrollCircleOptions } = scrollBarOptions ?? {}
|
|
1051
|
+
const resolvedLabels = useMemo(() => resolveVirtualScrollLabels(locale, labels), [locale, labels])
|
|
1030
1052
|
|
|
1031
1053
|
const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, enableEscapeRowReturn = false, wheelSpeedMultiplier, inertiaOptions, overscrollBehavior, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
|
|
1032
1054
|
|
|
@@ -2168,9 +2190,11 @@ const VirtualScrollInner = <T,>(
|
|
|
2168
2190
|
}, [contentSize, renderingEndIndex, renderingStartIndex, resolvedInsets.top, scheduleRangeEffect, scrollPosition, visibleEndIndex, visibleStartIndex])
|
|
2169
2191
|
|
|
2170
2192
|
/**
|
|
2171
|
-
* Renders the auto-hiding
|
|
2193
|
+
* Renders the auto-hiding top/bottom pill overlay (texts from the resolved `scrollToTop` /
|
|
2194
|
+
* `scrollToBottom` labels), neutralized while hidden.
|
|
2172
2195
|
*
|
|
2173
|
-
*
|
|
2196
|
+
* 自動非表示の先頭/末尾ピルのオーバーレイ (文言は解決済みラベルの `scrollToTop` /
|
|
2197
|
+
* `scrollToBottom`) を描画し、非表示中は無効化する処理。
|
|
2174
2198
|
*
|
|
2175
2199
|
* ❗ 非表示は `opacity: 0` で表現するため、要素は DOM に残り続ける (フェードのために
|
|
2176
2200
|
* アンマウントしない)。`opacity` はフォーカス可能性に影響せず、CSS の `pointer-events: none`
|
|
@@ -2213,7 +2237,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2213
2237
|
scrollToIndex(0)
|
|
2214
2238
|
setShowScrollButtons(false)
|
|
2215
2239
|
}}>
|
|
2216
|
-
|
|
2240
|
+
{resolvedLabels.scrollToTop}
|
|
2217
2241
|
</button>
|
|
2218
2242
|
</div>
|
|
2219
2243
|
) : (
|
|
@@ -2229,13 +2253,13 @@ const VirtualScrollInner = <T,>(
|
|
|
2229
2253
|
scrollToIndex(itemCount - 1)
|
|
2230
2254
|
setShowScrollButtons(false)
|
|
2231
2255
|
}}>
|
|
2232
|
-
|
|
2256
|
+
{resolvedLabels.scrollToBottom}
|
|
2233
2257
|
</button>
|
|
2234
2258
|
</div>
|
|
2235
2259
|
)}
|
|
2236
2260
|
</div>
|
|
2237
2261
|
)
|
|
2238
|
-
}, [enableScrollToTopBottomButtons, showScrollButtons, scrollDirection, scrollToIndex, itemCount])
|
|
2262
|
+
}, [enableScrollToTopBottomButtons, showScrollButtons, scrollDirection, scrollToIndex, itemCount, resolvedLabels])
|
|
2239
2263
|
|
|
2240
2264
|
// 量子化アンカー (fix: LayoutUnit/f32 精度対策)。行 top はコンテンツ絶対座標そのままではなく
|
|
2241
2265
|
// 「絶対座標 - アンカー」で描画し、ラッパー側 translateY にアンカーを足し戻す。
|
|
@@ -2258,7 +2282,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2258
2282
|
return {
|
|
2259
2283
|
visibleItems: (
|
|
2260
2284
|
<div className="aqvs-no-items-container">
|
|
2261
|
-
<div className="aqvs-no-items-text">
|
|
2285
|
+
<div className="aqvs-no-items-text">{resolvedLabels.noItems}</div>
|
|
2262
2286
|
</div>
|
|
2263
2287
|
),
|
|
2264
2288
|
renderAnchor: 0,
|
|
@@ -2397,6 +2421,7 @@ const VirtualScrollInner = <T,>(
|
|
|
2397
2421
|
registerItemRef,
|
|
2398
2422
|
renderingEndIndex,
|
|
2399
2423
|
renderingStartIndex,
|
|
2424
|
+
resolvedLabels,
|
|
2400
2425
|
updateScrollPositionImmediate,
|
|
2401
2426
|
visibleStartIndex,
|
|
2402
2427
|
])
|
|
@@ -2588,7 +2613,9 @@ const VirtualScrollInner = <T,>(
|
|
|
2588
2613
|
visibleStartIndex={visibleStartIndex}
|
|
2589
2614
|
visibleEndIndex={visibleEndIndex}
|
|
2590
2615
|
renderOverlay={renderOverlay}
|
|
2591
|
-
initialScrollPosition={initialValues.position}
|
|
2616
|
+
initialScrollPosition={initialValues.position}
|
|
2617
|
+
locale={locale}
|
|
2618
|
+
labels={labels}>
|
|
2592
2619
|
{renderVisibleItems}
|
|
2593
2620
|
</ScrollPane>
|
|
2594
2621
|
)
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export { computeTapScrollVelocity, type TapScrollAxisSpeedParams, type TapScrollVelocityInput } from "./computeTapScrollVelocity.ts"
|
|
9
|
+
export { resolveVirtualScrollLabels, resolveVirtualScrollLocale, VIRTUAL_SCROLL_LABEL_CATALOGS, VIRTUAL_SCROLL_LABEL_KEYS, VIRTUAL_SCROLL_LOCALES, type VirtualScrollLabelOverrides, type VirtualScrollLabels, type VirtualScrollLocale } from "./labels.ts"
|
|
9
10
|
export { createResidualQuantizer, type ResidualQuantizer, type ResidualQuantizerOptions } from "./residualQuantizer.ts"
|
|
10
11
|
export { computeAutoTapScrollMaxSpeedMultiplier, computeTapScrollSpeed, ScrollBar, type ScrollBarProps, type ScrollBarTapCircleOptions, type ScrollBarThumbOverlayRenderProps, TAP_SCROLL_SPEED_DEFAULTS, type TapScrollSpeedInput } from "./ScrollBar.tsx"
|
|
11
12
|
export { ScrollPane, type ScrollPaneContentInsets, type ScrollPaneHandle, type ScrollPaneInertiaOptions, type ScrollPaneProps } from "./ScrollPane.tsx"
|