@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.
@@ -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?.hasPointerCapture(releasedPointerId)) {
1120
- element.releasePointerCapture(releasedPointerId)
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.hasPointerCapture(event.pointerId)) {
1218
+ if (!isPointerCaptured(element, event.pointerId)) {
1180
1219
  // 注意: キャプチャは pointerdown 時ではなくドラッグ確定時に取得する。
1181
1220
  // pointerdown で即キャプチャすると click がキャプチャ要素へリターゲットされ、
1182
1221
  // ペイン内の子要素 (行など) の click ハンドラが一切発火しなくなる (Chromium 実測)。
1183
1222
  // ドラッグ確定後は shouldCancelNextClick で click を意図的に抑止するため副作用がない。
1184
- try {
1185
- element.setPointerCapture(event.pointerId)
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
- // pointerup を受け取れないままドラッグ継続が不可能になったときの共通終了処理。
1231
- // Shared teardown for cases where the drag can no longer continue without receiving a pointerup.
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
- if (element.hasPointerCapture(pointerId)) {
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
- if (element.hasPointerCapture(event.pointerId)) {
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 && element.hasPointerCapture(state.pointerId)) {
1545
- element.releasePointerCapture(state.pointerId)
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>
@@ -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?.hasPointerCapture(pointerId)) {
214
- element.releasePointerCapture(pointerId)
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
- try {
250
- element.setPointerCapture(event.pointerId)
251
- } catch {
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
- let candidateAlive = true
300
- if (element !== null && typeof element.setPointerCapture === "function") {
301
- try {
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)
@@ -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 (no built-in strings — row-parity). / liveRegion オプション: 文言とロケールは消費側所有 (内蔵文字列なし — 行側と同一方針)。 */
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 && (
@@ -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 package ships NO built-in strings — wording and language are the
228
- * consumer's (a generic package must not hardcode a locale).
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 Top/Bottom pill overlay, neutralized while hidden.
2193
+ * Renders the auto-hiding top/bottom pill overlay (texts from the resolved `scrollToTop` /
2194
+ * `scrollToBottom` labels), neutralized while hidden.
2172
2195
  *
2173
- * 自動非表示の Top/Bottom ピルのオーバーレイを描画し、非表示中は無効化する処理。
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
- Top
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
- Bottom
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">No items</div>
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"