@pyreon/dnd 0.11.5 → 0.11.6
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/README.md +1 -0
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +2 -2
- package/package.json +13 -13
- package/src/index.ts +8 -8
- package/src/tests/dnd.test.ts +218 -218
- package/src/types.ts +2 -2
- package/src/use-drag-monitor.ts +3 -3
- package/src/use-draggable.ts +5 -5
- package/src/use-droppable.ts +4 -4
- package/src/use-file-drop.ts +6 -6
- package/src/use-sortable.ts +19 -19
package/src/tests/dnd.test.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { describe, expect, it } from
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
3
|
|
|
4
4
|
// ─── useDraggable ───────────────────────────────────────────────────────────
|
|
5
5
|
|
|
6
|
-
describe(
|
|
7
|
-
it(
|
|
8
|
-
const { useDraggable } = await import(
|
|
9
|
-
expect(typeof useDraggable).toBe(
|
|
6
|
+
describe('useDraggable', () => {
|
|
7
|
+
it('exports useDraggable', async () => {
|
|
8
|
+
const { useDraggable } = await import('../index')
|
|
9
|
+
expect(typeof useDraggable).toBe('function')
|
|
10
10
|
})
|
|
11
11
|
|
|
12
|
-
it(
|
|
13
|
-
const { useDraggable } = await import(
|
|
14
|
-
const el = document.createElement(
|
|
15
|
-
const { isDragging } = useDraggable({ element: () => el, data: { id:
|
|
12
|
+
it('returns isDragging signal initialized to false', async () => {
|
|
13
|
+
const { useDraggable } = await import('../use-draggable')
|
|
14
|
+
const el = document.createElement('div')
|
|
15
|
+
const { isDragging } = useDraggable({ element: () => el, data: { id: '1' } })
|
|
16
16
|
expect(isDragging()).toBe(false)
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
it(
|
|
20
|
-
const { useDraggable } = await import(
|
|
21
|
-
const el = document.createElement(
|
|
19
|
+
it('accepts function data for dynamic values', async () => {
|
|
20
|
+
const { useDraggable } = await import('../use-draggable')
|
|
21
|
+
const el = document.createElement('div')
|
|
22
22
|
const counter = signal(0)
|
|
23
23
|
const { isDragging } = useDraggable({
|
|
24
24
|
element: () => el,
|
|
@@ -27,34 +27,34 @@ describe("useDraggable", () => {
|
|
|
27
27
|
expect(isDragging()).toBe(false)
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
it(
|
|
31
|
-
const { useDraggable } = await import(
|
|
32
|
-
const el = document.createElement(
|
|
30
|
+
it('accepts disabled as boolean', async () => {
|
|
31
|
+
const { useDraggable } = await import('../use-draggable')
|
|
32
|
+
const el = document.createElement('div')
|
|
33
33
|
const { isDragging } = useDraggable({
|
|
34
34
|
element: () => el,
|
|
35
|
-
data: { id:
|
|
35
|
+
data: { id: '1' },
|
|
36
36
|
disabled: true,
|
|
37
37
|
})
|
|
38
38
|
expect(isDragging()).toBe(false)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it(
|
|
42
|
-
const { useDraggable } = await import(
|
|
43
|
-
const el = document.createElement(
|
|
41
|
+
it('accepts disabled as reactive getter', async () => {
|
|
42
|
+
const { useDraggable } = await import('../use-draggable')
|
|
43
|
+
const el = document.createElement('div')
|
|
44
44
|
const disabled = signal(false)
|
|
45
45
|
const { isDragging } = useDraggable({
|
|
46
46
|
element: () => el,
|
|
47
|
-
data: { id:
|
|
47
|
+
data: { id: '1' },
|
|
48
48
|
disabled,
|
|
49
49
|
})
|
|
50
50
|
expect(isDragging()).toBe(false)
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
it(
|
|
54
|
-
const { useDraggable } = await import(
|
|
53
|
+
it('handles null element gracefully', async () => {
|
|
54
|
+
const { useDraggable } = await import('../use-draggable')
|
|
55
55
|
const { isDragging } = useDraggable({
|
|
56
56
|
element: () => null,
|
|
57
|
-
data: { id:
|
|
57
|
+
data: { id: '1' },
|
|
58
58
|
})
|
|
59
59
|
expect(isDragging()).toBe(false)
|
|
60
60
|
})
|
|
@@ -62,32 +62,32 @@ describe("useDraggable", () => {
|
|
|
62
62
|
|
|
63
63
|
// ─── useDroppable ───────────────────────────────────────────────────────────
|
|
64
64
|
|
|
65
|
-
describe(
|
|
66
|
-
it(
|
|
67
|
-
const { useDroppable } = await import(
|
|
68
|
-
expect(typeof useDroppable).toBe(
|
|
65
|
+
describe('useDroppable', () => {
|
|
66
|
+
it('exports useDroppable', async () => {
|
|
67
|
+
const { useDroppable } = await import('../index')
|
|
68
|
+
expect(typeof useDroppable).toBe('function')
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
it(
|
|
72
|
-
const { useDroppable } = await import(
|
|
73
|
-
const el = document.createElement(
|
|
71
|
+
it('returns isOver signal initialized to false', async () => {
|
|
72
|
+
const { useDroppable } = await import('../use-droppable')
|
|
73
|
+
const el = document.createElement('div')
|
|
74
74
|
const { isOver } = useDroppable({ element: () => el, onDrop: () => {} })
|
|
75
75
|
expect(isOver()).toBe(false)
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
it(
|
|
79
|
-
const { useDroppable } = await import(
|
|
80
|
-
const el = document.createElement(
|
|
78
|
+
it('accepts canDrop filter', async () => {
|
|
79
|
+
const { useDroppable } = await import('../use-droppable')
|
|
80
|
+
const el = document.createElement('div')
|
|
81
81
|
const { isOver } = useDroppable({
|
|
82
82
|
element: () => el,
|
|
83
|
-
canDrop: (data) => data.type ===
|
|
83
|
+
canDrop: (data) => data.type === 'card',
|
|
84
84
|
onDrop: () => {},
|
|
85
85
|
})
|
|
86
86
|
expect(isOver()).toBe(false)
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
-
it(
|
|
90
|
-
const { useDroppable } = await import(
|
|
89
|
+
it('handles null element gracefully', async () => {
|
|
90
|
+
const { useDroppable } = await import('../use-droppable')
|
|
91
91
|
const { isOver } = useDroppable({ element: () => null, onDrop: () => {} })
|
|
92
92
|
expect(isOver()).toBe(false)
|
|
93
93
|
})
|
|
@@ -95,18 +95,18 @@ describe("useDroppable", () => {
|
|
|
95
95
|
|
|
96
96
|
// ─── useSortable ────────────────────────────────────────────────────────────
|
|
97
97
|
|
|
98
|
-
describe(
|
|
99
|
-
it(
|
|
100
|
-
const { useSortable } = await import(
|
|
101
|
-
expect(typeof useSortable).toBe(
|
|
98
|
+
describe('useSortable', () => {
|
|
99
|
+
it('exports useSortable', async () => {
|
|
100
|
+
const { useSortable } = await import('../index')
|
|
101
|
+
expect(typeof useSortable).toBe('function')
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
-
it(
|
|
105
|
-
const { useSortable } = await import(
|
|
104
|
+
it('returns full sortable API with overEdge', async () => {
|
|
105
|
+
const { useSortable } = await import('../use-sortable')
|
|
106
106
|
const items = signal([
|
|
107
|
-
{ id:
|
|
108
|
-
{ id:
|
|
109
|
-
{ id:
|
|
107
|
+
{ id: '1', name: 'A' },
|
|
108
|
+
{ id: '2', name: 'B' },
|
|
109
|
+
{ id: '3', name: 'C' },
|
|
110
110
|
])
|
|
111
111
|
|
|
112
112
|
const result = useSortable({
|
|
@@ -115,16 +115,16 @@ describe("useSortable", () => {
|
|
|
115
115
|
onReorder: (newItems) => items.set(newItems),
|
|
116
116
|
})
|
|
117
117
|
|
|
118
|
-
expect(typeof result.containerRef).toBe(
|
|
119
|
-
expect(typeof result.itemRef).toBe(
|
|
118
|
+
expect(typeof result.containerRef).toBe('function')
|
|
119
|
+
expect(typeof result.itemRef).toBe('function')
|
|
120
120
|
expect(result.activeId()).toBeNull()
|
|
121
121
|
expect(result.overId()).toBeNull()
|
|
122
122
|
expect(result.overEdge()).toBeNull()
|
|
123
123
|
})
|
|
124
124
|
|
|
125
|
-
it(
|
|
126
|
-
const { useSortable } = await import(
|
|
127
|
-
const items = signal([{ id:
|
|
125
|
+
it('itemRef returns a ref callback per key', async () => {
|
|
126
|
+
const { useSortable } = await import('../use-sortable')
|
|
127
|
+
const items = signal([{ id: '1' }, { id: '2' }])
|
|
128
128
|
|
|
129
129
|
const { itemRef } = useSortable({
|
|
130
130
|
items,
|
|
@@ -132,16 +132,16 @@ describe("useSortable", () => {
|
|
|
132
132
|
onReorder: () => {},
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
-
const ref1 = itemRef(
|
|
136
|
-
const ref2 = itemRef(
|
|
137
|
-
expect(typeof ref1).toBe(
|
|
138
|
-
expect(typeof ref2).toBe(
|
|
135
|
+
const ref1 = itemRef('1')
|
|
136
|
+
const ref2 = itemRef('2')
|
|
137
|
+
expect(typeof ref1).toBe('function')
|
|
138
|
+
expect(typeof ref2).toBe('function')
|
|
139
139
|
expect(ref1).not.toBe(ref2)
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
-
it(
|
|
143
|
-
const { useSortable } = await import(
|
|
144
|
-
const items = signal([{ id:
|
|
142
|
+
it('itemRef sets accessibility attributes on elements', async () => {
|
|
143
|
+
const { useSortable } = await import('../use-sortable')
|
|
144
|
+
const items = signal([{ id: '1' }])
|
|
145
145
|
|
|
146
146
|
const { itemRef } = useSortable({
|
|
147
147
|
items,
|
|
@@ -149,18 +149,18 @@ describe("useSortable", () => {
|
|
|
149
149
|
onReorder: () => {},
|
|
150
150
|
})
|
|
151
151
|
|
|
152
|
-
const el = document.createElement(
|
|
153
|
-
itemRef(
|
|
152
|
+
const el = document.createElement('div')
|
|
153
|
+
itemRef('1')(el)
|
|
154
154
|
|
|
155
|
-
expect(el.getAttribute(
|
|
156
|
-
expect(el.getAttribute(
|
|
157
|
-
expect(el.getAttribute(
|
|
158
|
-
expect(el.dataset.pyreonSortKey).toBe(
|
|
155
|
+
expect(el.getAttribute('role')).toBe('listitem')
|
|
156
|
+
expect(el.getAttribute('aria-roledescription')).toBe('sortable item')
|
|
157
|
+
expect(el.getAttribute('tabindex')).toBe('0')
|
|
158
|
+
expect(el.dataset.pyreonSortKey).toBe('1')
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
it(
|
|
162
|
-
const { useSortable } = await import(
|
|
163
|
-
const items = signal([{ id:
|
|
161
|
+
it('does not override existing tabindex', async () => {
|
|
162
|
+
const { useSortable } = await import('../use-sortable')
|
|
163
|
+
const items = signal([{ id: '1' }])
|
|
164
164
|
|
|
165
165
|
const { itemRef } = useSortable({
|
|
166
166
|
items,
|
|
@@ -168,22 +168,22 @@ describe("useSortable", () => {
|
|
|
168
168
|
onReorder: () => {},
|
|
169
169
|
})
|
|
170
170
|
|
|
171
|
-
const el = document.createElement(
|
|
172
|
-
el.setAttribute(
|
|
173
|
-
itemRef(
|
|
171
|
+
const el = document.createElement('div')
|
|
172
|
+
el.setAttribute('tabindex', '-1')
|
|
173
|
+
itemRef('1')(el)
|
|
174
174
|
|
|
175
|
-
expect(el.getAttribute(
|
|
175
|
+
expect(el.getAttribute('tabindex')).toBe('-1')
|
|
176
176
|
})
|
|
177
177
|
|
|
178
|
-
it(
|
|
179
|
-
const { useSortable } = await import(
|
|
180
|
-
const items = signal([{ id:
|
|
178
|
+
it('supports horizontal axis', async () => {
|
|
179
|
+
const { useSortable } = await import('../use-sortable')
|
|
180
|
+
const items = signal([{ id: '1' }, { id: '2' }])
|
|
181
181
|
|
|
182
182
|
const result = useSortable({
|
|
183
183
|
items,
|
|
184
184
|
by: (item) => item.id,
|
|
185
185
|
onReorder: () => {},
|
|
186
|
-
axis:
|
|
186
|
+
axis: 'horizontal',
|
|
187
187
|
})
|
|
188
188
|
|
|
189
189
|
expect(result.activeId()).toBeNull()
|
|
@@ -192,15 +192,15 @@ describe("useSortable", () => {
|
|
|
192
192
|
|
|
193
193
|
// ─── useFileDrop ────────────────────────────────────────────────────────────
|
|
194
194
|
|
|
195
|
-
describe(
|
|
196
|
-
it(
|
|
197
|
-
const { useFileDrop } = await import(
|
|
198
|
-
expect(typeof useFileDrop).toBe(
|
|
195
|
+
describe('useFileDrop', () => {
|
|
196
|
+
it('exports useFileDrop', async () => {
|
|
197
|
+
const { useFileDrop } = await import('../index')
|
|
198
|
+
expect(typeof useFileDrop).toBe('function')
|
|
199
199
|
})
|
|
200
200
|
|
|
201
|
-
it(
|
|
202
|
-
const { useFileDrop } = await import(
|
|
203
|
-
const el = document.createElement(
|
|
201
|
+
it('returns isOver and isDraggingFiles signals', async () => {
|
|
202
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
203
|
+
const el = document.createElement('div')
|
|
204
204
|
const { isOver, isDraggingFiles } = useFileDrop({
|
|
205
205
|
element: () => el,
|
|
206
206
|
onDrop: () => {},
|
|
@@ -209,21 +209,21 @@ describe("useFileDrop", () => {
|
|
|
209
209
|
expect(isDraggingFiles()).toBe(false)
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
-
it(
|
|
213
|
-
const { useFileDrop } = await import(
|
|
214
|
-
const el = document.createElement(
|
|
212
|
+
it('accepts accept filter and maxFiles', async () => {
|
|
213
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
214
|
+
const el = document.createElement('div')
|
|
215
215
|
const { isOver } = useFileDrop({
|
|
216
216
|
element: () => el,
|
|
217
|
-
accept: [
|
|
217
|
+
accept: ['image/*', '.pdf'],
|
|
218
218
|
maxFiles: 5,
|
|
219
219
|
onDrop: () => {},
|
|
220
220
|
})
|
|
221
221
|
expect(isOver()).toBe(false)
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
-
it(
|
|
225
|
-
const { useFileDrop } = await import(
|
|
226
|
-
const el = document.createElement(
|
|
224
|
+
it('accepts disabled option', async () => {
|
|
225
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
226
|
+
const el = document.createElement('div')
|
|
227
227
|
const disabled = signal(true)
|
|
228
228
|
const { isOver } = useFileDrop({
|
|
229
229
|
element: () => el,
|
|
@@ -236,29 +236,29 @@ describe("useFileDrop", () => {
|
|
|
236
236
|
|
|
237
237
|
// ─── useDragMonitor ─────────────────────────────────────────────────────────
|
|
238
238
|
|
|
239
|
-
describe(
|
|
240
|
-
it(
|
|
241
|
-
const { useDragMonitor } = await import(
|
|
242
|
-
expect(typeof useDragMonitor).toBe(
|
|
239
|
+
describe('useDragMonitor', () => {
|
|
240
|
+
it('exports useDragMonitor', async () => {
|
|
241
|
+
const { useDragMonitor } = await import('../index')
|
|
242
|
+
expect(typeof useDragMonitor).toBe('function')
|
|
243
243
|
})
|
|
244
244
|
|
|
245
|
-
it(
|
|
246
|
-
const { useDragMonitor } = await import(
|
|
245
|
+
it('returns isDragging and dragData signals', async () => {
|
|
246
|
+
const { useDragMonitor } = await import('../use-drag-monitor')
|
|
247
247
|
const { isDragging, dragData } = useDragMonitor()
|
|
248
248
|
expect(isDragging()).toBe(false)
|
|
249
249
|
expect(dragData()).toBeNull()
|
|
250
250
|
})
|
|
251
251
|
|
|
252
|
-
it(
|
|
253
|
-
const { useDragMonitor } = await import(
|
|
252
|
+
it('accepts canMonitor filter', async () => {
|
|
253
|
+
const { useDragMonitor } = await import('../use-drag-monitor')
|
|
254
254
|
const { isDragging } = useDragMonitor({
|
|
255
|
-
canMonitor: (data) => data.type ===
|
|
255
|
+
canMonitor: (data) => data.type === 'card',
|
|
256
256
|
})
|
|
257
257
|
expect(isDragging()).toBe(false)
|
|
258
258
|
})
|
|
259
259
|
|
|
260
|
-
it(
|
|
261
|
-
const { useDragMonitor } = await import(
|
|
260
|
+
it('accepts onDragStart and onDrop callbacks', async () => {
|
|
261
|
+
const { useDragMonitor } = await import('../use-drag-monitor')
|
|
262
262
|
const { isDragging } = useDragMonitor({
|
|
263
263
|
onDragStart: () => {},
|
|
264
264
|
onDrop: () => {},
|
|
@@ -269,10 +269,10 @@ describe("useDragMonitor", () => {
|
|
|
269
269
|
|
|
270
270
|
// ─── useSortable: containerRef and auto-scroll ────────────────────────────
|
|
271
271
|
|
|
272
|
-
describe(
|
|
273
|
-
it(
|
|
274
|
-
const { useSortable } = await import(
|
|
275
|
-
const items = signal([{ id:
|
|
272
|
+
describe('useSortable containerRef', () => {
|
|
273
|
+
it('containerRef registers auto-scroll and drop target on element', async () => {
|
|
274
|
+
const { useSortable } = await import('../use-sortable')
|
|
275
|
+
const items = signal([{ id: '1' }, { id: '2' }, { id: '3' }])
|
|
276
276
|
|
|
277
277
|
const { containerRef } = useSortable({
|
|
278
278
|
items,
|
|
@@ -280,14 +280,14 @@ describe("useSortable containerRef", () => {
|
|
|
280
280
|
onReorder: () => {},
|
|
281
281
|
})
|
|
282
282
|
|
|
283
|
-
const container = document.createElement(
|
|
283
|
+
const container = document.createElement('ul')
|
|
284
284
|
// Should not throw — sets up autoScrollForElements + dropTargetForElements
|
|
285
285
|
expect(() => containerRef(container)).not.toThrow()
|
|
286
286
|
})
|
|
287
287
|
|
|
288
|
-
it(
|
|
289
|
-
const { useSortable } = await import(
|
|
290
|
-
const items = signal([{ id:
|
|
288
|
+
it('containerRef adds keydown event listener to element', async () => {
|
|
289
|
+
const { useSortable } = await import('../use-sortable')
|
|
290
|
+
const items = signal([{ id: '1' }, { id: '2' }])
|
|
291
291
|
|
|
292
292
|
const { containerRef } = useSortable({
|
|
293
293
|
items,
|
|
@@ -295,21 +295,21 @@ describe("useSortable containerRef", () => {
|
|
|
295
295
|
onReorder: () => {},
|
|
296
296
|
})
|
|
297
297
|
|
|
298
|
-
const container = document.createElement(
|
|
299
|
-
const spy = vi.spyOn(container,
|
|
298
|
+
const container = document.createElement('ul')
|
|
299
|
+
const spy = vi.spyOn(container, 'addEventListener')
|
|
300
300
|
containerRef(container)
|
|
301
301
|
|
|
302
|
-
expect(spy).toHaveBeenCalledWith(
|
|
302
|
+
expect(spy).toHaveBeenCalledWith('keydown', expect.any(Function))
|
|
303
303
|
spy.mockRestore()
|
|
304
304
|
})
|
|
305
305
|
})
|
|
306
306
|
|
|
307
307
|
// ─── useSortable: overEdge signal ─────────────────────────────────────────
|
|
308
308
|
|
|
309
|
-
describe(
|
|
310
|
-
it(
|
|
311
|
-
const { useSortable } = await import(
|
|
312
|
-
const items = signal([{ id:
|
|
309
|
+
describe('useSortable overEdge', () => {
|
|
310
|
+
it('overEdge signal is initially null', async () => {
|
|
311
|
+
const { useSortable } = await import('../use-sortable')
|
|
312
|
+
const items = signal([{ id: '1' }, { id: '2' }])
|
|
313
313
|
|
|
314
314
|
const { overEdge } = useSortable({
|
|
315
315
|
items,
|
|
@@ -320,9 +320,9 @@ describe("useSortable overEdge", () => {
|
|
|
320
320
|
expect(overEdge()).toBeNull()
|
|
321
321
|
})
|
|
322
322
|
|
|
323
|
-
it(
|
|
324
|
-
const { useSortable } = await import(
|
|
325
|
-
const items = signal([{ id:
|
|
323
|
+
it('overEdge is part of the returned API alongside activeId and overId', async () => {
|
|
324
|
+
const { useSortable } = await import('../use-sortable')
|
|
325
|
+
const items = signal([{ id: 'a' }, { id: 'b' }])
|
|
326
326
|
|
|
327
327
|
const result = useSortable({
|
|
328
328
|
items,
|
|
@@ -330,21 +330,21 @@ describe("useSortable overEdge", () => {
|
|
|
330
330
|
onReorder: () => {},
|
|
331
331
|
})
|
|
332
332
|
|
|
333
|
-
expect(result).toHaveProperty(
|
|
334
|
-
expect(result).toHaveProperty(
|
|
335
|
-
expect(result).toHaveProperty(
|
|
336
|
-
expect(result).toHaveProperty(
|
|
337
|
-
expect(result).toHaveProperty(
|
|
333
|
+
expect(result).toHaveProperty('overEdge')
|
|
334
|
+
expect(result).toHaveProperty('activeId')
|
|
335
|
+
expect(result).toHaveProperty('overId')
|
|
336
|
+
expect(result).toHaveProperty('containerRef')
|
|
337
|
+
expect(result).toHaveProperty('itemRef')
|
|
338
338
|
})
|
|
339
339
|
})
|
|
340
340
|
|
|
341
341
|
// ─── useSortable: keyboard handler ─────────────────────────────────────────
|
|
342
342
|
|
|
343
|
-
describe(
|
|
344
|
-
it(
|
|
345
|
-
const { useSortable } = await import(
|
|
343
|
+
describe('useSortable keyboard reordering', () => {
|
|
344
|
+
it('Alt+ArrowDown swaps focused item with next (vertical axis)', async () => {
|
|
345
|
+
const { useSortable } = await import('../use-sortable')
|
|
346
346
|
const reordered: { id: string }[][] = []
|
|
347
|
-
const items = signal([{ id:
|
|
347
|
+
const items = signal([{ id: '1' }, { id: '2' }, { id: '3' }])
|
|
348
348
|
|
|
349
349
|
const { containerRef, itemRef } = useSortable({
|
|
350
350
|
items,
|
|
@@ -353,16 +353,16 @@ describe("useSortable keyboard reordering", () => {
|
|
|
353
353
|
})
|
|
354
354
|
|
|
355
355
|
// Build a container with items
|
|
356
|
-
const container = document.createElement(
|
|
356
|
+
const container = document.createElement('ul')
|
|
357
357
|
document.body.appendChild(container)
|
|
358
358
|
containerRef(container)
|
|
359
359
|
|
|
360
|
-
const li1 = document.createElement(
|
|
361
|
-
const li2 = document.createElement(
|
|
362
|
-
const li3 = document.createElement(
|
|
363
|
-
itemRef(
|
|
364
|
-
itemRef(
|
|
365
|
-
itemRef(
|
|
360
|
+
const li1 = document.createElement('li')
|
|
361
|
+
const li2 = document.createElement('li')
|
|
362
|
+
const li3 = document.createElement('li')
|
|
363
|
+
itemRef('1')(li1)
|
|
364
|
+
itemRef('2')(li2)
|
|
365
|
+
itemRef('3')(li3)
|
|
366
366
|
container.appendChild(li1)
|
|
367
367
|
container.appendChild(li2)
|
|
368
368
|
container.appendChild(li3)
|
|
@@ -371,23 +371,23 @@ describe("useSortable keyboard reordering", () => {
|
|
|
371
371
|
li1.focus()
|
|
372
372
|
|
|
373
373
|
// Dispatch Alt+ArrowDown
|
|
374
|
-
const event = new KeyboardEvent(
|
|
375
|
-
key:
|
|
374
|
+
const event = new KeyboardEvent('keydown', {
|
|
375
|
+
key: 'ArrowDown',
|
|
376
376
|
altKey: true,
|
|
377
377
|
bubbles: true,
|
|
378
378
|
})
|
|
379
379
|
container.dispatchEvent(event)
|
|
380
380
|
|
|
381
381
|
expect(reordered.length).toBe(1)
|
|
382
|
-
expect(reordered[0]!.map((i) => i.id)).toEqual([
|
|
382
|
+
expect(reordered[0]!.map((i) => i.id)).toEqual(['2', '1', '3'])
|
|
383
383
|
|
|
384
384
|
document.body.removeChild(container)
|
|
385
385
|
})
|
|
386
386
|
|
|
387
|
-
it(
|
|
388
|
-
const { useSortable } = await import(
|
|
387
|
+
it('Alt+ArrowUp swaps focused item with previous (vertical axis)', async () => {
|
|
388
|
+
const { useSortable } = await import('../use-sortable')
|
|
389
389
|
const reordered: { id: string }[][] = []
|
|
390
|
-
const items = signal([{ id:
|
|
390
|
+
const items = signal([{ id: '1' }, { id: '2' }, { id: '3' }])
|
|
391
391
|
|
|
392
392
|
const { containerRef, itemRef } = useSortable({
|
|
393
393
|
items,
|
|
@@ -395,16 +395,16 @@ describe("useSortable keyboard reordering", () => {
|
|
|
395
395
|
onReorder: (newItems) => reordered.push(newItems),
|
|
396
396
|
})
|
|
397
397
|
|
|
398
|
-
const container = document.createElement(
|
|
398
|
+
const container = document.createElement('ul')
|
|
399
399
|
document.body.appendChild(container)
|
|
400
400
|
containerRef(container)
|
|
401
401
|
|
|
402
|
-
const li1 = document.createElement(
|
|
403
|
-
const li2 = document.createElement(
|
|
404
|
-
const li3 = document.createElement(
|
|
405
|
-
itemRef(
|
|
406
|
-
itemRef(
|
|
407
|
-
itemRef(
|
|
402
|
+
const li1 = document.createElement('li')
|
|
403
|
+
const li2 = document.createElement('li')
|
|
404
|
+
const li3 = document.createElement('li')
|
|
405
|
+
itemRef('1')(li1)
|
|
406
|
+
itemRef('2')(li2)
|
|
407
|
+
itemRef('3')(li3)
|
|
408
408
|
container.appendChild(li1)
|
|
409
409
|
container.appendChild(li2)
|
|
410
410
|
container.appendChild(li3)
|
|
@@ -412,23 +412,23 @@ describe("useSortable keyboard reordering", () => {
|
|
|
412
412
|
// Focus the second item
|
|
413
413
|
li2.focus()
|
|
414
414
|
|
|
415
|
-
const event = new KeyboardEvent(
|
|
416
|
-
key:
|
|
415
|
+
const event = new KeyboardEvent('keydown', {
|
|
416
|
+
key: 'ArrowUp',
|
|
417
417
|
altKey: true,
|
|
418
418
|
bubbles: true,
|
|
419
419
|
})
|
|
420
420
|
container.dispatchEvent(event)
|
|
421
421
|
|
|
422
422
|
expect(reordered.length).toBe(1)
|
|
423
|
-
expect(reordered[0]!.map((i) => i.id)).toEqual([
|
|
423
|
+
expect(reordered[0]!.map((i) => i.id)).toEqual(['2', '1', '3'])
|
|
424
424
|
|
|
425
425
|
document.body.removeChild(container)
|
|
426
426
|
})
|
|
427
427
|
|
|
428
|
-
it(
|
|
429
|
-
const { useSortable } = await import(
|
|
428
|
+
it('ignores keyboard events without Alt key', async () => {
|
|
429
|
+
const { useSortable } = await import('../use-sortable')
|
|
430
430
|
const reordered: { id: string }[][] = []
|
|
431
|
-
const items = signal([{ id:
|
|
431
|
+
const items = signal([{ id: '1' }, { id: '2' }])
|
|
432
432
|
|
|
433
433
|
const { containerRef, itemRef } = useSortable({
|
|
434
434
|
items,
|
|
@@ -436,21 +436,21 @@ describe("useSortable keyboard reordering", () => {
|
|
|
436
436
|
onReorder: (newItems) => reordered.push(newItems),
|
|
437
437
|
})
|
|
438
438
|
|
|
439
|
-
const container = document.createElement(
|
|
439
|
+
const container = document.createElement('ul')
|
|
440
440
|
document.body.appendChild(container)
|
|
441
441
|
containerRef(container)
|
|
442
442
|
|
|
443
|
-
const li1 = document.createElement(
|
|
444
|
-
const li2 = document.createElement(
|
|
445
|
-
itemRef(
|
|
446
|
-
itemRef(
|
|
443
|
+
const li1 = document.createElement('li')
|
|
444
|
+
const li2 = document.createElement('li')
|
|
445
|
+
itemRef('1')(li1)
|
|
446
|
+
itemRef('2')(li2)
|
|
447
447
|
container.appendChild(li1)
|
|
448
448
|
container.appendChild(li2)
|
|
449
449
|
|
|
450
450
|
li1.focus()
|
|
451
451
|
|
|
452
|
-
const event = new KeyboardEvent(
|
|
453
|
-
key:
|
|
452
|
+
const event = new KeyboardEvent('keydown', {
|
|
453
|
+
key: 'ArrowDown',
|
|
454
454
|
altKey: false,
|
|
455
455
|
bubbles: true,
|
|
456
456
|
})
|
|
@@ -461,10 +461,10 @@ describe("useSortable keyboard reordering", () => {
|
|
|
461
461
|
document.body.removeChild(container)
|
|
462
462
|
})
|
|
463
463
|
|
|
464
|
-
it(
|
|
465
|
-
const { useSortable } = await import(
|
|
464
|
+
it('ignores Alt+ArrowDown at the last item (boundary)', async () => {
|
|
465
|
+
const { useSortable } = await import('../use-sortable')
|
|
466
466
|
const reordered: { id: string }[][] = []
|
|
467
|
-
const items = signal([{ id:
|
|
467
|
+
const items = signal([{ id: '1' }, { id: '2' }])
|
|
468
468
|
|
|
469
469
|
const { containerRef, itemRef } = useSortable({
|
|
470
470
|
items,
|
|
@@ -472,22 +472,22 @@ describe("useSortable keyboard reordering", () => {
|
|
|
472
472
|
onReorder: (newItems) => reordered.push(newItems),
|
|
473
473
|
})
|
|
474
474
|
|
|
475
|
-
const container = document.createElement(
|
|
475
|
+
const container = document.createElement('ul')
|
|
476
476
|
document.body.appendChild(container)
|
|
477
477
|
containerRef(container)
|
|
478
478
|
|
|
479
|
-
const li1 = document.createElement(
|
|
480
|
-
const li2 = document.createElement(
|
|
481
|
-
itemRef(
|
|
482
|
-
itemRef(
|
|
479
|
+
const li1 = document.createElement('li')
|
|
480
|
+
const li2 = document.createElement('li')
|
|
481
|
+
itemRef('1')(li1)
|
|
482
|
+
itemRef('2')(li2)
|
|
483
483
|
container.appendChild(li1)
|
|
484
484
|
container.appendChild(li2)
|
|
485
485
|
|
|
486
486
|
// Focus last item
|
|
487
487
|
li2.focus()
|
|
488
488
|
|
|
489
|
-
const event = new KeyboardEvent(
|
|
490
|
-
key:
|
|
489
|
+
const event = new KeyboardEvent('keydown', {
|
|
490
|
+
key: 'ArrowDown',
|
|
491
491
|
altKey: true,
|
|
492
492
|
bubbles: true,
|
|
493
493
|
})
|
|
@@ -498,28 +498,28 @@ describe("useSortable keyboard reordering", () => {
|
|
|
498
498
|
document.body.removeChild(container)
|
|
499
499
|
})
|
|
500
500
|
|
|
501
|
-
it(
|
|
502
|
-
const { useSortable } = await import(
|
|
501
|
+
it('horizontal axis uses ArrowLeft/ArrowRight', async () => {
|
|
502
|
+
const { useSortable } = await import('../use-sortable')
|
|
503
503
|
const reordered: { id: string }[][] = []
|
|
504
|
-
const items = signal([{ id:
|
|
504
|
+
const items = signal([{ id: '1' }, { id: '2' }, { id: '3' }])
|
|
505
505
|
|
|
506
506
|
const { containerRef, itemRef } = useSortable({
|
|
507
507
|
items,
|
|
508
508
|
by: (item) => item.id,
|
|
509
509
|
onReorder: (newItems) => reordered.push(newItems),
|
|
510
|
-
axis:
|
|
510
|
+
axis: 'horizontal',
|
|
511
511
|
})
|
|
512
512
|
|
|
513
|
-
const container = document.createElement(
|
|
513
|
+
const container = document.createElement('div')
|
|
514
514
|
document.body.appendChild(container)
|
|
515
515
|
containerRef(container)
|
|
516
516
|
|
|
517
|
-
const el1 = document.createElement(
|
|
518
|
-
const el2 = document.createElement(
|
|
519
|
-
const el3 = document.createElement(
|
|
520
|
-
itemRef(
|
|
521
|
-
itemRef(
|
|
522
|
-
itemRef(
|
|
517
|
+
const el1 = document.createElement('div')
|
|
518
|
+
const el2 = document.createElement('div')
|
|
519
|
+
const el3 = document.createElement('div')
|
|
520
|
+
itemRef('1')(el1)
|
|
521
|
+
itemRef('2')(el2)
|
|
522
|
+
itemRef('3')(el3)
|
|
523
523
|
container.appendChild(el1)
|
|
524
524
|
container.appendChild(el2)
|
|
525
525
|
container.appendChild(el3)
|
|
@@ -528,16 +528,16 @@ describe("useSortable keyboard reordering", () => {
|
|
|
528
528
|
|
|
529
529
|
// ArrowDown should be ignored in horizontal mode
|
|
530
530
|
container.dispatchEvent(
|
|
531
|
-
new KeyboardEvent(
|
|
531
|
+
new KeyboardEvent('keydown', { key: 'ArrowDown', altKey: true, bubbles: true }),
|
|
532
532
|
)
|
|
533
533
|
expect(reordered.length).toBe(0)
|
|
534
534
|
|
|
535
535
|
// ArrowRight should work
|
|
536
536
|
container.dispatchEvent(
|
|
537
|
-
new KeyboardEvent(
|
|
537
|
+
new KeyboardEvent('keydown', { key: 'ArrowRight', altKey: true, bubbles: true }),
|
|
538
538
|
)
|
|
539
539
|
expect(reordered.length).toBe(1)
|
|
540
|
-
expect(reordered[0]!.map((i) => i.id)).toEqual([
|
|
540
|
+
expect(reordered[0]!.map((i) => i.id)).toEqual(['2', '1', '3'])
|
|
541
541
|
|
|
542
542
|
document.body.removeChild(container)
|
|
543
543
|
})
|
|
@@ -545,56 +545,56 @@ describe("useSortable keyboard reordering", () => {
|
|
|
545
545
|
|
|
546
546
|
// ─── useFileDrop: MIME type filtering ──────────────────────────────────────
|
|
547
547
|
|
|
548
|
-
describe(
|
|
549
|
-
it(
|
|
548
|
+
describe('useFileDrop MIME filtering logic', () => {
|
|
549
|
+
it('matchesAccept handles extension patterns (.pdf)', async () => {
|
|
550
550
|
// We can't test matchesAccept directly since it's private,
|
|
551
551
|
// but we can verify the useFileDrop options are accepted
|
|
552
|
-
const { useFileDrop } = await import(
|
|
553
|
-
const el = document.createElement(
|
|
552
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
553
|
+
const el = document.createElement('div')
|
|
554
554
|
const { isOver } = useFileDrop({
|
|
555
555
|
element: () => el,
|
|
556
|
-
accept: [
|
|
556
|
+
accept: ['.pdf', '.doc', '.docx'],
|
|
557
557
|
onDrop: () => {},
|
|
558
558
|
})
|
|
559
559
|
expect(isOver()).toBe(false)
|
|
560
560
|
})
|
|
561
561
|
|
|
562
|
-
it(
|
|
563
|
-
const { useFileDrop } = await import(
|
|
564
|
-
const el = document.createElement(
|
|
562
|
+
it('matchesAccept handles wildcard MIME types (image/*)', async () => {
|
|
563
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
564
|
+
const el = document.createElement('div')
|
|
565
565
|
const { isOver } = useFileDrop({
|
|
566
566
|
element: () => el,
|
|
567
|
-
accept: [
|
|
567
|
+
accept: ['image/*'],
|
|
568
568
|
onDrop: () => {},
|
|
569
569
|
})
|
|
570
570
|
expect(isOver()).toBe(false)
|
|
571
571
|
})
|
|
572
572
|
|
|
573
|
-
it(
|
|
574
|
-
const { useFileDrop } = await import(
|
|
575
|
-
const el = document.createElement(
|
|
573
|
+
it('matchesAccept handles exact MIME types (application/json)', async () => {
|
|
574
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
575
|
+
const el = document.createElement('div')
|
|
576
576
|
const { isOver } = useFileDrop({
|
|
577
577
|
element: () => el,
|
|
578
|
-
accept: [
|
|
578
|
+
accept: ['application/json', 'text/plain'],
|
|
579
579
|
onDrop: () => {},
|
|
580
580
|
})
|
|
581
581
|
expect(isOver()).toBe(false)
|
|
582
582
|
})
|
|
583
583
|
|
|
584
|
-
it(
|
|
585
|
-
const { useFileDrop } = await import(
|
|
586
|
-
const el = document.createElement(
|
|
584
|
+
it('handles maxFiles option', async () => {
|
|
585
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
586
|
+
const el = document.createElement('div')
|
|
587
587
|
const { isOver } = useFileDrop({
|
|
588
588
|
element: () => el,
|
|
589
|
-
accept: [
|
|
589
|
+
accept: ['image/*'],
|
|
590
590
|
maxFiles: 1,
|
|
591
591
|
onDrop: () => {},
|
|
592
592
|
})
|
|
593
593
|
expect(isOver()).toBe(false)
|
|
594
594
|
})
|
|
595
595
|
|
|
596
|
-
it(
|
|
597
|
-
const { useFileDrop } = await import(
|
|
596
|
+
it('handles null element gracefully', async () => {
|
|
597
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
598
598
|
const { isOver, isDraggingFiles } = useFileDrop({
|
|
599
599
|
element: () => null,
|
|
600
600
|
onDrop: () => {},
|
|
@@ -603,9 +603,9 @@ describe("useFileDrop MIME filtering logic", () => {
|
|
|
603
603
|
expect(isDraggingFiles()).toBe(false)
|
|
604
604
|
})
|
|
605
605
|
|
|
606
|
-
it(
|
|
607
|
-
const { useFileDrop } = await import(
|
|
608
|
-
const el = document.createElement(
|
|
606
|
+
it('accepts disabled as reactive getter', async () => {
|
|
607
|
+
const { useFileDrop } = await import('../use-file-drop')
|
|
608
|
+
const el = document.createElement('div')
|
|
609
609
|
const isDisabled = signal(false)
|
|
610
610
|
const { isOver } = useFileDrop({
|
|
611
611
|
element: () => el,
|
|
@@ -618,15 +618,15 @@ describe("useFileDrop MIME filtering logic", () => {
|
|
|
618
618
|
|
|
619
619
|
// ─── useDragMonitor: canMonitor filter ─────────────────────────────────────
|
|
620
620
|
|
|
621
|
-
describe(
|
|
622
|
-
it(
|
|
623
|
-
const { useDragMonitor } = await import(
|
|
621
|
+
describe('useDragMonitor canMonitor filter', () => {
|
|
622
|
+
it('canMonitor option is a function that receives drag data', async () => {
|
|
623
|
+
const { useDragMonitor } = await import('../use-drag-monitor')
|
|
624
624
|
const canMonitorCalls: unknown[] = []
|
|
625
625
|
|
|
626
626
|
const { isDragging } = useDragMonitor({
|
|
627
627
|
canMonitor: (data) => {
|
|
628
628
|
canMonitorCalls.push(data)
|
|
629
|
-
return data.type ===
|
|
629
|
+
return data.type === 'card'
|
|
630
630
|
},
|
|
631
631
|
})
|
|
632
632
|
|
|
@@ -635,21 +635,21 @@ describe("useDragMonitor canMonitor filter", () => {
|
|
|
635
635
|
// We can't trigger real drag events, but verify setup doesn't throw
|
|
636
636
|
})
|
|
637
637
|
|
|
638
|
-
it(
|
|
639
|
-
const { useDragMonitor } = await import(
|
|
638
|
+
it('creates monitor without canMonitor (monitors all drags)', async () => {
|
|
639
|
+
const { useDragMonitor } = await import('../use-drag-monitor')
|
|
640
640
|
const { isDragging, dragData } = useDragMonitor()
|
|
641
641
|
|
|
642
642
|
expect(isDragging()).toBe(false)
|
|
643
643
|
expect(dragData()).toBeNull()
|
|
644
644
|
})
|
|
645
645
|
|
|
646
|
-
it(
|
|
647
|
-
const { useDragMonitor } = await import(
|
|
646
|
+
it('creates monitor with all options', async () => {
|
|
647
|
+
const { useDragMonitor } = await import('../use-drag-monitor')
|
|
648
648
|
const starts: unknown[] = []
|
|
649
649
|
const drops: unknown[] = []
|
|
650
650
|
|
|
651
651
|
const { isDragging } = useDragMonitor({
|
|
652
|
-
canMonitor: (data) => data.type ===
|
|
652
|
+
canMonitor: (data) => data.type === 'task',
|
|
653
653
|
onDragStart: (data) => starts.push(data),
|
|
654
654
|
onDrop: (source, target) => drops.push({ source, target }),
|
|
655
655
|
})
|
|
@@ -661,9 +661,9 @@ describe("useDragMonitor canMonitor filter", () => {
|
|
|
661
661
|
|
|
662
662
|
// ─── Module exports ─────────────────────────────────────────────────────────
|
|
663
663
|
|
|
664
|
-
describe(
|
|
665
|
-
it(
|
|
666
|
-
const mod = await import(
|
|
664
|
+
describe('module exports', () => {
|
|
665
|
+
it('exports all 5 hooks', async () => {
|
|
666
|
+
const mod = await import('../index')
|
|
667
667
|
expect(mod.useDraggable).toBeDefined()
|
|
668
668
|
expect(mod.useDroppable).toBeDefined()
|
|
669
669
|
expect(mod.useSortable).toBeDefined()
|