-
Notifications
You must be signed in to change notification settings - Fork 125
Description
Genuine question: Is Nested Droppable possible?
We are using hello-pangea, to have like 2 layer tree structure of sorting.
Item
Group
- Item
- Item
Item
The behavior goes like:
- Item and Group can be reordered in the root
- Item can be reordered from Root to Group, Group to Root, Group to another Group.
But I am encountering issues with the implementation like
- The item's drag overlay is further from the cursor
https://github.com/user-attachments/assets/575216cd-2113-4c30-809b-e180fb93a320 - Cannot insert item to group
The architecture is like this:
DragDropContext
└── Droppable (id: "root")
├── Draggable (section-1)
│ └── SectionCard
│ └── Droppable (id: "section-section-1") ← Nested droppable
│ ├── Draggable (field-1)
│ └── Draggable (field-2)
├── Draggable (field-3) ← Top-level field
└── Draggable (section-2)
`const onDragEnd = (result: DropResult) => {
setIsDraggingAnything(false);
setDraggedId(null);
setDropTargets(new Map());
if (!result.destination) {
console.log('🏁 CANCELLED');
return;
}
console.log('🏁 END:', {
from: result.source.droppableId,
to: result.destination.droppableId,
});
// Reorder logic here...
const newItems = handleReorder(items, result);
onReorder(newItems);
};
// === Render ===
return (
{/* ROOT DROPPABLE - for top-level sections and fields */}
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{items.map((item, index) => {
// === SECTION RENDERING ===
if (item.type === 'SECTION') {
const draggableId = section-${item.id};
const isExpanded = expandedSections.has(item.id);
return (
<Draggable
key={draggableId}
draggableId={draggableId}
index={index}
isDragDisabled={!!sidebarOpenForFieldId}
>
{(dragProvided, dragSnapshot) => (
<div
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
style={{
...dragProvided.draggableProps.style,
opacity: dragSnapshot.isDragging ? 0.5 : 1,
}}
>
<Group
section={item.data}
sectionChildren={item.children || []}
isExpanded={isExpanded}
isDragging={dragSnapshot.isDragging}
isDraggingAnything={isDraggingAnything}
isDraggingSection={draggedId?.startsWith('section-') ?? false}
dropTargetIndex={dropTargets.get(`section-${item.id}`) ?? null}
sidebarOpenForFieldId={sidebarOpenForFieldId}
dragHandleProps={dragProvided.dragHandleProps}
/>
</div>
)}
</Draggable>
);
}
// === FIELD RENDERING ===
const draggableId = `field-${item.id}`;
return (
<Draggable
key={draggableId}
draggableId={draggableId}
index={index}
isDragDisabled={!!sidebarOpenForFieldId}
>
{(dragProvided, dragSnapshot) => (
<div
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
style={{
...dragProvided.draggableProps.style,
opacity: dragSnapshot.isDragging ? 0.5 : 1,
}}
>
<Item
field={item.data}
isDragging={dragSnapshot.isDragging}
/>
</div>
)}
</Draggable>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);`
section,
sectionChildren,
isExpanded,
isDragging,
isDraggingAnything,
isDraggingSection,
dropTargetIndex,
sidebarOpenForFieldId,
dragHandleProps,
}) => {
const children = sectionChildren || [];
return (
<div>
<div {...dragHandleProps}>
<h3>{section.label}</h3>
<button onClick={() => console.log('toggle')}>
{isExpanded ? '▼' : '▶'}
</button>
</div>
{/* SECTION CONTENT - Collapsible */}
<div
style={{
maxHeight: isExpanded && !isDragging && !isDraggingSection ? 'none' : 0,
overflow: 'hidden',
transition: 'max-height 0.3s ease',
}}
>
{/* NESTED DROPPABLE - for fields inside this section */}
<Droppable
droppableId={`section-${section.id}`}
isDropDisabled={isDraggingSection} // CRITICAL: Disable when dragging sections
>
{(provided, snapshot) => {
console.log(`🎯 Section Droppable [${section.id}]:`, {
isDraggingOver: snapshot.isDraggingOver,
draggingOverWith: snapshot.draggingOverWith,
isDropDisabled: isDraggingSection,
isExpanded,
});
return (
<div ref={provided.innerRef} {...provided.droppableProps}>
{children.length === 0 ? (
<div>Empty section - drop fields here</div>
) : (
children.map((child, childIndex) => {
const draggableId = `field-${child.id}`;
return (
<Draggable
key={draggableId}
draggableId={draggableId}
index={childIndex}
isDragDisabled={!!sidebarOpenForFieldId}
>
{(dragProvided, dragSnapshot) => (
<div
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
style={{
...dragProvided.draggableProps.style,
opacity: dragSnapshot.isDragging ? 0.5 : 1,
}}
>
<CanvasFieldCard
field={child.data}
isDragging={dragSnapshot.isDragging}
/>
</div>
)}
</Draggable>
);
})
)}
{provided.placeholder}
</div>
);
}}
</Droppable>
</div>
</div>
);
};
export const Item: React.FC<CanvasFieldCardProps> = ({
field,
isDragging
}) => {
return (
<div
style={{
padding: '12px',
border: '1px solid #ccc',
borderRadius: '4px',
backgroundColor: '#fff',
opacity: isDragging ? 0.5 : 1,
}}
>
<strong>{field.label}</strong>
<p>Type: {field.type}</p>
</div>
);
};