@noya-app/noya-designsystem 0.1.56 → 0.1.58
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +14 -0
- package/dist/index.d.mts +61 -46
- package/dist/index.d.ts +61 -46
- package/dist/index.js +1150 -1056
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +356 -265
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/validateDropIndicator.test.ts +64 -31
- package/src/components/Collection.tsx +3 -0
- package/src/components/List.tsx +4 -0
- package/src/components/ListView.tsx +7 -0
- package/src/components/sorting/DragRegistration.tsx +3 -6
- package/src/components/sorting/SharedDragProvider.tsx +258 -135
- package/src/components/sorting/Sortable.tsx +54 -108
- package/src/components/sorting/createSharedDrag.tsx +10 -15
- package/src/components/sorting/sorting.ts +48 -44
- package/src/utils/moveTreeItem.ts +6 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
Active,
|
|
2
3
|
closestCenter,
|
|
3
4
|
CollisionDetection,
|
|
4
5
|
DndContext,
|
|
@@ -6,11 +7,15 @@ import {
|
|
|
6
7
|
DragMoveEvent,
|
|
7
8
|
DragOverlay,
|
|
8
9
|
DragStartEvent,
|
|
10
|
+
Over,
|
|
9
11
|
PointerSensor,
|
|
10
12
|
pointerWithin,
|
|
11
13
|
useSensor,
|
|
12
14
|
useSensors,
|
|
13
15
|
} from "@dnd-kit/core";
|
|
16
|
+
import { Point, rectContainsPoint, unionRects } from "@noya-app/noya-geometry";
|
|
17
|
+
import { windowsOf } from "@noya-app/noya-utils";
|
|
18
|
+
import { useStableCallback } from "@noya-app/react-utils";
|
|
14
19
|
import * as React from "react";
|
|
15
20
|
import { useMemo } from "react";
|
|
16
21
|
import { createPortal } from "react-dom";
|
|
@@ -20,19 +25,28 @@ import {
|
|
|
20
25
|
AnyDragContext,
|
|
21
26
|
DragRegistrationContext,
|
|
22
27
|
DragRegistrationContextType,
|
|
23
|
-
|
|
28
|
+
RegisteredList,
|
|
29
|
+
SharedDragProps,
|
|
24
30
|
useDragRegistrationManager,
|
|
25
31
|
} from "./DragRegistration";
|
|
26
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
DragItem,
|
|
34
|
+
DragState,
|
|
35
|
+
MoveDragItemHandler,
|
|
36
|
+
MoveDragItemParameters,
|
|
37
|
+
RelativeDropPosition,
|
|
38
|
+
} from "./sorting";
|
|
27
39
|
|
|
28
40
|
type SharedDragProviderProps = {
|
|
29
41
|
children: React.ReactNode;
|
|
30
|
-
|
|
42
|
+
onMoveItem?: MoveDragItemHandler;
|
|
43
|
+
sharedDragProps?: SharedDragProps;
|
|
31
44
|
};
|
|
32
45
|
|
|
33
46
|
export function SharedDragProvider({
|
|
34
47
|
children,
|
|
35
|
-
|
|
48
|
+
onMoveItem,
|
|
49
|
+
sharedDragProps = {
|
|
36
50
|
dragRegistrationContext: DragRegistrationContext,
|
|
37
51
|
activeDragContext: ActiveDragContext,
|
|
38
52
|
},
|
|
@@ -46,28 +60,19 @@ export function SharedDragProvider({
|
|
|
46
60
|
);
|
|
47
61
|
|
|
48
62
|
const mounted = useMounted();
|
|
49
|
-
const [
|
|
50
|
-
|
|
63
|
+
const [dragState, setDragState] = React.useState<DragState>({
|
|
64
|
+
source: null,
|
|
65
|
+
target: null,
|
|
66
|
+
indicator: null,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// console.log("dragState", dragState.target);
|
|
70
|
+
|
|
51
71
|
const { registerList, unregisterList, registeredLists } =
|
|
52
72
|
useDragRegistrationManager();
|
|
53
73
|
|
|
54
74
|
const activatorEventRef = React.useRef<PointerEvent | null>(null);
|
|
55
75
|
|
|
56
|
-
const acceptsListDrop = React.useCallback(
|
|
57
|
-
(sourceListId: string, targetListId: string) => {
|
|
58
|
-
const sourceList = registeredLists.get(sourceListId);
|
|
59
|
-
const targetList = registeredLists.get(targetListId);
|
|
60
|
-
|
|
61
|
-
if (!sourceList || !targetList) return false;
|
|
62
|
-
|
|
63
|
-
return targetList.acceptsFromList({
|
|
64
|
-
sourceListId,
|
|
65
|
-
targetListId,
|
|
66
|
-
});
|
|
67
|
-
},
|
|
68
|
-
[registeredLists]
|
|
69
|
-
);
|
|
70
|
-
|
|
71
76
|
const handleDragStart = React.useCallback(
|
|
72
77
|
(event: DragStartEvent) => {
|
|
73
78
|
// Capture the activator event for position calculations
|
|
@@ -79,12 +84,16 @@ export function SharedDragProvider({
|
|
|
79
84
|
|
|
80
85
|
if (itemIndex >= 0) {
|
|
81
86
|
const dragItem: DragItem = {
|
|
82
|
-
|
|
87
|
+
itemId: event.active.id,
|
|
83
88
|
listId,
|
|
84
89
|
index: itemIndex,
|
|
85
90
|
};
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
setDragState({
|
|
93
|
+
source: dragItem,
|
|
94
|
+
target: null,
|
|
95
|
+
indicator: null,
|
|
96
|
+
});
|
|
88
97
|
break;
|
|
89
98
|
}
|
|
90
99
|
}
|
|
@@ -92,139 +101,113 @@ export function SharedDragProvider({
|
|
|
92
101
|
[registeredLists]
|
|
93
102
|
);
|
|
94
103
|
|
|
95
|
-
const handleDragMove = React.useCallback(
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
const handleDragMove = React.useCallback(
|
|
105
|
+
(event: DragMoveEvent) => {
|
|
106
|
+
const { active, over } = event;
|
|
107
|
+
|
|
108
|
+
if (!activatorEventRef.current || !dragState.source) return;
|
|
109
|
+
|
|
110
|
+
const dropTarget = findDropTarget({
|
|
111
|
+
activationPoint: {
|
|
112
|
+
x: activatorEventRef.current.clientX,
|
|
113
|
+
y: activatorEventRef.current.clientY,
|
|
114
|
+
},
|
|
115
|
+
delta: { x: event.delta.x, y: event.delta.y },
|
|
116
|
+
over,
|
|
117
|
+
active,
|
|
118
|
+
source: dragState.source,
|
|
119
|
+
registeredLists,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
setDragState({
|
|
123
|
+
source: dragState.source,
|
|
124
|
+
target: dropTarget?.target ?? null,
|
|
125
|
+
indicator: dropTarget?.indicator ?? null,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
[registeredLists, dragState.source]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const stableOnMoveItem = useStableCallback(onMoveItem);
|
|
98
132
|
|
|
99
133
|
const handleDragEnd = React.useCallback(
|
|
100
134
|
(event: DragEndEvent) => {
|
|
101
135
|
const { active, over } = event;
|
|
102
|
-
const activatorEventPoint = activatorEventRef.current
|
|
103
|
-
? {
|
|
104
|
-
x: activatorEventRef.current.clientX,
|
|
105
|
-
y: activatorEventRef.current.clientY,
|
|
106
|
-
}
|
|
107
|
-
: null;
|
|
108
136
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
if (!activatorEventRef.current || !dragState.source) return;
|
|
138
|
+
|
|
139
|
+
const dropTarget = findDropTarget({
|
|
140
|
+
activationPoint: {
|
|
141
|
+
x: activatorEventRef.current.clientX,
|
|
142
|
+
y: activatorEventRef.current.clientY,
|
|
143
|
+
},
|
|
144
|
+
delta: { x: event.delta.x, y: event.delta.y },
|
|
145
|
+
over,
|
|
146
|
+
active,
|
|
147
|
+
source: dragState.source,
|
|
148
|
+
registeredLists,
|
|
149
|
+
});
|
|
115
150
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
151
|
+
setDragState({
|
|
152
|
+
source: null,
|
|
153
|
+
target: null,
|
|
154
|
+
indicator: null,
|
|
155
|
+
});
|
|
156
|
+
activatorEventRef.current = null;
|
|
119
157
|
|
|
120
|
-
|
|
121
|
-
const itemIndex = list.keys.findIndex((key) => key === over.id);
|
|
158
|
+
if (!dropTarget) return;
|
|
122
159
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
targetIndex = itemIndex;
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
160
|
+
const sourceList = registeredLists.get(dropTarget.source.listId);
|
|
161
|
+
const targetList = registeredLists.get(dropTarget.target.listId);
|
|
128
162
|
|
|
129
|
-
|
|
130
|
-
if (over.id === listId) {
|
|
131
|
-
targetListId = listId;
|
|
132
|
-
targetIndex = list.keys.length; // Append to end
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
163
|
+
if (!sourceList || !targetList) return;
|
|
136
164
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
const parameters: MoveDragItemParameters = {
|
|
166
|
+
sourceIndex: dropTarget.source.index,
|
|
167
|
+
targetIndex: dropTarget.target.index,
|
|
168
|
+
position: dropTarget.indicator,
|
|
169
|
+
sourceListId: dropTarget.source.listId,
|
|
170
|
+
targetListId: dropTarget.target.listId,
|
|
171
|
+
};
|
|
142
172
|
|
|
143
|
-
|
|
144
|
-
|
|
173
|
+
// Both lists have the opportunity to handle the move
|
|
174
|
+
sourceList.onMoveItem(parameters);
|
|
145
175
|
|
|
146
|
-
if (
|
|
147
|
-
|
|
176
|
+
if (dropTarget.target.listId !== dropTarget.source.listId) {
|
|
177
|
+
targetList.onMoveItem(parameters);
|
|
148
178
|
}
|
|
149
179
|
|
|
150
|
-
|
|
151
|
-
if (targetIndex < targetList.keys.length && activatorEventPoint) {
|
|
152
|
-
// Calculate absolute cursor position
|
|
153
|
-
const eventX = activatorEventPoint.x;
|
|
154
|
-
const eventY = activatorEventPoint.y;
|
|
155
|
-
const currentX = eventX + delta.x;
|
|
156
|
-
const currentY = eventY + delta.y;
|
|
157
|
-
const absolutePosition = { x: currentX, y: currentY };
|
|
158
|
-
|
|
159
|
-
const indicator = targetList.getDropIndicator({
|
|
160
|
-
absolutePosition,
|
|
161
|
-
active,
|
|
162
|
-
over,
|
|
163
|
-
sourceListId: activeItem.listId,
|
|
164
|
-
targetListId,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
if (!indicator) return;
|
|
168
|
-
|
|
169
|
-
// Notify the source list to handle the move
|
|
170
|
-
sourceList.onMoveItem(
|
|
171
|
-
activeItem.index,
|
|
172
|
-
targetIndex,
|
|
173
|
-
indicator,
|
|
174
|
-
activeItem.listId,
|
|
175
|
-
targetListId
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
// Handle drops into empty lists or at the end of lists
|
|
179
|
-
else if (targetIndex >= targetList.keys.length) {
|
|
180
|
-
// For empty lists or drops at the end, we don't need detailed position validation
|
|
181
|
-
// Just check if the drop is acceptable with a "below" position
|
|
182
|
-
const canDrop = targetList.acceptsDrop(
|
|
183
|
-
activeItem.index,
|
|
184
|
-
Math.max(0, targetList.keys.length - 1), // Use last valid index or 0 for empty lists
|
|
185
|
-
"below",
|
|
186
|
-
activeItem.listId,
|
|
187
|
-
targetListId
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
if (canDrop) {
|
|
191
|
-
sourceList.onMoveItem(
|
|
192
|
-
activeItem.index,
|
|
193
|
-
targetIndex,
|
|
194
|
-
"below",
|
|
195
|
-
activeItem.listId,
|
|
196
|
-
targetListId
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
180
|
+
stableOnMoveItem?.(parameters);
|
|
200
181
|
},
|
|
201
|
-
[
|
|
182
|
+
[registeredLists, dragState.source, stableOnMoveItem]
|
|
202
183
|
);
|
|
203
184
|
|
|
204
185
|
const contextValue: DragRegistrationContextType = React.useMemo(
|
|
205
186
|
() => ({
|
|
206
|
-
activeItem,
|
|
207
|
-
delta,
|
|
208
187
|
registerList,
|
|
209
188
|
unregisterList,
|
|
210
189
|
}),
|
|
211
|
-
[
|
|
190
|
+
[registerList, unregisterList]
|
|
212
191
|
);
|
|
213
192
|
|
|
214
|
-
const activeList =
|
|
193
|
+
const activeList = dragState.source
|
|
194
|
+
? registeredLists.get(dragState.source.listId)
|
|
195
|
+
: null;
|
|
215
196
|
const activeDragContextValue: ActiveDragContextType = React.useMemo(
|
|
216
|
-
() =>
|
|
217
|
-
[
|
|
197
|
+
() => dragState,
|
|
198
|
+
[dragState]
|
|
218
199
|
);
|
|
219
200
|
|
|
220
201
|
const collisionDetection = useMemo(() => {
|
|
221
|
-
return getItemFirstCollisionDetection(
|
|
202
|
+
return getItemFirstCollisionDetection(registeredLists);
|
|
222
203
|
}, [registeredLists]);
|
|
223
204
|
|
|
224
205
|
return (
|
|
225
206
|
<AnyDragContext.Provider value={true}>
|
|
226
|
-
<
|
|
227
|
-
<
|
|
207
|
+
<sharedDragProps.dragRegistrationContext.Provider value={contextValue}>
|
|
208
|
+
<sharedDragProps.activeDragContext.Provider
|
|
209
|
+
value={activeDragContextValue}
|
|
210
|
+
>
|
|
228
211
|
<DndContext
|
|
229
212
|
sensors={sensors}
|
|
230
213
|
collisionDetection={collisionDetection}
|
|
@@ -235,16 +218,16 @@ export function SharedDragProvider({
|
|
|
235
218
|
{children}
|
|
236
219
|
{mounted &&
|
|
237
220
|
activeList?.renderOverlay &&
|
|
238
|
-
|
|
221
|
+
dragState.source &&
|
|
239
222
|
createPortal(
|
|
240
223
|
<DragOverlay dropAnimation={null}>
|
|
241
|
-
{activeList.renderOverlay(
|
|
224
|
+
{activeList.renderOverlay(dragState.source.itemId)}
|
|
242
225
|
</DragOverlay>,
|
|
243
226
|
document.body
|
|
244
227
|
)}
|
|
245
228
|
</DndContext>
|
|
246
|
-
</
|
|
247
|
-
</
|
|
229
|
+
</sharedDragProps.activeDragContext.Provider>
|
|
230
|
+
</sharedDragProps.dragRegistrationContext.Provider>
|
|
248
231
|
</AnyDragContext.Provider>
|
|
249
232
|
);
|
|
250
233
|
}
|
|
@@ -279,13 +262,13 @@ export function withDragProvider<T extends object>(
|
|
|
279
262
|
|
|
280
263
|
// Custom collision detection that prioritizes items over containers
|
|
281
264
|
export const getItemFirstCollisionDetection =
|
|
282
|
-
(
|
|
265
|
+
(registeredLists: Map<string, RegisteredList>): CollisionDetection =>
|
|
283
266
|
(parameters) => {
|
|
284
267
|
const pointerCollisions = pointerWithin(parameters);
|
|
285
268
|
|
|
286
269
|
if (pointerCollisions.length > 0) {
|
|
287
270
|
const itemCollisions = pointerCollisions.filter((collision) => {
|
|
288
|
-
for (const listId of
|
|
271
|
+
for (const listId of registeredLists.keys()) {
|
|
289
272
|
if (collision.id === listId) {
|
|
290
273
|
return false;
|
|
291
274
|
}
|
|
@@ -299,9 +282,149 @@ export const getItemFirstCollisionDetection =
|
|
|
299
282
|
}
|
|
300
283
|
}
|
|
301
284
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
285
|
+
const listCollision = pointerCollisions.at(0);
|
|
286
|
+
const nearestCenters = closestCenter(parameters);
|
|
287
|
+
|
|
288
|
+
// Handle a pointer between items by checking if the pointer is within
|
|
289
|
+
// a window of two items.
|
|
290
|
+
if (parameters.pointerCoordinates && listCollision) {
|
|
291
|
+
const list = registeredLists.get(listCollision.id as string);
|
|
292
|
+
|
|
293
|
+
if (list) {
|
|
294
|
+
const pairs = windowsOf(list.keys, 2);
|
|
295
|
+
|
|
296
|
+
for (const pair of pairs) {
|
|
297
|
+
const clientRects = pair.map(
|
|
298
|
+
(key) => parameters.droppableRects.get(key)!
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const merged = unionRects(
|
|
302
|
+
...clientRects.map((rect) => ({
|
|
303
|
+
x: rect.left,
|
|
304
|
+
y: rect.top,
|
|
305
|
+
width: rect.width,
|
|
306
|
+
height: rect.height,
|
|
307
|
+
}))
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
if (rectContainsPoint(merged, parameters.pointerCoordinates)) {
|
|
311
|
+
return nearestCenters.filter((center) =>
|
|
312
|
+
pair.some((id) => id === center.id)
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// This handles empty lists
|
|
320
|
+
return pointerCollisions;
|
|
307
321
|
};
|
|
322
|
+
|
|
323
|
+
function findDropTarget({
|
|
324
|
+
activationPoint,
|
|
325
|
+
delta,
|
|
326
|
+
over,
|
|
327
|
+
active,
|
|
328
|
+
source,
|
|
329
|
+
registeredLists,
|
|
330
|
+
}: {
|
|
331
|
+
activationPoint: Point;
|
|
332
|
+
delta: Point;
|
|
333
|
+
over: Over | null;
|
|
334
|
+
active: Active;
|
|
335
|
+
source: DragItem;
|
|
336
|
+
registeredLists: Map<string, RegisteredList>;
|
|
337
|
+
}):
|
|
338
|
+
| {
|
|
339
|
+
source: DragItem;
|
|
340
|
+
target: DragItem;
|
|
341
|
+
indicator: RelativeDropPosition;
|
|
342
|
+
}
|
|
343
|
+
| undefined {
|
|
344
|
+
if (!over || active.id === over.id) return;
|
|
345
|
+
|
|
346
|
+
// Find target list and item
|
|
347
|
+
let targetListId: string | null = null;
|
|
348
|
+
let targetIndex: number = -1;
|
|
349
|
+
|
|
350
|
+
for (const [listId, list] of registeredLists) {
|
|
351
|
+
const itemIndex = list.keys.findIndex((key) => key === over.id);
|
|
352
|
+
|
|
353
|
+
if (itemIndex >= 0) {
|
|
354
|
+
targetListId = listId;
|
|
355
|
+
targetIndex = itemIndex;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Also check if dropping on the list container itself
|
|
360
|
+
if (over.id === listId) {
|
|
361
|
+
targetListId = listId;
|
|
362
|
+
targetIndex = list.keys.length; // Append to end
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const targetList =
|
|
368
|
+
targetListId !== null ? registeredLists.get(targetListId) : null;
|
|
369
|
+
const sourceList = registeredLists.get(source.listId);
|
|
370
|
+
|
|
371
|
+
if (
|
|
372
|
+
!targetList ||
|
|
373
|
+
!sourceList ||
|
|
374
|
+
!targetList.acceptsFromList({
|
|
375
|
+
sourceListId: sourceList.id,
|
|
376
|
+
targetListId: targetList.id,
|
|
377
|
+
})
|
|
378
|
+
) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Dropping on the list container itself
|
|
383
|
+
if (over.id === targetList.id) {
|
|
384
|
+
const canDrop = targetList.acceptsDrop({
|
|
385
|
+
sourceIndex: source.index,
|
|
386
|
+
targetIndex: Math.max(0, targetList.keys.length - 1), // Use last valid index or 0 for empty lists
|
|
387
|
+
position: "below",
|
|
388
|
+
sourceListId: sourceList.id,
|
|
389
|
+
targetListId: targetList.id,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (canDrop) {
|
|
393
|
+
return {
|
|
394
|
+
source,
|
|
395
|
+
target: {
|
|
396
|
+
itemId: over.id,
|
|
397
|
+
listId: targetList.id,
|
|
398
|
+
index: targetIndex,
|
|
399
|
+
},
|
|
400
|
+
indicator: "below",
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const eventX = activationPoint.x;
|
|
406
|
+
const eventY = activationPoint.y;
|
|
407
|
+
const currentX = eventX + delta.x;
|
|
408
|
+
const currentY = eventY + delta.y;
|
|
409
|
+
const absolutePosition = { x: currentX, y: currentY };
|
|
410
|
+
|
|
411
|
+
const indicator = targetList.getDropIndicator({
|
|
412
|
+
absolutePosition,
|
|
413
|
+
active,
|
|
414
|
+
over,
|
|
415
|
+
sourceListId: sourceList.id,
|
|
416
|
+
targetListId: targetList.id,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (indicator) {
|
|
420
|
+
return {
|
|
421
|
+
source,
|
|
422
|
+
target: {
|
|
423
|
+
itemId: over.id,
|
|
424
|
+
listId: targetList.id,
|
|
425
|
+
index: targetIndex,
|
|
426
|
+
},
|
|
427
|
+
indicator,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|