@noya-app/noya-designsystem 0.1.55 → 0.1.57
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 +113 -36
- package/dist/index.d.ts +113 -36
- package/dist/index.js +1042 -665
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +995 -625
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/validateDropIndicator.test.ts +35 -9
- package/src/components/Collection.tsx +3 -0
- package/src/components/List.tsx +4 -0
- package/src/components/ListView.tsx +12 -8
- package/src/components/Toolbar.tsx +15 -2
- package/src/components/sorting/DragRegistration.tsx +107 -0
- package/src/components/sorting/SharedDragProvider.tsx +376 -0
- package/src/components/sorting/Sortable.tsx +357 -0
- package/src/components/sorting/createSharedDrag.tsx +41 -0
- package/src/components/sorting/sorting.ts +215 -0
- package/src/index.tsx +4 -2
- package/src/utils/moveTreeItem.ts +4 -2
- package/src/components/Sortable.tsx +0 -468
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Active,
|
|
3
|
+
closestCenter,
|
|
4
|
+
CollisionDetection,
|
|
5
|
+
DndContext,
|
|
6
|
+
DragEndEvent,
|
|
7
|
+
DragMoveEvent,
|
|
8
|
+
DragOverlay,
|
|
9
|
+
DragStartEvent,
|
|
10
|
+
Over,
|
|
11
|
+
PointerSensor,
|
|
12
|
+
pointerWithin,
|
|
13
|
+
useSensor,
|
|
14
|
+
useSensors,
|
|
15
|
+
} from "@dnd-kit/core";
|
|
16
|
+
import { Point } from "@noya-app/noya-geometry";
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import { useMemo } from "react";
|
|
19
|
+
import { createPortal } from "react-dom";
|
|
20
|
+
import {
|
|
21
|
+
ActiveDragContext,
|
|
22
|
+
ActiveDragContextType,
|
|
23
|
+
AnyDragContext,
|
|
24
|
+
DragRegistrationContext,
|
|
25
|
+
DragRegistrationContextType,
|
|
26
|
+
RegisteredList,
|
|
27
|
+
SharedDragProps,
|
|
28
|
+
useDragRegistrationManager,
|
|
29
|
+
} from "./DragRegistration";
|
|
30
|
+
import { DragItem, DragState, RelativeDropPosition } from "./sorting";
|
|
31
|
+
|
|
32
|
+
type SharedDragProviderProps = {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
sharedDragProps?: SharedDragProps;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function SharedDragProvider({
|
|
38
|
+
children,
|
|
39
|
+
sharedDragProps = {
|
|
40
|
+
dragRegistrationContext: DragRegistrationContext,
|
|
41
|
+
activeDragContext: ActiveDragContext,
|
|
42
|
+
},
|
|
43
|
+
}: SharedDragProviderProps) {
|
|
44
|
+
const sensors = useSensors(
|
|
45
|
+
useSensor(PointerSensor, {
|
|
46
|
+
activationConstraint: {
|
|
47
|
+
distance: 4,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const mounted = useMounted();
|
|
53
|
+
const [dragState, setDragState] = React.useState<DragState>({
|
|
54
|
+
source: null,
|
|
55
|
+
target: null,
|
|
56
|
+
indicator: null,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { registerList, unregisterList, registeredLists } =
|
|
60
|
+
useDragRegistrationManager();
|
|
61
|
+
|
|
62
|
+
const activatorEventRef = React.useRef<PointerEvent | null>(null);
|
|
63
|
+
|
|
64
|
+
const handleDragStart = React.useCallback(
|
|
65
|
+
(event: DragStartEvent) => {
|
|
66
|
+
// Capture the activator event for position calculations
|
|
67
|
+
activatorEventRef.current = event.activatorEvent as PointerEvent;
|
|
68
|
+
|
|
69
|
+
// Find which list contains this item
|
|
70
|
+
for (const [listId, list] of registeredLists) {
|
|
71
|
+
const itemIndex = list.keys.findIndex((key) => key === event.active.id);
|
|
72
|
+
|
|
73
|
+
if (itemIndex >= 0) {
|
|
74
|
+
const dragItem: DragItem = {
|
|
75
|
+
itemId: event.active.id,
|
|
76
|
+
listId,
|
|
77
|
+
index: itemIndex,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
setDragState({
|
|
81
|
+
source: dragItem,
|
|
82
|
+
target: null,
|
|
83
|
+
indicator: null,
|
|
84
|
+
});
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[registeredLists]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const handleDragMove = React.useCallback(
|
|
93
|
+
(event: DragMoveEvent) => {
|
|
94
|
+
const { active, over } = event;
|
|
95
|
+
|
|
96
|
+
if (!activatorEventRef.current || !dragState.source) return;
|
|
97
|
+
|
|
98
|
+
const dropTarget = findDropTarget({
|
|
99
|
+
activationPoint: {
|
|
100
|
+
x: activatorEventRef.current.clientX,
|
|
101
|
+
y: activatorEventRef.current.clientY,
|
|
102
|
+
},
|
|
103
|
+
delta: { x: event.delta.x, y: event.delta.y },
|
|
104
|
+
over,
|
|
105
|
+
active,
|
|
106
|
+
source: dragState.source,
|
|
107
|
+
registeredLists,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
setDragState({
|
|
111
|
+
source: dragState.source,
|
|
112
|
+
target: dropTarget?.target ?? null,
|
|
113
|
+
indicator: dropTarget?.indicator ?? null,
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
[registeredLists, dragState.source]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const handleDragEnd = React.useCallback(
|
|
120
|
+
(event: DragEndEvent) => {
|
|
121
|
+
const { active, over } = event;
|
|
122
|
+
|
|
123
|
+
if (!activatorEventRef.current || !dragState.source) return;
|
|
124
|
+
|
|
125
|
+
const dropTarget = findDropTarget({
|
|
126
|
+
activationPoint: {
|
|
127
|
+
x: activatorEventRef.current.clientX,
|
|
128
|
+
y: activatorEventRef.current.clientY,
|
|
129
|
+
},
|
|
130
|
+
delta: { x: event.delta.x, y: event.delta.y },
|
|
131
|
+
over,
|
|
132
|
+
active,
|
|
133
|
+
source: dragState.source,
|
|
134
|
+
registeredLists,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
setDragState({
|
|
138
|
+
source: null,
|
|
139
|
+
target: null,
|
|
140
|
+
indicator: null,
|
|
141
|
+
});
|
|
142
|
+
activatorEventRef.current = null;
|
|
143
|
+
|
|
144
|
+
if (!dropTarget) return;
|
|
145
|
+
|
|
146
|
+
const sourceList = registeredLists.get(dropTarget.source.listId);
|
|
147
|
+
const targetList = registeredLists.get(dropTarget.target.listId);
|
|
148
|
+
|
|
149
|
+
if (!sourceList || !targetList) return;
|
|
150
|
+
|
|
151
|
+
sourceList.onMoveItem(
|
|
152
|
+
dropTarget.source.index,
|
|
153
|
+
dropTarget.target.index,
|
|
154
|
+
dropTarget.indicator,
|
|
155
|
+
dropTarget.source.listId,
|
|
156
|
+
dropTarget.target.listId
|
|
157
|
+
);
|
|
158
|
+
},
|
|
159
|
+
[registeredLists, dragState.source]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const contextValue: DragRegistrationContextType = React.useMemo(
|
|
163
|
+
() => ({
|
|
164
|
+
registerList,
|
|
165
|
+
unregisterList,
|
|
166
|
+
}),
|
|
167
|
+
[registerList, unregisterList]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const activeList = dragState.source
|
|
171
|
+
? registeredLists.get(dragState.source.listId)
|
|
172
|
+
: null;
|
|
173
|
+
const activeDragContextValue: ActiveDragContextType = React.useMemo(
|
|
174
|
+
() => dragState,
|
|
175
|
+
[dragState]
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const collisionDetection = useMemo(() => {
|
|
179
|
+
return getItemFirstCollisionDetection(Array.from(registeredLists.keys()));
|
|
180
|
+
}, [registeredLists]);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<AnyDragContext.Provider value={true}>
|
|
184
|
+
<sharedDragProps.dragRegistrationContext.Provider value={contextValue}>
|
|
185
|
+
<sharedDragProps.activeDragContext.Provider
|
|
186
|
+
value={activeDragContextValue}
|
|
187
|
+
>
|
|
188
|
+
<DndContext
|
|
189
|
+
sensors={sensors}
|
|
190
|
+
collisionDetection={collisionDetection}
|
|
191
|
+
onDragStart={handleDragStart}
|
|
192
|
+
onDragMove={handleDragMove}
|
|
193
|
+
onDragEnd={handleDragEnd}
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
{mounted &&
|
|
197
|
+
activeList?.renderOverlay &&
|
|
198
|
+
dragState.source &&
|
|
199
|
+
createPortal(
|
|
200
|
+
<DragOverlay dropAnimation={null}>
|
|
201
|
+
{activeList.renderOverlay(dragState.source.itemId)}
|
|
202
|
+
</DragOverlay>,
|
|
203
|
+
document.body
|
|
204
|
+
)}
|
|
205
|
+
</DndContext>
|
|
206
|
+
</sharedDragProps.activeDragContext.Provider>
|
|
207
|
+
</sharedDragProps.dragRegistrationContext.Provider>
|
|
208
|
+
</AnyDragContext.Provider>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function useMounted() {
|
|
213
|
+
const [mounted, setMounted] = React.useState(false);
|
|
214
|
+
|
|
215
|
+
React.useEffect(() => {
|
|
216
|
+
setMounted(true);
|
|
217
|
+
}, []);
|
|
218
|
+
|
|
219
|
+
return mounted;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function withDragProvider<T extends object>(
|
|
223
|
+
Component: React.ComponentType<T>
|
|
224
|
+
) {
|
|
225
|
+
return function WithDragProvider(props: T) {
|
|
226
|
+
const anyDragContext = React.useContext(AnyDragContext);
|
|
227
|
+
|
|
228
|
+
if (!anyDragContext) {
|
|
229
|
+
return (
|
|
230
|
+
<SharedDragProvider>
|
|
231
|
+
<Component {...props} />
|
|
232
|
+
</SharedDragProvider>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return <Component {...props} />;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Custom collision detection that prioritizes items over containers
|
|
241
|
+
export const getItemFirstCollisionDetection =
|
|
242
|
+
(registeredListIds: string[]): CollisionDetection =>
|
|
243
|
+
(parameters) => {
|
|
244
|
+
const pointerCollisions = pointerWithin(parameters);
|
|
245
|
+
|
|
246
|
+
if (pointerCollisions.length > 0) {
|
|
247
|
+
const itemCollisions = pointerCollisions.filter((collision) => {
|
|
248
|
+
for (const listId of registeredListIds) {
|
|
249
|
+
if (collision.id === listId) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return true;
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
if (itemCollisions.length > 0) {
|
|
258
|
+
return itemCollisions;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fall back to pointer-based detection (includes containers)
|
|
263
|
+
// This handles empty lists and edge cases
|
|
264
|
+
return pointerCollisions.length > 0
|
|
265
|
+
? pointerCollisions
|
|
266
|
+
: closestCenter(parameters);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
function findDropTarget({
|
|
270
|
+
activationPoint,
|
|
271
|
+
delta,
|
|
272
|
+
over,
|
|
273
|
+
active,
|
|
274
|
+
source,
|
|
275
|
+
registeredLists,
|
|
276
|
+
}: {
|
|
277
|
+
activationPoint: Point;
|
|
278
|
+
delta: Point;
|
|
279
|
+
over: Over | null;
|
|
280
|
+
active: Active;
|
|
281
|
+
source: DragItem;
|
|
282
|
+
registeredLists: Map<string, RegisteredList>;
|
|
283
|
+
}):
|
|
284
|
+
| {
|
|
285
|
+
source: DragItem;
|
|
286
|
+
target: DragItem;
|
|
287
|
+
indicator: RelativeDropPosition;
|
|
288
|
+
}
|
|
289
|
+
| undefined {
|
|
290
|
+
if (!over || active.id === over.id) return;
|
|
291
|
+
|
|
292
|
+
// Find target list and item
|
|
293
|
+
let targetListId: string | null = null;
|
|
294
|
+
let targetIndex: number = -1;
|
|
295
|
+
|
|
296
|
+
for (const [listId, list] of registeredLists) {
|
|
297
|
+
const itemIndex = list.keys.findIndex((key) => key === over.id);
|
|
298
|
+
|
|
299
|
+
if (itemIndex >= 0) {
|
|
300
|
+
targetListId = listId;
|
|
301
|
+
targetIndex = itemIndex;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Also check if dropping on the list container itself
|
|
306
|
+
if (over.id === listId) {
|
|
307
|
+
targetListId = listId;
|
|
308
|
+
targetIndex = list.keys.length; // Append to end
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const targetList =
|
|
314
|
+
targetListId !== null ? registeredLists.get(targetListId) : null;
|
|
315
|
+
const sourceList = registeredLists.get(source.listId);
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
!targetList ||
|
|
319
|
+
!sourceList ||
|
|
320
|
+
!targetList.acceptsFromList({
|
|
321
|
+
sourceListId: sourceList.id,
|
|
322
|
+
targetListId: targetList.id,
|
|
323
|
+
})
|
|
324
|
+
) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Dropping on the list container itself
|
|
329
|
+
if (over.id === targetList.id) {
|
|
330
|
+
const canDrop = targetList.acceptsDrop(
|
|
331
|
+
source.index,
|
|
332
|
+
Math.max(0, targetList.keys.length - 1), // Use last valid index or 0 for empty lists
|
|
333
|
+
"below",
|
|
334
|
+
sourceList.id,
|
|
335
|
+
targetList.id
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (canDrop) {
|
|
339
|
+
return {
|
|
340
|
+
source,
|
|
341
|
+
target: {
|
|
342
|
+
itemId: over.id,
|
|
343
|
+
listId: targetList.id,
|
|
344
|
+
index: targetIndex,
|
|
345
|
+
},
|
|
346
|
+
indicator: "below",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const eventX = activationPoint.x;
|
|
352
|
+
const eventY = activationPoint.y;
|
|
353
|
+
const currentX = eventX + delta.x;
|
|
354
|
+
const currentY = eventY + delta.y;
|
|
355
|
+
const absolutePosition = { x: currentX, y: currentY };
|
|
356
|
+
|
|
357
|
+
const indicator = targetList.getDropIndicator({
|
|
358
|
+
absolutePosition,
|
|
359
|
+
active,
|
|
360
|
+
over,
|
|
361
|
+
sourceListId: sourceList.id,
|
|
362
|
+
targetListId: targetList.id,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
if (indicator) {
|
|
366
|
+
return {
|
|
367
|
+
source,
|
|
368
|
+
target: {
|
|
369
|
+
itemId: over.id,
|
|
370
|
+
listId: targetList.id,
|
|
371
|
+
index: targetIndex,
|
|
372
|
+
},
|
|
373
|
+
indicator,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { UniqueIdentifier, useDroppable } from "@dnd-kit/core";
|
|
2
|
+
import {
|
|
3
|
+
horizontalListSortingStrategy,
|
|
4
|
+
SortableContext,
|
|
5
|
+
useSortable,
|
|
6
|
+
verticalListSortingStrategy,
|
|
7
|
+
} from "@dnd-kit/sortable";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
import {
|
|
10
|
+
ActiveDragContext,
|
|
11
|
+
ActiveDragContextType,
|
|
12
|
+
DragRegistrationContext,
|
|
13
|
+
RegisteredList,
|
|
14
|
+
SharedDragProps,
|
|
15
|
+
useActiveDrag,
|
|
16
|
+
useDragRegistration,
|
|
17
|
+
} from "./DragRegistration";
|
|
18
|
+
import { withDragProvider } from "./SharedDragProvider";
|
|
19
|
+
import {
|
|
20
|
+
AcceptsFromList,
|
|
21
|
+
createDragItemKey,
|
|
22
|
+
defaultAcceptsDrop,
|
|
23
|
+
DropValidator,
|
|
24
|
+
MoveDragItemHandler,
|
|
25
|
+
RelativeDropPosition,
|
|
26
|
+
validateDropIndicator,
|
|
27
|
+
} from "./sorting";
|
|
28
|
+
|
|
29
|
+
type UseSortableReturnType = ReturnType<typeof useSortable>;
|
|
30
|
+
|
|
31
|
+
type ItemChildrenProps<T> = {
|
|
32
|
+
ref: React.Ref<T>;
|
|
33
|
+
relativeDropPosition?: RelativeDropPosition;
|
|
34
|
+
style?: React.CSSProperties;
|
|
35
|
+
} & UseSortableReturnType["attributes"];
|
|
36
|
+
|
|
37
|
+
// Context for drop indicators within a sortable list
|
|
38
|
+
interface SortableItemContextProps {
|
|
39
|
+
acceptsDrop: DropValidator;
|
|
40
|
+
listId: string;
|
|
41
|
+
getDropTargetParentIndex?: (index: number) => number | undefined;
|
|
42
|
+
activeDragContext: React.Context<ActiveDragContextType | null>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SortableItemContext = React.createContext<SortableItemContextProps>({
|
|
46
|
+
acceptsDrop: defaultAcceptsDrop,
|
|
47
|
+
listId: "",
|
|
48
|
+
getDropTargetParentIndex: undefined,
|
|
49
|
+
activeDragContext: ActiveDragContext,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Custom hook for drop indicators in sortable lists
|
|
53
|
+
function useSortableDropIndicator({
|
|
54
|
+
index,
|
|
55
|
+
itemId,
|
|
56
|
+
listId,
|
|
57
|
+
acceptsDrop,
|
|
58
|
+
getDropTargetParentIndex,
|
|
59
|
+
}: {
|
|
60
|
+
index: number;
|
|
61
|
+
itemId: UniqueIdentifier;
|
|
62
|
+
listId: string;
|
|
63
|
+
acceptsDrop: DropValidator;
|
|
64
|
+
getDropTargetParentIndex?: (index: number) => number | undefined;
|
|
65
|
+
}): RelativeDropPosition | undefined {
|
|
66
|
+
const { activeDragContext } = React.useContext(SortableItemContext);
|
|
67
|
+
const activeDrag = useActiveDrag(activeDragContext);
|
|
68
|
+
const { source, target } = activeDrag;
|
|
69
|
+
|
|
70
|
+
if (!source || !target) return;
|
|
71
|
+
|
|
72
|
+
const indicator =
|
|
73
|
+
target.listId === listId && target.itemId === itemId
|
|
74
|
+
? activeDrag.indicator
|
|
75
|
+
: undefined;
|
|
76
|
+
|
|
77
|
+
const targetParentIndex =
|
|
78
|
+
target.index === -1 ? undefined : getDropTargetParentIndex?.(target.index);
|
|
79
|
+
|
|
80
|
+
const isValidParent =
|
|
81
|
+
targetParentIndex === undefined || targetParentIndex === -1
|
|
82
|
+
? false
|
|
83
|
+
: acceptsDrop(
|
|
84
|
+
source.index,
|
|
85
|
+
targetParentIndex,
|
|
86
|
+
"inside",
|
|
87
|
+
source.listId,
|
|
88
|
+
target.listId
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const overIdAcceptsDrop =
|
|
92
|
+
target.index === -1
|
|
93
|
+
? undefined
|
|
94
|
+
: acceptsDrop(
|
|
95
|
+
source.index,
|
|
96
|
+
target.index,
|
|
97
|
+
"inside",
|
|
98
|
+
source.listId,
|
|
99
|
+
target.listId
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const thisItemIsTargetParent = targetParentIndex === index;
|
|
103
|
+
|
|
104
|
+
const showDragInsideIndicatorOnParent =
|
|
105
|
+
!overIdAcceptsDrop && isValidParent && thisItemIsTargetParent;
|
|
106
|
+
|
|
107
|
+
return indicator ?? (showDragInsideIndicatorOnParent ? "inside" : undefined);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface RootProps {
|
|
111
|
+
id?: string;
|
|
112
|
+
keys: UniqueIdentifier[];
|
|
113
|
+
axis?: "x" | "y";
|
|
114
|
+
children?: React.ReactNode;
|
|
115
|
+
renderOverlay?: (index: number) => React.ReactNode;
|
|
116
|
+
onMoveItem?: MoveDragItemHandler;
|
|
117
|
+
acceptsFromList?: AcceptsFromList;
|
|
118
|
+
acceptsDrop?: DropValidator;
|
|
119
|
+
getDropTargetParentIndex?: (index: number) => number | undefined;
|
|
120
|
+
sharedDragProps?: SharedDragProps;
|
|
121
|
+
containerRef?: React.RefObject<HTMLElement>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function SortableItem<TElement extends HTMLElement>({
|
|
125
|
+
id,
|
|
126
|
+
disabled,
|
|
127
|
+
children,
|
|
128
|
+
}: {
|
|
129
|
+
id: UniqueIdentifier;
|
|
130
|
+
disabled?: boolean;
|
|
131
|
+
children: (props: ItemChildrenProps<TElement>) => React.ReactNode;
|
|
132
|
+
}) {
|
|
133
|
+
const { acceptsDrop, listId, getDropTargetParentIndex } =
|
|
134
|
+
React.useContext(SortableItemContext);
|
|
135
|
+
|
|
136
|
+
const dragItemKey = createDragItemKey(listId, id);
|
|
137
|
+
|
|
138
|
+
const { attributes, listeners, setNodeRef, isDragging, index } = useSortable({
|
|
139
|
+
id: dragItemKey,
|
|
140
|
+
disabled,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const ref = React.useCallback(
|
|
144
|
+
(node: TElement | null) => setNodeRef(node),
|
|
145
|
+
[setNodeRef]
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const relativeDropPosition = useSortableDropIndicator({
|
|
149
|
+
index,
|
|
150
|
+
itemId: dragItemKey,
|
|
151
|
+
listId,
|
|
152
|
+
acceptsDrop,
|
|
153
|
+
getDropTargetParentIndex,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return children({
|
|
157
|
+
ref,
|
|
158
|
+
...attributes,
|
|
159
|
+
...listeners,
|
|
160
|
+
relativeDropPosition,
|
|
161
|
+
style: {
|
|
162
|
+
opacity: isDragging ? 0.5 : 1,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function SortableRoot_({
|
|
168
|
+
id: idProp,
|
|
169
|
+
keys: keysProp,
|
|
170
|
+
onMoveItem,
|
|
171
|
+
renderOverlay,
|
|
172
|
+
acceptsFromList,
|
|
173
|
+
acceptsDrop = defaultAcceptsDrop,
|
|
174
|
+
axis = "y",
|
|
175
|
+
children,
|
|
176
|
+
getDropTargetParentIndex,
|
|
177
|
+
sharedDragProps,
|
|
178
|
+
containerRef,
|
|
179
|
+
}: RootProps) {
|
|
180
|
+
const defaultId = React.useId();
|
|
181
|
+
const id = idProp ?? defaultId;
|
|
182
|
+
const keys = React.useMemo(() => {
|
|
183
|
+
return keysProp.map((key) => createDragItemKey(id, key));
|
|
184
|
+
}, [keysProp, id]);
|
|
185
|
+
|
|
186
|
+
const { registerList, unregisterList } = useDragRegistration(
|
|
187
|
+
sharedDragProps?.dragRegistrationContext ?? DragRegistrationContext
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const { setNodeRef: setDroppableRef } = useDroppable({
|
|
191
|
+
id,
|
|
192
|
+
// Make the container droppable only when the list is empty
|
|
193
|
+
// This avoids conflicts with individual sortable items.
|
|
194
|
+
// This isn't strictly necessary since we also use a custom collision detection
|
|
195
|
+
// that prioritizes items over containers, but it helps with dropping into
|
|
196
|
+
// the best position (either above or below, rather than always below).
|
|
197
|
+
disabled: keys.length > 0,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Register this list with the global drag system
|
|
201
|
+
React.useEffect(() => {
|
|
202
|
+
const listConfig: RegisteredList = {
|
|
203
|
+
id,
|
|
204
|
+
keys,
|
|
205
|
+
onMoveItem: onMoveItem ?? (() => {}),
|
|
206
|
+
renderOverlay: (id) => {
|
|
207
|
+
const index = keys.findIndex((key) => key === id);
|
|
208
|
+
return renderOverlay?.(index) ?? null;
|
|
209
|
+
},
|
|
210
|
+
acceptsFromList: acceptsFromList ?? (() => true),
|
|
211
|
+
acceptsDrop,
|
|
212
|
+
getDropIndicator: (parameters) => {
|
|
213
|
+
const { absolutePosition, active, over, sourceListId, targetListId } =
|
|
214
|
+
parameters;
|
|
215
|
+
|
|
216
|
+
const offsetStart =
|
|
217
|
+
axis === "x" ? absolutePosition.x : absolutePosition.y;
|
|
218
|
+
const elementStart = axis === "x" ? over.rect.left : over.rect.top;
|
|
219
|
+
const elementSize = axis === "x" ? over.rect.width : over.rect.height;
|
|
220
|
+
|
|
221
|
+
const relativeDropPosition = validateDropIndicator({
|
|
222
|
+
acceptsDrop,
|
|
223
|
+
keys,
|
|
224
|
+
activeId: active.id,
|
|
225
|
+
overId: over.id,
|
|
226
|
+
offsetStart,
|
|
227
|
+
elementStart,
|
|
228
|
+
elementSize,
|
|
229
|
+
sourceListId,
|
|
230
|
+
targetListId,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return relativeDropPosition;
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
registerList(listConfig);
|
|
238
|
+
|
|
239
|
+
return () => {
|
|
240
|
+
unregisterList(id);
|
|
241
|
+
};
|
|
242
|
+
}, [
|
|
243
|
+
id,
|
|
244
|
+
onMoveItem,
|
|
245
|
+
renderOverlay,
|
|
246
|
+
acceptsFromList,
|
|
247
|
+
acceptsDrop,
|
|
248
|
+
axis,
|
|
249
|
+
registerList,
|
|
250
|
+
unregisterList,
|
|
251
|
+
keys,
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
const contextValue: SortableItemContextProps = React.useMemo(
|
|
255
|
+
() => ({
|
|
256
|
+
keys,
|
|
257
|
+
acceptsDrop,
|
|
258
|
+
listId: id,
|
|
259
|
+
getDropTargetParentIndex,
|
|
260
|
+
activeDragContext:
|
|
261
|
+
sharedDragProps?.activeDragContext ?? ActiveDragContext,
|
|
262
|
+
}),
|
|
263
|
+
[keys, acceptsDrop, id, getDropTargetParentIndex, sharedDragProps]
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Set the droppable ref after every render.
|
|
267
|
+
// Since we don't render the dom node internally, we aggressively set it
|
|
268
|
+
// after every render to make sure we are using the latest.
|
|
269
|
+
React.useEffect(() => {
|
|
270
|
+
if (containerRef) {
|
|
271
|
+
setDroppableRef(containerRef.current);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<SortableItemContext.Provider value={contextValue}>
|
|
277
|
+
<SortableContext
|
|
278
|
+
items={keys}
|
|
279
|
+
strategy={
|
|
280
|
+
axis === "x"
|
|
281
|
+
? horizontalListSortingStrategy
|
|
282
|
+
: verticalListSortingStrategy
|
|
283
|
+
}
|
|
284
|
+
>
|
|
285
|
+
{children}
|
|
286
|
+
</SortableContext>
|
|
287
|
+
</SortableItemContext.Provider>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const SortableRoot = withDragProvider(SortableRoot_) as typeof SortableRoot_;
|
|
292
|
+
|
|
293
|
+
export interface SortableProps<TItem, TElement extends HTMLElement>
|
|
294
|
+
extends Omit<RootProps, "children" | "keys" | "renderOverlay"> {
|
|
295
|
+
items: TItem[];
|
|
296
|
+
keyExtractor: (item: TItem) => UniqueIdentifier;
|
|
297
|
+
shouldRenderOverlay?: boolean;
|
|
298
|
+
renderItem: (
|
|
299
|
+
item: TItem,
|
|
300
|
+
props: ItemChildrenProps<TElement>,
|
|
301
|
+
{ isOverlay }: { isOverlay: boolean }
|
|
302
|
+
) => React.ReactNode;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const Sortable_ = function Sortable_<TItem, TElement extends HTMLElement>({
|
|
306
|
+
items,
|
|
307
|
+
keyExtractor,
|
|
308
|
+
shouldRenderOverlay = true,
|
|
309
|
+
renderItem,
|
|
310
|
+
...rest
|
|
311
|
+
}: SortableProps<TItem, TElement>) {
|
|
312
|
+
const keys = React.useMemo(
|
|
313
|
+
() => items.map(keyExtractor),
|
|
314
|
+
[items, keyExtractor]
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const renderOverlay = React.useCallback(
|
|
318
|
+
(index: number) => {
|
|
319
|
+
return renderItem(
|
|
320
|
+
items[index],
|
|
321
|
+
{
|
|
322
|
+
ref: () => {},
|
|
323
|
+
style: {},
|
|
324
|
+
tabIndex: -1,
|
|
325
|
+
"aria-disabled": true,
|
|
326
|
+
"aria-pressed": false,
|
|
327
|
+
"aria-roledescription": "Item being dragged",
|
|
328
|
+
"aria-describedby": "Item being dragged",
|
|
329
|
+
role: "button",
|
|
330
|
+
},
|
|
331
|
+
{ isOverlay: true }
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
[renderItem, items]
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<SortableRoot
|
|
339
|
+
{...rest}
|
|
340
|
+
keys={keys}
|
|
341
|
+
renderOverlay={shouldRenderOverlay ? renderOverlay : undefined}
|
|
342
|
+
>
|
|
343
|
+
{keys.map((key, index) => (
|
|
344
|
+
<SortableItem<TElement> key={key} id={key}>
|
|
345
|
+
{(props) => {
|
|
346
|
+
return renderItem(items[index], props, { isOverlay: false });
|
|
347
|
+
}}
|
|
348
|
+
</SortableItem>
|
|
349
|
+
))}
|
|
350
|
+
</SortableRoot>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const Sortable = Object.assign(Sortable_, {
|
|
355
|
+
Root: SortableRoot,
|
|
356
|
+
Item: SortableItem,
|
|
357
|
+
});
|