I am trying to build a game in React, It needs a Drag-and-drop feature. The game has two major components, SideBar and a GameBoard. SideBar contains elements to be dragged onto GameBoard. The Drag and Drop works, but the element is getting dropped at the incorrect position (Offset from the position of the cursor), I looked for resources online on how to drop at the position of cursor but most of the answers are only using a third party package and the ones that are not do not really have an answer
You can see the output of the below code here: https://gist.github.com/assets/31410839/48cbdaae-2323-461e-a660-286d0fc971bd
Here is complete code
App.tsx which holds the SideBar and GameBoard with draggedElement state
// App.tsx
import React, { useState } from "react";
import GameBoard from "../src/components/GameBoard/GameBoard";
import SideBar from "../src/components/SideBar/SideBar";
import { ElementData } from "./Types";
function App() {
const [draggedElement, setDraggedElement] = useState<ElementData | null>(null);
return (
<div className="min-h-screen flex flex-col md:flex-row bg-gray-100">
<div className="md:w-1/5 bg-fuchsia-50 p-4">
<h1 className="text-xl font-bold mb-4">Infinite Things ♾️</h1>
<SideBar setDraggedElement={setDraggedElement} />
</div>
<div className="md:w-4/5 bg-amber-50 flex-grow">
<GameBoard draggedElement={draggedElement} />
</div>
</div>
);
}
export default App;
GameBoard.tsx
import React, { useState } from "react";
import Element from "../Element";
import { ElementData } from "../../Types";
interface GameBoardProps {
draggedElement: ElementData | null;
}
const GameBoard: React.FC<GameBoardProps> = ({ draggedElement }) => {
const [elements, setElements] = useState<ElementData[]>([
{ id: "Water", dx: 0, dy: 0, color: "#3498db" },
{ id: "Fire", dx: 0, dy: 0, color: "#e74c3c" },
{ id: "Wind", dx: 0, dy: 0, color: "#2ecc71" },
]);
const [draggingElement, setDraggingElement] = useState<ElementData | null>(null);
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.currentTarget.style.border = "none";
if (draggedElement) {
// Check if the dropped element is from the sidebar or internal drag
const isExternalDrop = !draggingElement; // Corrected this line
if (isExternalDrop) {
// External drop: add the dragged element from the sidebar
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const newElement = { ...draggedElement, dx: x, dy: y };
setElements((prevElements) => [...prevElements, newElement]);
} else {
// Internal drop: update the position of the dragged element
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const updatedElement = { ...draggingElement, dx: x, dy: y };
setElements((prevElements) =>
prevElements.map((el) => (el.id === draggingElement.id ? updatedElement : el))
);
}
}
// Reset the dragging element in the state
setDraggingElement(null);
};
const handleDragStart = (
e: React.DragEvent<HTMLDivElement>,
element: ElementData
) => {
console.log("drag start", element.id);
// Set the dragged element in the state
setDraggingElement(element);
// Set data to be transferred during the drag
e.dataTransfer.setData("elementId", element.id.toString());
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
// Check if there's a dragging element
if (draggingElement) {
// Calculate new position based on cursor location
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Update dx and dy in draggingElement (or state)
setDraggingElement((prevElement) => {
if (prevElement) {
return { ...prevElement, dx: x, dy: y };
}
return prevElement;
});
}
};
const handleDragEnd = () => {
// Reset the dragging element in the state
setDraggingElement(null);
};
const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.currentTarget.style.border = "2px dashed #333";
// Get the elementId from dataTransfer only if draggingElement is null
if (!draggingElement && draggedElement) {
console.log("drag enter", draggedElement?.id);
setDraggingElement(draggedElement);
}
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.currentTarget.style.border = "none";
};
return (
<div
className="game-board bg-gray-200 relative h-full p-4"
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragEnd={handleDragEnd}
>
<h2 className="text-xl font-bold mb-4">Game Board</h2>
<div className="min-w-0 absolute">
{elements.map((element) => (
<Element
key={element.id}
element={element}
onDragStart={(e) => {
e.dataTransfer.setData("elementId", element.id.toString());
handleDragStart(e, element);
}}
/>
))}
</div>
</div>
);
};
export default GameBoard;
Sidebar which holds the elements
// SideBar.tsx
import React from "react";
import Element from "../Element";
import { ElementData } from "../../Types";
interface SideBarProps {
setDraggedElement: React.Dispatch<React.SetStateAction<ElementData | null>>;
}
const SideBar: React.FC<SideBarProps> = ({ setDraggedElement }) => {
const elements: ElementData[] = [
{ id: "Water", dx: 0, dy: 0, color: "#3498db" },
{ id: "Fire", dx: 0, dy: 0, color: "#e74c3c" },
{ id: "Wind", dx: 0, dy: 0, color: "#2ecc71" },
];
const handleDragStart = (
e: React.DragEvent<HTMLDivElement>,
element: ElementData
) => {
console.log("drag start in Sidebar for", element.id);
setDraggedElement(element);
};
return (
<div className="sidebar">
<h2 className="text-xl font-bold mb-4">Side Bar</h2>
<div className="grid grid-cols-2 gap-4">
{elements.map((element) => (
<Element
key={element.id}
element={element}
onDragStart={(e) => handleDragStart(e, element)}
/>
))}
</div>
</div>
);
};
export default SideBar;
Element component which will be dragged
// Element.tsx
import React from "react";
import { ElementData } from "../Types";
interface ElementProps {
element: ElementData;
onDragStart: (e: React.DragEvent<HTMLDivElement>, element: ElementData) => void;
}
function Element({ onDragStart, element }: ElementProps) {
return (
<div
className={`element p-4 min-w-4 text-white text-center font-bold rounded-2xl shadow-lg cursor-pointer`}
onDragStart={(e) => {
onDragStart(e, element);
}}
style={{
background: `${element.color}`,
transform: `translate(${element.dx}px, ${element.dy}px)`,
}}
draggable
>
{element.id}
</div>
);
}
export default Element;
Apologies if the code looks messy, I am a beginner in React.
In
handleDrop()inGameBoard, you need to fully account for the starting position of the element being repositioned. This is becausetranslate()moves the element relative to its "starting" position.