The snippet below contains an array of 10 items. I'm able to drag and drop the list items and even able to achieve some basic animations when grabbing the list item:
const App = () => {
const [myArray, setMyArray] = React.useState([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const [draggedIndex, setDraggedIndex] = React.useState(-1);
const onDragStart = (e, index) => {
setDraggedIndex(index);
const emptyDiv = document.createElement('div');
emptyDiv.style.width = '0px';
emptyDiv.style.height = '0px';
e.dataTransfer.setDragImage(emptyDiv, 0, 0);
e.currentTarget.className = 'draggable';
};
const onMouseDown = (e) => {
e.currentTarget.className = 'draggable';
};
const onMouseUp = (e) => {
e.currentTarget.className = 'listItem';
};
const onDragOver = (e, index) => {
e.preventDefault();
if (draggedIndex === -1 || draggedIndex === index) {
return;
}
let items = myArray.filter((item, i) => i !== draggedIndex);
items.splice(index, 0, myArray[draggedIndex]);
setMyArray(items);
setDraggedIndex(index);
};
const onDragEnd = (e) => {
setDraggedIndex(-1);
e.target.className = 'listItem';
};
return (
<div className="App">
{myArray.map((x, i) => (
<div
className="listItem"
draggable
key={x}
onDragStart={(e) => onDragStart(e, i)}
onDragOver={(e) => onDragOver(e, i)}
onDragEnd={onDragEnd}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
>
<h3>hello - {x}</h3>
</div>
))}
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
.App {
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
}
.listItem {
border: 2px solid black;
margin: 5px;
width: 400px;
cursor: grab;
transform: scale(100%);
transition: transform 0.3s ease-in-out;
}
.draggable {
border: 2px solid green;
margin: 5px;
width: 400px;
cursor: grab;
transform: scale(108%);
transition: transform 0.3s ease-in-out;
}
.listItem:-moz-drag-over {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Can I get help with CSS animations to make the list item movements smoother during dragging so it looks less choppy? The goal is to achieve the following effect - when I drag an item it would smoothly reposition itself up/down, and the item that is being dragged over would smoothly move in the opposite direction.
Basically, what I want is to animate the transition of the dragged item depending on which direction (up or down) the item is being dragged. In theory, while dragging an item up, it could apply a class something like .dragged-up and that class would have animation/transition that would create an illusion that that item moving from the lower to the higher position.
The same principle could be applied to the items above and below the item being dragged. For example, If the item that is being dragged over moves from the top to the bottom, a different class could be applied, something like .listItem-down, and that class could contain an opposite animation. Also, I suspect it would need to have a lower z-index so it would appear below the dragged item.
Not sure if it's the most efficient approach and if it's possible to do it that way at all. So far, while trying to implement something like this, I've been getting issues of items overlapping and as a result, the event function was being executed on the wrong div, causing some undesired effects.
Some help and a working snippet would be much appreciated!
This answer is inspired by this solution, and attempts to make a greatly simplified port of its main ideas to functional React components that works for the draggable elements in the use case.
In the posted example, order of items in the array is updated on every event of dragging over. To create a transition when the reorder happens, the difference before and after the change for each item can be detected, and used as the starting and ending points for the animation.
The following approach assigns a keyed ref for each item to keep track of the updates, and check for the changes in their rendered position with getBoundingClientRect in a useLayoutEffect, so that further actions can be taken before the browser repaints the screen.
In order to calculate the differences, the position of items in the last render prevPos is stored separately as another ref, so that it persists between renders. In this simplified example, only top position is checked and calculated for a difference, to create an offset for translateY to happen.
Then to arrange for the transition, requestAnimationFrame is called two times, with the first frame rendering the items in the offset positions (starting point, with offset in translateY), and the second their in new natural positions (ending point, with 0 in translateY).
While at this point useLayoutEffect already handle the animations as expected, the fact that onDragOver triggers and updates the state very often could easily cause errors in the motion display.
I tried to implement some basic debouncing for the update of the state array and introduced another useEffect to handle the debounced update, but it seems that the effects might be still occasionally unstable.
While lots of improvements could still be done, here is the experimental example:
const App = () => {
const [myArray, setMyArray] = React.useState([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
]);
const [draggedKey, setDraggedKey] = React.useState(null);
const [pendingNewKey, setPendingNewKey] = React.useState(null);
const elRef = React.useRef({});
const keyInAnimation = React.useRef(null);
const prevPos = React.useRef({});
// 👇 Attempt to debounce update of array
React.useEffect(() => {
if (
pendingNewKey === null ||
draggedKey === null ||
draggedKey === pendingNewKey ||
keyInAnimation.current === draggedKey
)
return;
const updateArray = () => {
setMyArray((prev) => {
const prevIndex = prev.findIndex((x) => x === draggedKey);
const newIndex = prev.findIndex((x) => x === pendingNewKey);
const newArray = [...prev];
newArray[prevIndex] = pendingNewKey;
newArray[newIndex] = draggedKey;
return newArray;
});
};
const debouncedUpdate = setTimeout(updateArray, 100);
return () => clearTimeout(debouncedUpdate);
}, [pendingNewKey, draggedKey]);
React.useLayoutEffect(() => {
Object.entries(elRef.current).forEach(([key, el]) => {
if (!el) return;
// 👇 Get difference in position to calculate an offset for transition
const { top } = el.getBoundingClientRect();
if (!prevPos.current[key] && prevPos.current[key] !== 0)
prevPos.current[key] = top;
const diffTop = Math.floor(prevPos.current[key] - top);
if (diffTop === 0 || Math.abs(diffTop) < 30) return;
prevPos.current[key] = top;
el.style.transform = `translateY(${diffTop}px)`;
el.style.transition = 'scale 0.3s ease-in-out, transform 0s';
// 👇 First frame renders offset positions, second the transition ends
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (!el) return;
el.style.transform = `translateY(0px)`;
el.style.transition =
'scale 0.3s ease-in-out, transform 100ms ease-out';
});
});
});
}, [myArray.toString()]);
const onDragStart = (e, key) => {
keyInAnimation.current = key;
setDraggedKey(key);
const emptyDiv = document.createElement('div');
emptyDiv.style.width = '0px';
emptyDiv.style.height = '0px';
e.dataTransfer.setDragImage(emptyDiv, 0, 0);
e.currentTarget.className = 'draggable';
};
const onMouseDown = (e) => {
e.currentTarget.className = 'draggable';
};
const onMouseUp = (e) => {
e.currentTarget.className = 'listItem';
};
const onDragOver = (e, key) => {
e.preventDefault();
if (draggedKey === null) return;
if (draggedKey === key) {
keyInAnimation.current = key;
setPendingNewKey(null);
return;
}
if (keyInAnimation.current === key) {
return;
}
keyInAnimation.current = key;
setPendingNewKey(key);
// 👇 Attempt to reduce motion error but could be unnecessary
Object.values(elRef.current).forEach((el) => {
if (!el) return;
el.style.transform = `translateY(0px)`;
el.style.transition = 'scale 0.3s ease-in-out, transform 0s';
});
};
const onDragEnd = (e) => {
setDraggedKey(null);
setPendingNewKey(null);
keyInAnimation.current = null;
e.target.className = 'listItem';
};
return (
<div className="App">
{myArray.map((x) => (
<div
className="listItem"
draggable
key={x}
onDragStart={(e) => onDragStart(e, x)}
onDragOver={(e) => onDragOver(e, x)}
onDragEnd={onDragEnd}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
ref={(el) => (elRef.current[x] = el)}
>
<h3>hello - {x}</h3>
</div>
))}
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"));
.App {
text-align: center;
align-items: center;
display: flex;
flex-direction: column;
isolation: isolate;
gap: 15px;
}
.listItem {
border: 2px solid black;
margin: 5px;
width: 400px;
cursor: grab;
z-index: 1;
transition: scale 0.3s ease-in-out;
background-color: white;
}
.draggable {
border: 2px solid hotpink;
margin: 5px;
width: 400px;
cursor: grab;
scale: 108%;
z-index: 10;
transition: scale 0.3s ease-in-out;
background-color: white;
}
.listItem:-moz-drag-over {
cursor: pointer;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js"></script>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With