maininformer
maininformer

Reputation: 1077

Drag and Drop in React using hooks

I'm mainly a backend engineer and have been trying to implement, and failing, a simple drag and drop for a slider I am making in React.

First I will show you the behavior without using debounce: no-debounce

And here is the behavior with debounce: with-debounce

The debounce I took from here with a little modification.

I think I have two problems, one is fast flickering, which debounce should solve, and the other is the incorrect left which I cannot figure how to fix. For some reason, onDrag, rect.left has all the left margins of the parents (100 + 10) added to it as well. This happens on Chrome and Safari.

My question is, how do I make this drag and drop work? What am I doing wrong? My code is below:

 import React, {
   Dispatch,
   MouseEvent,
   RefObject,
   SetStateAction,
   useEffect,
   useRef,
   useState
 } from "react";

 const useDebounce = (callback: any, delay: number) => {
   const latestCallback = useRef(callback);
   const latestTimeout = useRef(1);

   useEffect(() => {
     latestCallback.current = callback;
   }, [callback]);

   return (obj: any) => {
     const { event, args } = obj;
     event.persist();
     if (latestTimeout.current) {
       clearTimeout(latestTimeout.current);
     }

     latestTimeout.current = window.setTimeout(
       () => latestCallback.current(event, ...args),
       delay
     );
   };
 };

 const setPosition = (
   event: any,
   setter: any
 ) => {
     const rect = event.target.getBoundingClientRect();
     const clientX: number = event.pageX;
     console.log('clientX: ', clientX)
     // console.log(rect.left)
     setter(clientX - rect.left)
 };

 const Slider: React.FC = () => {
   const [x, setX] = useState(null);
   const handleOnDrag = useDebounce(setPosition, 100)

   return (
     <div style={{ position: "absolute", margin: '10px' }}>
       <div
         style={{ position: "absolute", left: "0", top: "0" }}
         onDragOver={e => e.preventDefault()}
       >
         <svg width="300" height="10">
           <rect
             y="5"
             width="300"
             height="2"
             rx="10"
             ry="10"
             style={{ fill: "rgb(96,125,139)" }}
           />
         </svg>
       </div>
       <div
         draggable={true}
         onDrag={event => handleOnDrag({event, args: [setX]})}
         // onDrag={event => setPosition(event, setX)}
         style={{
           position: "absolute",
           left: (x || 0).toString() + "px",
           top: "5px",
           width: "10px",
           height: "10px",
           padding: "0px",
         }}
       >
         <svg width="10" height="10" style={{display:"block"}}>
           <circle cx="5" cy="5" r="4" />
         </svg>
       </div>
     </div>
   );
 };

Thank you.

Upvotes: 1

Views: 3428

Answers (1)

johnny peter
johnny peter

Reputation: 4862

Draggable and onDrag hast its own woes. Tried my hand with simple mouse events.

You can find a working code in the following sandbox

Edit twilight-shadow-og8v7


The source for it is as follows

import React, { useRef, useState, useEffect, useCallback } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const isDragging = useRef(false);
  const dragHeadRef = useRef();
  const [position, setPosition] = useState(0);

  const onMouseDown = useCallback(e => {
    if (dragHeadRef.current && dragHeadRef.current.contains(e.target)) {
      isDragging.current = true;
    }
  }, []);

  const onMouseUp = useCallback(() => {
    if (isDragging.current) {
      isDragging.current = false;
    }
  }, []);

  const onMouseMove = useCallback(e => {
    if (isDragging.current) {
      setPosition(position => position + e.movementX);
    }
  }, []);

  useEffect(() => {
    document.addEventListener("mouseup", onMouseUp);
    document.addEventListener("mousedown", onMouseDown);
    document.addEventListener("mousemove", onMouseMove);
    return () => {
      document.removeEventListener("mouseup", onMouseUp);
      document.removeEventListener("mousedown", onMouseDown);
      document.removeEventListener("mousemove", onMouseMove);
    };
  }, [onMouseMove, onMouseDown, onMouseUp]);

  return (
    <div
      style={{
        flex: "1",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        height: "100vh"
      }}
    >
      <div
        style={{
          height: "5px",
          width: "500px",
          background: "black",
          position: "absolute"
        }}
      >
        <div
          ref={dragHeadRef}
          style={{
            left: `${position}px`,
            transition: 'left 0.1s ease-out',
            top: "-12.5px",
            position: "relative",
            height: "30px",
            width: "30px",
            background: "black",
            borderRadius: "50%"
          }}
        />
      </div>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

The crux of the logic is e.movementX which returns the amount of distance moved along x axis by the mouse since the last occurrence of that event. Use that to set the left position of the dragHeader

Upvotes: 2

Related Questions