@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.
- package/CHANGELOG.md +73 -0
- package/README.md +5 -3
- package/dist/ScrollBar.d.cts +123 -0
- package/dist/ScrollBar.d.ts +123 -0
- package/dist/ScrollBar.d.ts.map +1 -1
- package/dist/ScrollPane.d.cts +16 -0
- package/dist/ScrollPane.d.ts +16 -0
- package/dist/ScrollPane.d.ts.map +1 -1
- package/dist/VirtualScroll.d.cts +9 -0
- package/dist/VirtualScroll.d.ts +9 -0
- package/dist/VirtualScroll.d.ts.map +1 -1
- package/dist/_headFenwick.d.cts +450 -0
- package/dist/_headFenwick.d.ts +451 -0
- package/dist/_headFenwick.d.ts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1468 -1360
- package/dist/styles/virtualscroll.css +1 -1
- package/dist/styles/virtualscroll.standalone.css +1 -1
- package/dist/tapScrollCircleSampleVisual.d.ts.map +1 -1
- package/dist/useFenwickMapTree.d.cts +43 -0
- package/dist/useFenwickMapTree.d.ts +43 -0
- package/dist/useFenwickMapTree.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/ScrollBar.tsx +211 -33
- package/src/ScrollPane.tsx +232 -19
- package/src/VirtualScroll.tsx +11 -1
- package/src/index.ts +1 -1
- package/src/styles/virtualscroll.css +3 -3
- package/src/tapScrollCircleSampleVisual.tsx +22 -5
- package/src/useFenwickMapTree.ts +95 -13
- package/src/ScrollBar.spec.tsx +0 -620
- package/src/ScrollPane.spec.tsx +0 -496
- package/src/TapScrollCircle.spec.tsx +0 -275
- package/src/VirtualScroll.spec.ts +0 -641
- package/src/cli.server.spec.ts +0 -137
- package/src/logger.spec.ts +0 -128
- package/src/useFenwickMapTree.huge.spec.ts +0 -388
- package/src/useFenwickMapTree.spec.ts +0 -1518
- package/src/useLruCache.spec.ts +0 -382
- 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
|
-
})
|