Reputation: 4923
I have a react-beautiful-dnd app where the user can drag cards from one column to another. When I drag a card to a column with existing cards, the existing cards jump down. This makes sense if I'm trying to drag the card to the top of the column. However, the first card slides down even if I drag the new card to the middle or the list. And if I drag the new card to the bottom of the list everything above it slides down. Once I release the mouse button and the dragging is complete, the cards snap back to where they should be. Therefore, it's something to do with isDraggingOver
. I checked my styled components, but the only styles that change on isDraggingOver
are background colors and border colors. Has anyone else encountered this? If so, how did yo fix it. Thanks in advance!
Update 9/6:
After unsuccessfully trying @Jordan solution I looked at the GitHub issues for the project and found this. Sure enough, if I do not render provided.placeholder
the problem goes away. Of course, now I get a warning in the console about not rendering that placeholder. I then tried rendering it again but wrapping it in a call to React.cloneElement()
and passing {style: {height: '0'}}
as props. This does not work. So for now, I am simply not rendering the placeholder. Does anyone know another way around this? Here's a screenshot of the latest diff:
Update 8/25 Trying the solution of @jordan but I"m still seeing the issue. Here is my updated component file for the draggable component called Task
Draggable file:
import React, { memo, useState } from 'react'
import { Draggable } from 'react-beautiful-dnd'
import TaskForm from '../TaskForm/TaskForm'
import Task from '../Task/Task'
import { useStyletron } from 'baseui'
import useDraggableInPortal from '../../Hooks/useDraggableInPortal'
const TaskCard = ({ description, id, index, column }) => {
const renderDraggable = useDraggableInPortal()
const [isEditing, setIsEditing] = useState(false)
const [css, theme] = useStyletron()
const handleEdit = id => {
setIsEditing(true)
}
return (
<Draggable draggableId={id} index={index}>
{renderDraggable((provided, snapshot) => (
<div
className={css({
boxSizing: 'border-box',
':focus': {
outline: `${theme.colors.hotPink} 3px solid`,
borderRadius: '6px',
margin: '3px',
}
})}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}>
{!isEditing &&
<Task
column={column}
description={description}
handleEdit={handleEdit}
id={id}
snapshot={snapshot}
/>
}
{isEditing &&
<TaskForm
column={column}
handleOnCancel={() => setIsEditing(false)}
initialValues={{ description, id }} isEditing={true}
/>
}
</div>
))}
</Draggable>
)
}
export default memo(TaskCard)
Hook from @jordan :
import { createPortal } from 'react-dom'
import { useEffect, useRef } from 'react'
const useDraggableInPortal = () => {
const self = useRef({}).current
useEffect(() => {
const div = document.createElement('div')
div.style.position = 'absolute'
div.style.pointerEvents = 'none'
div.style.top = '0'
div.style.width = '100%'
div.style.height = '100%'
self.elt = div
document.body.appendChild(div)
return () => {
document.body.removeChild(div)
}
}, [self])
return (render) => (provided, ...args) => {
const element = render(provided, ...args)
if (provided.draggableProps.style.position === 'fixed') {
return createPortal(element, self.elt)
}
return element
}
}
export default useDraggableInPortal
Upvotes: 0
Views: 5202
Reputation: 23
I had this issue, and I found a solution to it here.
Basically when the library is using position: fixed, there are are some unintended consequences - and in those cases you need to use portal to get around them:
I had the same issue. Following the great solution of @kasperpihl, I created a simple hook do do the trick:
const useDraggableInPortal = () => {
const self = useRef({}).current;
useEffect(() => {
const div = document.createElement('div');
div.style.position = 'absolute';
div.style.pointerEvents = 'none';
div.style.top = '0';
div.style.width = '100%';
div.style.height = '100%';
self.elt = div;
document.body.appendChild(div);
return () => {
document.body.removeChild(div);
};
}, [self]);
return (render) => (provided, ...args) => {
const element = render(provided, ...args);
if (provided.draggableProps.style.position === 'fixed') {
return createPortal(element, self.elt);
}
return element;
};
};
Usage: Considering the following component:
const MyComponent = (props) => {
return (
<DragDropContext onDragEnd={/* ... */}>
<Droppable droppableId="droppable">
{({ innerRef, droppableProps, placeholder }) => (
<div ref={innerRef} {...droppableProps}>
{props.items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{item.title}
</div>
)}
</Draggable>
)}
{placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
Just call the hook and use the returned function to wrap the children callback of component:
const MyComponent = (props) => {
const renderDraggable = useDraggableInPortal();
return (
<DragDropContext onDragEnd={/* ... */}>
<Droppable droppableId="droppable">
{({ innerRef, droppableProps, placeholder }) => (
<div ref={innerRef} {...droppableProps}>
{props.items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{renderDraggable((provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{item.title}
</div>
))}
</Draggable>
))}
{placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
};
Another potential fix is to add a css rule to override whatever behaviour you've unintentionally set for your draggable:
HTML:
<div className="draggableClassName"
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
<div>
<input
//...etc.
index.css
.draggableClassName {
left: auto !important;
top: auto !important;
}
Upvotes: 2
Reputation: 4923
Okay, I finally figured this out. I feel rather silly. I had originally rendered the Droppable part of my React component tree like so:
<Droppable droppableId={column}>
{(provided, snapShot) => (
<TaskList
ref={provided.innerRef}
{...provided.droppableProps}
isDraggingOver={snapShot.isDraggingOver}
tasks={tasks}>
{provided.placeholder} // incorrect
</TaskList>
)}
</Droppable>
This makes no sense as my TaskList component does not take children
as a prop. I think I was trying to imitate some sample code that I had seen and I got confused.
The correct way to mark it up for my specific context is:
<Droppable droppableId={column}>
{(provided, snapShot) => (
<>
<TaskList
ref={provided.innerRef}
{...provided.droppableProps}
isDraggingOver={snapShot.isDraggingOver}
tasks={tasks}
/>
{provided.placeholder} // correct
</>
)}
</Droppable>
The Eureka moment was when I read the warning in the console which said that the provided.placeholder
must be a child of the Droppable
.
I have to put the provided.placeholder
after my TaskList. If it is the first child of the fragment I still have the original problem (the entire list slides down when you drag a new item onto it)
Upvotes: 0