Search code examples
reactjsdrag-and-dropcss-positioncss-transformspointer-events

Position of element following cursor offset by container content


I am trying to create a drag and drop feature that is a child component within a modal where the currently dragged item follows the position of the mouse, however, the position of the mouse is being offset by other content in the modal not part of the drag and drop component. Here is a repl showing the issue I would like for the element to follow the cursor exactly.

Showing the relation between cursor position and modal content

The basic element structure is as follows:

<Main>
  <Modal>
    <h2>Modal Header</h2>

    <DndComponent>
      <div>Drag Me</div>
      <div>Element that follows cursor</div>
    </DndComponent>

  </Modal>
</Main>

I have tried different combinations of e.clientY, e.offsetY, and e.pageY in combination with getBoundingClientRect from the component container. I would prefer to not put a ref on the modal as the drag and drop feature will be used in many different places. I have also tried using both position: absolute and position: fixed


Solution

  • Consider applying position: absolute to the dragitem. This ensures the "start" position of the element is relative to the boundsRef element, which is where the mousePos is calculated relative to.

    const { useState, useRef } = React;
    
    function App() {
      const [isDragging, setIsDragging] = useState(false);
      const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
      const boundsRef = useRef(null);
    
      const handleMouseDown = () => {
        setIsDragging(true);
      };
    
      const handleMouseMove = (e) => {
        const bounds = boundsRef.current.getBoundingClientRect();
        if (isDragging) {
          setMousePos({ x: e.clientX - bounds.x, y: e.clientY - bounds.y });
        }
      };
    
      return (
        <main>
          <div className="container" onPointerMove={handleMouseMove}>
            <div className="modal">
              <h3>Modal Header</h3>
              <h4 style={{ marginBottom: "20px" }}>Sub Header</h4>
    
              {/* start of child component */}
              <div ref={boundsRef} style={{ position: "relative" }}>
                <ul>
                  <li className="modal-content" onPointerDown={handleMouseDown}>
                    <p>Drag Me</p>
                  </li>
                </ul>
    
                {isDragging && (
                  <div
                    className="dragItem"
                    style={{
                      transform: `translate(${mousePos.x}px, ${mousePos.y}px)`,
                    }}
                  >
                    <p>Item Being Dragged</p>
                  </div>
                )}
              </div>
              {/* end of child component */}
            </div>
          </div>
        </main>
      );
    }
    
    ReactDOM.createRoot(document.getElementById("app")).render(<App />);
    * {
      margin: 0;
      padding: 0;
    }
    
    .container {
      position: relative;
      height: 100vh;
      width: 100vw;
      background-color:#fff;
    }
    
    .modal {
      height: 50vh;
      width: 50vw;
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: #ededed;
    }
    
    .modal-content {
      user-select: none;
    }
    
    .modal-content:hover {
      cursor: grab;
    }
    
    .dragItem {
      position: absolute;
      z-index: 1000;
      left: 0;
      top: 0;
      pointer-events: none;
      background-color: #8593ff24;
      padding: 0.5rem 1rem;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js" integrity="sha512-8Q6Y9XnTbOE+JNvjBQwJ2H8S+UV4uA6hiRykhdtIyDYZ2TprdNmWOUaKdGzOhyr4dCyk287OejbPvwl7lrfqrQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js" integrity="sha512-MOCpqoRoisCTwJ8vQQiciZv0qcpROCidek3GTFS6KTk2+y7munJIlKCVkFCYY+p3ErYFXCjmFjnfTTRSC1OHWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    
    <div id="app"></div>