@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/src/labels.ts ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * @module labels
3
+ * @description Built-in UI chrome label catalog of the package. Covers exactly the seven strings
4
+ * the components render on their own: the ScrollBar arrow aria-labels, the VirtualScroll
5
+ * scroll-to-edge pill texts and the empty-state text. Live-region wording stays consumer-owned
6
+ * (`liveRegion.format` / `liveRegion.buildMessage`) and is not part of this catalog. The default
7
+ * locale is `"en"`; `"ja"` is the second locale. No language negotiation happens here: hosts map
8
+ * `navigator.language` (or anything else) to a supported locale themselves.
9
+ *
10
+ * @description パッケージ内蔵の UI クローム文言カタログ。コンポーネント自身が描画する 7 文言
11
+ * (ScrollBar 矢印の aria-label、VirtualScroll の端スクロールピル文言、空状態文言) だけを対象とし、
12
+ * ライブリージョンの文言は利用側の所有 (`liveRegion.format` / `liveRegion.buildMessage`) で
13
+ * 本カタログの対象外。既定ロケールは `"en"`、第 2 ロケールは `"ja"`。言語ネゴシエーションは行わず、
14
+ * `navigator.language` 等から対応ロケールへの対応付けはホスト側の責務。
15
+ */
16
+
17
+ /**
18
+ * Supported UI chrome locales, in declaration order. The first entry is NOT implicitly the
19
+ * default — the default lives only in {@link resolveVirtualScrollLocale}.
20
+ * 対応する UI クロームロケールの一覧 (宣言順)。先頭要素が暗黙の既定値になるわけではなく、
21
+ * 既定値は {@link resolveVirtualScrollLocale} にのみ存在。
22
+ */
23
+ export const VIRTUAL_SCROLL_LOCALES = ["en", "ja"] as const
24
+
25
+ /**
26
+ * A supported UI chrome locale (`"en"` | `"ja"`).
27
+ * 対応 UI クロームロケール (`"en"` | `"ja"`)。
28
+ */
29
+ export type VirtualScrollLocale = (typeof VIRTUAL_SCROLL_LOCALES)[number]
30
+
31
+ /**
32
+ * The complete set of built-in chrome strings, one value per rendered surface.
33
+ * 内蔵クローム文言の完全な集合 (描画面ごとに 1 値)。
34
+ */
35
+ export type VirtualScrollLabels = {
36
+ /**
37
+ * aria-label of the vertical ScrollBar start (up) arrow button.
38
+ * 縦 ScrollBar 始端 (上) 矢印ボタンの aria-label。
39
+ */
40
+ readonly scrollUp: string
41
+ /**
42
+ * aria-label of the vertical ScrollBar end (down) arrow button.
43
+ * 縦 ScrollBar 終端 (下) 矢印ボタンの aria-label。
44
+ */
45
+ readonly scrollDown: string
46
+ /**
47
+ * aria-label of the horizontal ScrollBar start (left) arrow button.
48
+ * 横 ScrollBar 始端 (左) 矢印ボタンの aria-label。
49
+ */
50
+ readonly scrollLeft: string
51
+ /**
52
+ * aria-label of the horizontal ScrollBar end (right) arrow button.
53
+ * 横 ScrollBar 終端 (右) 矢印ボタンの aria-label。
54
+ */
55
+ readonly scrollRight: string
56
+ /**
57
+ * Text of the VirtualScroll scroll-to-top pill, shown when
58
+ * `scrollBarOptions.enableScrollToTopBottomButtons` is on.
59
+ * `scrollBarOptions.enableScrollToTopBottomButtons` 有効時に表示される VirtualScroll 先頭移動ピルの文言。
60
+ */
61
+ readonly scrollToTop: string
62
+ /**
63
+ * Text of the VirtualScroll scroll-to-bottom pill, shown when
64
+ * `scrollBarOptions.enableScrollToTopBottomButtons` is on.
65
+ * `scrollBarOptions.enableScrollToTopBottomButtons` 有効時に表示される VirtualScroll 末尾移動ピルの文言。
66
+ */
67
+ readonly scrollToBottom: string
68
+ /**
69
+ * VirtualScroll empty-state text shown when `itemCount === 0`. VirtualGrid shows it too, for
70
+ * 0 rows, an all-frozen row set and degenerate bands (its embedded VirtualScroll gets 0 items).
71
+ * `itemCount === 0` 時の VirtualScroll 空状態文言。VirtualGrid も 0 行・全行固定・退化帯で表示
72
+ * (内蔵 VirtualScroll の項目数が 0 になるため)。
73
+ */
74
+ readonly noItems: string
75
+ }
76
+
77
+ /**
78
+ * Partial per-key overrides laid over the catalog of the chosen locale. An `undefined` value keeps
79
+ * the catalog value; any present value must be a string with non-whitespace content.
80
+ * 選択ロケールのカタログへ重ねるキー単位の部分上書き。`undefined` 値はカタログ値を維持し、
81
+ * 値を与える場合は空白以外の文字を含む文字列であることが必須。
82
+ */
83
+ export type VirtualScrollLabelOverrides = { readonly [K in keyof VirtualScrollLabels]?: VirtualScrollLabels[K] }
84
+
85
+ /**
86
+ * Every key of {@link VirtualScrollLabels}, in catalog order. This is the closed key set that
87
+ * overrides are validated against.
88
+ * {@link VirtualScrollLabels} の全キー (カタログ順)。上書き検証に用いる閉じたキー集合。
89
+ */
90
+ export const VIRTUAL_SCROLL_LABEL_KEYS = ["scrollUp", "scrollDown", "scrollLeft", "scrollRight", "scrollToTop", "scrollToBottom", "noItems"] as const satisfies readonly (keyof VirtualScrollLabels)[]
91
+
92
+ // satisfies は部分集合しか保証しないため、キー追加時の登録漏れを型で検出する
93
+ const keysCoverEveryLabel: [Exclude<keyof VirtualScrollLabels, (typeof VIRTUAL_SCROLL_LABEL_KEYS)[number]>] extends [never] ? true : never = true
94
+ void keysCoverEveryLabel
95
+
96
+ /**
97
+ * Frozen built-in catalogs per locale. `en` is the package's historical English wording.
98
+ * ロケールごとの凍結済み内蔵カタログ。`en` はパッケージ従来の英語文言。
99
+ */
100
+ export const VIRTUAL_SCROLL_LABEL_CATALOGS: Readonly<Record<VirtualScrollLocale, VirtualScrollLabels>> = Object.freeze({
101
+ en: Object.freeze({
102
+ scrollUp: "Scroll up",
103
+ scrollDown: "Scroll down",
104
+ scrollLeft: "Scroll left",
105
+ scrollRight: "Scroll right",
106
+ scrollToTop: "Top",
107
+ scrollToBottom: "Bottom",
108
+ noItems: "No items",
109
+ }),
110
+ ja: Object.freeze({
111
+ scrollUp: "上へスクロール",
112
+ scrollDown: "下へスクロール",
113
+ scrollLeft: "左へスクロール",
114
+ scrollRight: "右へスクロール",
115
+ scrollToTop: "先頭へ",
116
+ scrollToBottom: "末尾へ",
117
+ noItems: "項目がありません",
118
+ }),
119
+ })
120
+
121
+ /**
122
+ * Type guard: whether `value` is a member of {@link VIRTUAL_SCROLL_LOCALES}.
123
+ * `value` が {@link VIRTUAL_SCROLL_LOCALES} の要素かどうかの型ガード。
124
+ *
125
+ * @param value - Candidate value / 判定対象の値
126
+ * @returns `true` when `value` is a supported locale / 対応ロケールなら `true`
127
+ */
128
+ const isVirtualScrollLocale = (value: unknown): value is VirtualScrollLocale => (VIRTUAL_SCROLL_LOCALES as readonly unknown[]).includes(value)
129
+
130
+ /**
131
+ * Describes a rejected input for an error message without ever throwing: JSON for strings,
132
+ * `"null"` for `null`, otherwise the `typeof` tag. Never calls `String()` / `JSON.stringify` on
133
+ * arbitrary values, so BigInt, Symbol and null-prototype inputs still produce the documented
134
+ * RangeError with an accurate description.
135
+ * 拒否した入力をエラーメッセージ用に説明 (例外を投げない)。文字列は JSON 表記、`null` は `"null"`、
136
+ * それ以外は `typeof` のタグ。任意値へ `String()` / `JSON.stringify` を適用しないため、BigInt・Symbol・
137
+ * null プロトタイプの入力でも文書どおりの RangeError と正確な説明を維持。
138
+ *
139
+ * @param value - Rejected input / 拒否した入力
140
+ * @returns A short, non-throwing description of `value` / `value` の短い説明 (例外なし)
141
+ */
142
+ const describeRejectedValue = (value: unknown): string => {
143
+ if (typeof value === "string") {
144
+ return JSON.stringify(value)
145
+ }
146
+ if (value === null) {
147
+ return "null"
148
+ }
149
+ return typeof value
150
+ }
151
+
152
+ /**
153
+ * Resolves the UI chrome locale. `undefined` (an absent prop) maps to the default `"en"`; a member
154
+ * of {@link VIRTUAL_SCROLL_LOCALES} is returned as-is. This is the single place the default lives.
155
+ * UI クロームロケールの解決。`undefined` (prop 未指定) は既定の `"en"`、
156
+ * {@link VIRTUAL_SCROLL_LOCALES} の要素はそのまま返却。既定値を持つ唯一の場所。
157
+ *
158
+ * @param locale - Requested locale, or `undefined` for the default / 要求ロケール (`undefined` で既定)
159
+ * @returns The resolved supported locale / 解決済みの対応ロケール
160
+ * @throws {RangeError} When `locale` is neither `undefined` nor a supported locale (`null`, `""`,
161
+ * `"EN"`, `"ja-JP"`, `"fr"`, numbers, ...) — no language negotiation /
162
+ * `undefined` でも対応ロケールでもない場合 (`null`、`""`、`"EN"`、`"ja-JP"`、`"fr"`、数値など)。言語ネゴシエーションなし
163
+ */
164
+ export const resolveVirtualScrollLocale = (locale: VirtualScrollLocale | undefined): VirtualScrollLocale => {
165
+ if (locale === undefined) {
166
+ return "en"
167
+ }
168
+ if (isVirtualScrollLocale(locale)) {
169
+ return locale
170
+ }
171
+ const allowed = VIRTUAL_SCROLL_LOCALES.map((candidate) => JSON.stringify(candidate)).join(", ")
172
+ throw new RangeError(`[virtualscroll] locale must be one of ${allowed}; got ${describeRejectedValue(locale)}`)
173
+ }
174
+
175
+ /**
176
+ * Type guard: whether `key` is a member of {@link VIRTUAL_SCROLL_LABEL_KEYS}.
177
+ * `key` が {@link VIRTUAL_SCROLL_LABEL_KEYS} の要素かどうかの型ガード。
178
+ *
179
+ * @param key - Candidate own key of an overrides object / 上書きオブジェクトの自身のキー候補
180
+ * @returns `true` when `key` is a label key / ラベルキーなら `true`
181
+ */
182
+ const isVirtualScrollLabelKey = (key: string): key is keyof VirtualScrollLabels => (VIRTUAL_SCROLL_LABEL_KEYS as readonly string[]).includes(key)
183
+
184
+ /**
185
+ * Resolves the effective labels: the catalog of the resolved locale overlaid by `overrides`.
186
+ * Returns the frozen catalog itself (same identity) when `overrides` is `undefined`, otherwise a
187
+ * new frozen object. Inputs are never mutated.
188
+ * 実効ラベルの解決。解決済みロケールのカタログへ `overrides` を重ねた結果。`overrides` が
189
+ * `undefined` なら凍結カタログそのもの (同一参照)、それ以外は新しい凍結オブジェクトを返却。入力は不変。
190
+ *
191
+ * @param locale - Requested locale, or `undefined` for the default / 要求ロケール (`undefined` で既定)
192
+ * @param overrides - Per-key overrides, or `undefined` for none / キー単位の上書き (`undefined` で上書きなし)
193
+ * @returns The resolved, frozen labels / 解決済みの凍結ラベル
194
+ * @throws {RangeError} When the locale is unsupported, `overrides` is `null` or not an object,
195
+ * `overrides` is not a plain object (its prototype is neither `Object.prototype` nor `null`, e.g.
196
+ * an array or a class instance), an own key is not a label key, or a present own value is not a
197
+ * string with non-whitespace content. Only own properties are read /
198
+ * ロケールが非対応、`overrides` が `null` または非オブジェクト、`overrides` が素のオブジェクトでない
199
+ * (プロトタイプが `Object.prototype` でも `null` でもない。配列やクラスインスタンスなど)、自身のキーが
200
+ * ラベルキー外、または与えられた自身の値が空白以外の文字を含む文字列でない場合。読むのは自身の
201
+ * プロパティのみ
202
+ */
203
+ export const resolveVirtualScrollLabels = (locale: VirtualScrollLocale | undefined, overrides: VirtualScrollLabelOverrides | undefined): VirtualScrollLabels => {
204
+ const catalog = VIRTUAL_SCROLL_LABEL_CATALOGS[resolveVirtualScrollLocale(locale)]
205
+ if (overrides === undefined) {
206
+ return catalog
207
+ }
208
+ if (overrides === null || typeof overrides !== "object") {
209
+ throw new RangeError(`[virtualscroll] labels must be an object; got ${describeRejectedValue(overrides)}`)
210
+ }
211
+ // 既知キー検査は自身のキーしか見ないため、継承経路を持つ入力 (配列・クラスインスタンス・
212
+ // 他オブジェクト継承) を許すと継承側の綴り誤りが無言で無視される
213
+ const prototype: unknown = Object.getPrototypeOf(overrides)
214
+ if (prototype !== Object.prototype && prototype !== null) {
215
+ throw new RangeError(`[virtualscroll] labels must be a plain object (prototype Object.prototype or null); got ${Array.isArray(overrides) ? "an array" : "an object with a non-plain prototype"}`)
216
+ }
217
+ const ownKeys = new Set(Object.keys(overrides))
218
+ for (const key of ownKeys) {
219
+ if (!isVirtualScrollLabelKey(key)) {
220
+ throw new RangeError(`[virtualscroll] labels.${key} is not a known label key; expected one of ${VIRTUAL_SCROLL_LABEL_KEYS.join(", ")}`)
221
+ }
222
+ }
223
+ const resolved: Record<keyof VirtualScrollLabels, string> = { ...catalog }
224
+ for (const key of VIRTUAL_SCROLL_LABEL_KEYS) {
225
+ // 検査したのと同じ自身のキー集合だけを読み、Object.prototype 汚染で継承されたラベルキーを拾わない
226
+ const value: unknown = ownKeys.has(key) ? overrides[key] : undefined
227
+ if (value === undefined) {
228
+ continue
229
+ }
230
+ if (typeof value !== "string" || value.trim() === "") {
231
+ throw new RangeError(`[virtualscroll] labels.${key} must be a non-blank string; got ${describeRejectedValue(value)}`)
232
+ }
233
+ resolved[key] = value
234
+ }
235
+ return Object.freeze(resolved)
236
+ }
@@ -0,0 +1,89 @@
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
+ /**
23
+ * Outcome of a pointer capture request.
24
+ * ポインタキャプチャ要求の結果。
25
+ *
26
+ * - `"captured"`: The element now holds the capture. / 要素がキャプチャを保持した。
27
+ * - `"unsupported"`: The element does not implement the Pointer Capture API; nothing was attempted. / 要素が API を実装しておらず、要求自体を行っていない。
28
+ * - `"inactive-pointer"`: The UA rejected the pointer as inactive (`NotFoundError`). / ポインタが非アクティブとして拒否された (`NotFoundError`)。
29
+ * - `"rejected"`: The UA rejected the request for any other reason. / それ以外の理由で拒否された。
30
+ */
31
+ export type PointerCaptureResult = "captured" | "unsupported" | "inactive-pointer" | "rejected"
32
+
33
+ /**
34
+ * Reports whether the element implements the whole Pointer Capture API.
35
+ * 要素が Pointer Capture API (取得・解放・保持確認の 3 メソッド) をすべて実装しているかの判定処理。
36
+ *
37
+ * @param element The element to inspect. 判定対象の要素。
38
+ * @returns True when set / release / has are all callable. 3 メソッドがすべて呼び出し可能な場合に true。
39
+ */
40
+ export const supportsPointerCapture = (element: Element): boolean => typeof element.setPointerCapture === "function" && typeof element.releasePointerCapture === "function" && typeof element.hasPointerCapture === "function"
41
+
42
+ /**
43
+ * Reports whether the element holds (or has a pending) capture of the pointer.
44
+ * 要素が指定ポインタのキャプチャを保持 (または保留) しているかの判定処理。
45
+ *
46
+ * @param element The element to inspect. 判定対象の要素。
47
+ * @param pointerId The pointer to look up. 対象のポインタ ID。
48
+ * @returns True when the capture is held; always false when the API is unsupported. 保持している場合に true。API 非対応の要素では常に false。
49
+ */
50
+ export const isPointerCaptured = (element: Element, pointerId: number): boolean => supportsPointerCapture(element) && element.hasPointerCapture(pointerId)
51
+
52
+ /**
53
+ * Requests capture of the pointer for the element and classifies the outcome without throwing.
54
+ * 要素へ指定ポインタのキャプチャを要求し、例外を投げずに結果を分類する処理。
55
+ *
56
+ * @param element The element that should receive the capture. キャプチャを受け取る要素。
57
+ * @param pointerId The pointer to capture. キャプチャするポインタ ID。
58
+ * @returns The classified outcome of the request. 要求結果の分類。
59
+ */
60
+ export const capturePointer = (element: Element, pointerId: number): PointerCaptureResult => {
61
+ if (!supportsPointerCapture(element)) {
62
+ return "unsupported"
63
+ }
64
+ try {
65
+ element.setPointerCapture(pointerId)
66
+ return "captured"
67
+ } catch (error) {
68
+ // クロスレルム (iframe への portal 等) では DOMException の instanceof が false になるため name で判定する
69
+ const errorName = typeof error === "object" && error !== null && "name" in error ? (error as { name?: unknown }).name : undefined
70
+ return errorName === "NotFoundError" ? "inactive-pointer" : "rejected"
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Releases the element's capture of the pointer when it holds one; does nothing otherwise.
76
+ * 要素が指定ポインタのキャプチャを保持していれば解放し、保持していなければ何もしない処理。
77
+ *
78
+ * 呼び出し側が状態を確定させてから呼ぶこと。`lostpointercapture` を同期発火するブラウザでは、
79
+ * この呼び出しの最中に喪失ハンドラーが走る。
80
+ *
81
+ * @param element The element that may hold the capture. キャプチャを保持している可能性のある要素。
82
+ * @param pointerId The pointer to release. 解放するポインタ ID。
83
+ * @returns Nothing. なし。
84
+ */
85
+ export const releaseCapturedPointer = (element: Element, pointerId: number): void => {
86
+ if (isPointerCaptured(element, pointerId)) {
87
+ element.releasePointerCapture(pointerId)
88
+ }
89
+ }