Drag into a Sortable list item
We have a use case like folders and files where folders can be sorted separately and files sorted separately. Folders can be dropped into folders and files can be dropped into folders. But folders cannot be dropped into files.
I have been trying to figure out how to use the SortableContext along with a Draggable/Droppable item contained inside the SortableContext to accept this scenario.
Any help is much appreciated!
Imagine this to be very similar to the MacOS for folders.
We managed to implement this behavior following way.
- several SorableContexts responsible for different lists with item.data.type
- global DndContext with drag overlay via portal, which displays drag preview according to item.data.type
- DndContext.onDragStart sets currently dragged item to provide ui changes
- DndContext.collisionDetectionStrategy with custom implementation which manages how to react on collisions
- DndContext.onDragEnd compares active.data.type and over.data.type and performs data changes.
Maybe where is a simpler way, but this one works fine.
export const collisionDetectionStrategy: CollisionDetection = (args) => {
const folderArgs = { ...args };
const surveyArgs = { ...args };
const rootArgs = { ...args };
let overId = null;
rootArgs.droppableContainers = rootArgs.droppableContainers.filter(
(c) => c.data.current?.type === Droppables.rootFolder
);
folderArgs.droppableContainers = folderArgs.droppableContainers.filter(
(c) => c.data.current?.type === Droppables.folder
);
surveyArgs.droppableContainers = surveyArgs.droppableContainers.filter(
(c) => c.data.current?.type === Droppables.research
);
const folderIntersectionId = rectIntersection(folderArgs);
if (folderIntersectionId && folderIntersectionId.length > 0) {
return closestCorners(folderArgs);
}
const rootIntersection = rectIntersection(rootArgs);
if (rootIntersection && rootIntersection.length > 0) {
return rootIntersection;
}
overId = closestCorners(args);
const overContainer = args.droppableContainers.find((c) => c.id === overId);
const overType = overContainer?.data?.current?.type;
if (overId === Droppables.rootFolder) {
return overId;
}
if (overType === Droppables.folder) {
return overId;
}
if (overType === Droppables.research) {
return rectIntersection({
...args,
droppableContainers: args.droppableContainers.filter(
(container) => container?.data?.current?.type === overType
)
});
}
return overId;
};
Thanks for this code snippet and help, do you have a sample of how your component layout looks like? I assume it is something like below:
<DndContext onDragOver={...} onDragEnd={...} collisionStrategy={collisionStrategy}>
<SortableContext items={folderItems}>
{folderItems.map((folderItem) => (
<Sortable id={folderItem.id}> // This is useSortable({ id, data: { type: 'folder' })
...
</Sortable>
)}
</SortableContext>
<SortableContext items={surveyItems}>
{surveyItems.map((surveyItem) => (
<Sortable id={surveyItem.id}> // This is useSortable({ id, data: { type: 'survey' })
...
</Sortable>
)}
</SortableContext>
</DndContext>
Apart from this do you have any droppable containers setup elsewhere? Like you mentioned the portal, what does that do and how does that look like?
SortableContext is a drop target. Inside App there are several SortableContext's which uses useSortable. You can put some restrictions on SortableContext or use another DndContext below in components tree to exclude them from global handling. I can share some more code
const DraggedItem: React.FC<{
draggedItem: { id: number; type: Droppables };
}> = ({ draggedItem: { id, type } }) => (
<DragOverlay>
{type === Droppables.research && <DraggedResearchListItem id={id} />}
{type === Droppables.folder && <DraggedFolder id={id} />}
</DragOverlay>
);
....
const onDragStart = useCallback(
({
active: {
data: {
current: { type }
},
id
}
}: DragStartEvent) => {
setDraggedItem({ id, type });
},
[]
);
const measuring = {
droppable: {
strategy: MeasuringStrategy.Always
}
};
<DndContext
collisionDetection={collisionDetectionStrategy}
measuring={measuring}
sensors={sensors}
onDragEnd={onDragEnd}
onDragStart={onDragStart}
>
<App />
{createPortal(<DraggedItem draggedItem={draggedItem} />, document.body)}
</DndContext>
Hi, can you setup your demo on https://codesandbox.io?
Second that, @cherepanov. This is exactly what I'm trying to do. A simple nested list where only folders can take nested items.
Here's a proof-of-concept app that illustrates dragging items into and out of a sortable list. It illustrates a few relevant facts when intermixing core and sortable components, which should also apply to mixing nested sortable components, including:
- you need to add items to the list or drop them yourself, for example in the
onDragOveroronDragEndcallbacks. Because you need to do this your self, this is where you can implement your logic of which items are allowed to drag into or out of a list. - array manipulation is so common that
@dnd-kit/sortableexports helpers likearrayMove, which are indeed helpful. - your
onDragOverevent handler needs to be idempotent (since it can be fired repeatedly) - you should not duplicate actions between
onDragEndandonDragOver - you should handle intermediate transformations (like re-arranging lists)
in
onDragOver. - If you don't get the item ids right, then nothing else works right (that is (e.g. the items in
<SortableContext items={...} />must match the ids inuseSortable({ id })exactly). onDragOveris called when the 'over' state changes, so it can be used to detect drag-out events too (i.e.if(!over){/* Nothing is being dragged over */})- You need a
<DragOverLay>component in most cases (b/c the CSS either doesn't work or doesn't look right most of the time when intermixing sortable and core components) - You don's need the
useDraggablecss transforms if you use a drag overlay (and they are often ugly / strangely scaled compared to using theDragOverlaycomponent) - The
useSortablecss transforms are useful even when using a drag overlay because they implement the sorting CSS animations
This was inspired by the docs and this example -- which is also worth looking at if you want to implement multiple lists.
Hope this is useful.