Setup: A kanban style component, with draggable columns that contain draggable cards.
The columns are able to render correctly when you drag a column and reorder it. This is done by setting the Index of the column that is dragging when its “onDragStart” event is fired. Then, when another column fires an “onDragOver” event, the array of columns is spliced, to remove the dragging column from it’s original position and insert it into the new order. This is working fine.
The issue arises when I try to drag cards, instead of the columns. When the “onDragStart” event fires on a card, I am setting a flag in a state hook. setDragCategory(“card”). The event listeners on the column elements are supposed to check to see if the “dragCategory === ‘card'”. And if it does, they are supposed to exit the function and not run any of the code.
The goal is that when you start dragging on a column, all of its event listeners fire, per normal. But if you start dragging on a card, the columns’ event listeners are essentially deactivated via exiting them before they do anything.
Even though the “onDragStart” handler on the card is running first (where the state is set to dragCategory === “card”, it is not preventing the columns’ event handlers from running. The column’s event handlers are then setting dragCategory === “column.” So, I a trying to drag a card, but instead, the columns are reordering.
I do not understand why the column’s event listeners are not exiting their handlers before this can happen.
Thank you for any pointers!
This code should work, if you paste it directly into the App.js file of a create-react-app
project.
App.js:
import React, { useState } from "react";
import { v4 as uuid } from "uuid";
import "./App.css";
const data = {};
data.columns = [
{ name: "zero", cards: [{ text: "card one" }, { text: "card two" }] },
{ name: "one", cards: [{ text: "card three" }, { text: "card four" }] },
{ name: "two", cards: [{ text: "card five" }, { text: "card six" }] },
{ name: "three", cards: [{ text: "card seven" }, { text: "card eight" }] },
{ name: "four", cards: [{ text: "card nine" }, { text: "card ten" }] },
{ name: "five", cards: [{ text: "card eleven" }, { text: "card twelve" }] },
];
function App() {
// when a card starts to drag, dragCategory is set to "card." This is supposed to be a flag that will stop the columns' event listeners before they cause any changes in the state.
let [dragCategory, setDragCategory] = useState(null);
// all of this is for reordering the columns. I have not gotten it to work well enough yet, to be able to write the state for reordering the cards:
let [columns, setColumns] = useState(data.columns);
let [draggingIndex, setDraggingIndex] = useState(null);
let [targetIndex, setTargetIndex] = useState(null);
return (
<div className="App">
<header>drag drop</header>
<div className="kanban">
{columns.map((column, i) => {
return (
<div
data-index={i}
onDragStart={(e) => {
console.log("column drag start");
if (dragCategory === "card") {
e.preventDefault();
return null;
}
setDragCategory("column");
setDraggingIndex(i);
}}
// using onDragOver instead of onDragEnter because the onDragEnter handler causes the drop animation to return to the original place in the DOM instead of the current position that it should drop to.
onDragOver={(e) => {
if (dragCategory === "card") return null;
// allows the drop event
e.preventDefault();
setTargetIndex(i);
if (
dragCategory === "column" &&
targetIndex != null &&
targetIndex != draggingIndex
) {
let nextColumns = [...columns];
let currentItem = nextColumns[draggingIndex];
// remove current item
nextColumns.splice(draggingIndex, 1);
// insert item
nextColumns.splice(targetIndex, 0, currentItem);
setColumns(nextColumns);
setTargetIndex(i);
setDraggingIndex(i);
}
}}
onDragEnter={(e) => {}}
onDragEnd={(e) => {
setDragCategory(null);
}}
onDrop={(e) => {}}
className="column"
key={uuid()}
draggable={true}
>
{column.name}
{column.cards.map((card) => {
return (
<div
onDragStart={(e) => {
setDragCategory("card");
}}
key={uuid()}
className="card"
draggable={true}
>
{card.text}
</div>
);
})}
</div>
);
})}
</div>
</div>
);
}
export default App;
And paste this starter css in the App.css file.
App.css
.kanban {
display: flex;
height: 90vh;
}
.column {
border: solid orange 0.2rem;
flex: 1;
}
.card {
height: 5rem;
width: 90%;
margin: auto;
margin-top: 2rem;
border: solid gray 0.2rem;
}