@aiquants/virtualscroll 1.20.0 → 1.22.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +5 -3
  3. package/dist/ScrollBar.d.cts +123 -0
  4. package/dist/ScrollBar.d.ts +123 -0
  5. package/dist/ScrollBar.d.ts.map +1 -1
  6. package/dist/ScrollPane.d.cts +16 -0
  7. package/dist/ScrollPane.d.ts +16 -0
  8. package/dist/ScrollPane.d.ts.map +1 -1
  9. package/dist/VirtualScroll.d.cts +9 -0
  10. package/dist/VirtualScroll.d.ts +9 -0
  11. package/dist/VirtualScroll.d.ts.map +1 -1
  12. package/dist/_headFenwick.d.cts +450 -0
  13. package/dist/_headFenwick.d.ts +451 -0
  14. package/dist/_headFenwick.d.ts.map +1 -0
  15. package/dist/index.cjs +1 -1
  16. package/dist/index.d.cts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1468 -1360
  20. package/dist/styles/virtualscroll.css +1 -1
  21. package/dist/styles/virtualscroll.standalone.css +1 -1
  22. package/dist/tapScrollCircleSampleVisual.d.ts.map +1 -1
  23. package/dist/useFenwickMapTree.d.cts +43 -0
  24. package/dist/useFenwickMapTree.d.ts +43 -0
  25. package/dist/useFenwickMapTree.d.ts.map +1 -1
  26. package/package.json +4 -1
  27. package/src/ScrollBar.tsx +211 -33
  28. package/src/ScrollPane.tsx +232 -19
  29. package/src/VirtualScroll.tsx +11 -1
  30. package/src/index.ts +1 -1
  31. package/src/styles/virtualscroll.css +3 -3
  32. package/src/tapScrollCircleSampleVisual.tsx +22 -5
  33. package/src/useFenwickMapTree.ts +95 -13
  34. package/src/ScrollBar.spec.tsx +0 -620
  35. package/src/ScrollPane.spec.tsx +0 -496
  36. package/src/TapScrollCircle.spec.tsx +0 -275
  37. package/src/VirtualScroll.spec.ts +0 -641
  38. package/src/cli.server.spec.ts +0 -137
  39. package/src/logger.spec.ts +0 -128
  40. package/src/useFenwickMapTree.huge.spec.ts +0 -388
  41. package/src/useFenwickMapTree.spec.ts +0 -1518
  42. package/src/useLruCache.spec.ts +0 -382
  43. package/src/utils.spec.ts +0 -39
@@ -1,275 +0,0 @@
1
- /**
2
- * Provides regression tests for TapScrollCircle pointer-capture robustness.
3
- *
4
- * TapScrollCircle のポインタキャプチャ堅牢化に関する回帰テストを提供。
5
- */
6
- import { act, cleanup, fireEvent, render } from "@testing-library/react"
7
- import { createRef } from "react"
8
- import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"
9
- import { TapScrollCircle, type TapScrollCircleDragState, type TapScrollCircleHandle } from "./TapScrollCircle.tsx"
10
-
11
- // jsdom は PointerEvent とポインタキャプチャ API を持たないため補完する。
12
- // jsdom lacks PointerEvent and pointer-capture APIs; polyfill them for these tests.
13
- beforeAll(() => {
14
- if (typeof window.PointerEvent === "undefined") {
15
- class PointerEventPolyfill extends MouseEvent {
16
- pointerId: number
17
- pointerType: string
18
- constructor(type: string, params: PointerEventInit = {}) {
19
- super(type, params)
20
- this.pointerId = params.pointerId ?? 0
21
- this.pointerType = params.pointerType ?? "mouse"
22
- }
23
- }
24
- // @ts-expect-error jsdom への PointerEvent 補完
25
- window.PointerEvent = PointerEventPolyfill
26
- }
27
- HTMLElement.prototype.setPointerCapture ??= function setPointerCapture() {}
28
- HTMLElement.prototype.releasePointerCapture ??= function releasePointerCapture() {}
29
- HTMLElement.prototype.hasPointerCapture ??= function hasPointerCapture() {
30
- return false
31
- }
32
- })
33
-
34
- afterEach(() => {
35
- cleanup()
36
- })
37
-
38
- /**
39
- * Builds a PointerEvent-like init for jsdom fireEvent.pointer*.
40
- *
41
- * jsdom の fireEvent.pointer* 用に PointerEvent 相当の初期化を生成。
42
- */
43
- const pointerInit = (pointerId: number, clientX: number, clientY: number) => ({
44
- pointerId,
45
- clientX,
46
- clientY,
47
- // jsdom は PointerEvent を持たないため bubbles/pointerType を明示する
48
- bubbles: true,
49
- pointerType: "touch",
50
- })
51
-
52
- describe("TapScrollCircle pointer capture robustness", () => {
53
- it("does not throw when setPointerCapture fails (pointer already inactive)", () => {
54
- const onDragChange = vi.fn()
55
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
56
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
57
-
58
- // ウインドウ外へ出た直後などをシミュレート: setPointerCapture が NotFoundError を投げる
59
- circle.setPointerCapture = () => {
60
- throw new DOMException("pointer not found", "NotFoundError")
61
- }
62
-
63
- expect(() => {
64
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
65
- }).not.toThrow()
66
-
67
- // キャプチャ失敗後もドラッグ状態は開始している
68
- const last = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
69
- expect(last.active).toBe(true)
70
- })
71
-
72
- it("resets drag state and notifies inactive on lostpointercapture", () => {
73
- const onDragChange = vi.fn()
74
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
75
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
76
- circle.setPointerCapture = vi.fn()
77
-
78
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
79
- // 下方向へドラッグして direction を確定させる
80
- fireEvent.pointerMove(circle, pointerInit(1, 0, 140))
81
- const dragging = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
82
- expect(dragging.active).toBe(true)
83
- expect(dragging.direction).not.toBe(0)
84
-
85
- // 強制的なキャプチャ解放 (pointerup を受け取れないケース)
86
- fireEvent.lostPointerCapture(circle, { pointerId: 1, bubbles: true })
87
-
88
- const afterLost = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
89
- expect(afterLost.active).toBe(false)
90
- expect(afterLost.direction).toBe(0)
91
- expect(afterLost.pointerId).toBeNull()
92
- })
93
-
94
- it("ignores lostpointercapture for a non-matching pointerId", () => {
95
- const onDragChange = vi.fn()
96
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
97
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
98
- circle.setPointerCapture = vi.fn()
99
-
100
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
101
- fireEvent.pointerMove(circle, pointerInit(1, 0, 140))
102
- const callsBefore = onDragChange.mock.calls.length
103
-
104
- // 別ポインターの lostpointercapture では状態を触らない
105
- fireEvent.lostPointerCapture(circle, { pointerId: 99, bubbles: true })
106
- expect(onDragChange.mock.calls.length).toBe(callsBefore)
107
- const last = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
108
- expect(last.active).toBe(true)
109
- })
110
-
111
- it("ignores a second pointer while dragging with the first", () => {
112
- const onDragChange = vi.fn()
113
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
114
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
115
- const capture = vi.fn()
116
- circle.setPointerCapture = capture
117
-
118
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
119
- expect(capture).toHaveBeenCalledTimes(1)
120
-
121
- // 2 本目のタッチ (別 pointerId) は無視されるべき
122
- fireEvent.pointerDown(circle, pointerInit(2, 0, 100))
123
- expect(capture).toHaveBeenCalledTimes(1)
124
-
125
- // ドラッグ中の pointerId は 1 のまま
126
- const last = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
127
- expect(last.pointerId).toBe(1)
128
-
129
- // 2 本目のポインターの move は無視される
130
- const callsBeforeMove = onDragChange.mock.calls.length
131
- fireEvent.pointerMove(circle, pointerInit(2, 0, 200))
132
- expect(onDragChange.mock.calls.length).toBe(callsBeforeMove)
133
- })
134
-
135
- it("hands off control to a waiting second pointer when the primary is released", () => {
136
- const onDragChange = vi.fn()
137
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
138
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
139
- const capture = vi.fn()
140
- circle.setPointerCapture = capture
141
-
142
- // finger1 でドラッグ開始
143
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
144
- fireEvent.pointerMove(circle, pointerInit(1, 0, 140))
145
- expect((onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState).active).toBe(true)
146
-
147
- // finger2 をサークルに置く (待機候補として記録される)
148
- fireEvent.pointerDown(circle, pointerInit(2, 0, 60))
149
-
150
- // finger1 を離すと finger2 へ制御が移譲される
151
- fireEvent.pointerUp(circle, pointerInit(1, 0, 140))
152
- const afterHandoff = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
153
- expect(afterHandoff.active).toBe(true)
154
- expect(afterHandoff.pointerId).toBe(2)
155
- expect(capture).toHaveBeenCalledWith(2)
156
-
157
- // 移譲後は finger2 の move でドラッグが継続する
158
- fireEvent.pointerMove(circle, pointerInit(2, 0, 200))
159
- const moved = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
160
- expect(moved.active).toBe(true)
161
- expect(moved.pointerId).toBe(2)
162
- expect(moved.offsetY).toBe(200)
163
-
164
- // finger2 を離すと通常どおりリセットされる
165
- fireEvent.pointerUp(circle, pointerInit(2, 0, 200))
166
- const released = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
167
- expect(released.active).toBe(false)
168
- expect(released.pointerId).toBeNull()
169
- })
170
-
171
- it("tracks the waiting pointer position so the handoff starts at its latest location", () => {
172
- const onDragChange = vi.fn()
173
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
174
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
175
- circle.setPointerCapture = vi.fn()
176
-
177
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
178
- fireEvent.pointerDown(circle, pointerInit(2, 0, 60))
179
- // 待機中の finger2 が移動しても座標は追従する (onDragChange は発火しない)
180
- const callsBefore = onDragChange.mock.calls.length
181
- fireEvent.pointerMove(circle, pointerInit(2, 0, 160))
182
- expect(onDragChange.mock.calls.length).toBe(callsBefore)
183
-
184
- fireEvent.pointerUp(circle, pointerInit(1, 0, 100))
185
- const afterHandoff = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
186
- expect(afterHandoff.active).toBe(true)
187
- expect(afterHandoff.pointerId).toBe(2)
188
- expect(afterHandoff.offsetY).toBe(160)
189
- })
190
-
191
- it("resets normally when the waiting pointer was already released before the primary", () => {
192
- const onDragChange = vi.fn()
193
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
194
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
195
- circle.setPointerCapture = vi.fn()
196
-
197
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
198
- fireEvent.pointerDown(circle, pointerInit(2, 0, 60))
199
- // finger2 が先に離れた場合、候補は破棄され通常のリセットになる
200
- fireEvent.pointerUp(circle, pointerInit(2, 0, 60))
201
- fireEvent.pointerUp(circle, pointerInit(1, 0, 100))
202
- const last = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
203
- expect(last.active).toBe(false)
204
- expect(last.pointerId).toBeNull()
205
- })
206
-
207
- it("treats a cross-realm NotFoundError (name-based) as a dead candidate on handoff", () => {
208
- const onDragChange = vi.fn()
209
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
210
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
211
-
212
- // クロスレルムの DOMException を模倣: instanceof DOMException は false だが name は NotFoundError
213
- class CrossRealmDOMException extends Error {}
214
- const crossRealmError = new CrossRealmDOMException("pointer not found")
215
- crossRealmError.name = "NotFoundError"
216
- expect(crossRealmError instanceof DOMException).toBe(false)
217
-
218
- // 候補ポインタ (2) の生存プローブだけが失敗する
219
- circle.setPointerCapture = (pointerId: number) => {
220
- if (pointerId === 2) {
221
- throw crossRealmError
222
- }
223
- }
224
-
225
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
226
- fireEvent.pointerMove(circle, pointerInit(1, 0, 140))
227
- // finger2 を待機候補として置くが、既に非アクティブ (プローブが NotFoundError)
228
- fireEvent.pointerDown(circle, pointerInit(2, 0, 60))
229
- fireEvent.pointerUp(circle, pointerInit(1, 0, 140))
230
-
231
- // 死んだ候補へは移譲されず、状態はリセットされる (active=true 固着の防止)
232
- const last = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
233
- expect(last.active).toBe(false)
234
- expect(last.pointerId).toBeNull()
235
- })
236
-
237
- it("continues the handoff when the liveness probe fails with a non-NotFoundError error", () => {
238
- const onDragChange = vi.fn()
239
- const { getByTestId } = render(<TapScrollCircle onDragChange={onDragChange} opacity={1} />)
240
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
241
-
242
- // NotFoundError 以外 (キャプチャ未サポート等) は生死不明のため移譲を継続する
243
- circle.setPointerCapture = (pointerId: number) => {
244
- if (pointerId === 2) {
245
- throw new Error("capture not supported")
246
- }
247
- }
248
-
249
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
250
- fireEvent.pointerDown(circle, pointerInit(2, 0, 60))
251
- fireEvent.pointerUp(circle, pointerInit(1, 0, 100))
252
-
253
- const last = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
254
- expect(last.active).toBe(true)
255
- expect(last.pointerId).toBe(2)
256
- })
257
-
258
- it("reset() clears state via imperative handle", () => {
259
- const onDragChange = vi.fn()
260
- const ref = createRef<TapScrollCircleHandle>()
261
- const { getByTestId } = render(<TapScrollCircle ref={ref} onDragChange={onDragChange} opacity={1} />)
262
- const circle = getByTestId("virtual-scroll-tap-circle") as HTMLDivElement
263
- circle.setPointerCapture = vi.fn()
264
-
265
- fireEvent.pointerDown(circle, pointerInit(1, 0, 100))
266
- fireEvent.pointerMove(circle, pointerInit(1, 0, 140))
267
-
268
- act(() => {
269
- ref.current?.reset()
270
- })
271
- const last = onDragChange.mock.calls.at(-1)?.[0] as TapScrollCircleDragState
272
- expect(last.active).toBe(false)
273
- expect(last.pointerId).toBeNull()
274
- })
275
- })