17

How to implement drag and drop for Facebooks' react.js with support for touch events?

There's a couple of questions and articles and libraries about drag and drop for react.js, but none of them seems to mention touch events, and none of the demo's work on my phone.

In general I wonder what would be the easiest: Try to implement this using existing d&d libraries which already support touch, but may need some work to properly work together with react. Or try to use any of the react d&d examples and make them work with touch (which, seeing this issue, may not be trivial?)

Community
  • 1
  • 1
Flion
  • 10,468
  • 13
  • 48
  • 68

3 Answers3

8

react-motion (with touch events)

We have tried "react-motion" for dragging items in a list. With more than 15-20 items it becomes really laggy. (But with small list it works good, like in this demo). Be aware that mobile devices are much slower than desktops.

Important note about react-motion: Don't forget to use production mode when testing your animation's performance!

react-dnd (with touch events)

The second option was "react-dnd". It is a great library. It is low level, however, it is rather easy to understand how to work with it. But at first, "react-dnd" was not an option for us because of no touch events support.

Later, when Yahoo had released react-dnd-touch-backend we decided to switch our app from "react-motion" to "react-dnd". This solved all our performance issues. We have list 50-70 items and it just works as expected.

Yahoo has done really good work and the solution works in our production apps.

koorchik
  • 1,646
  • 2
  • 13
  • 10
  • I couldn't get this to work with React + ES6 at all. I got it to render, but any interaction with the elements wouldn't update the style properties to be useful. :[ – Brad Gunn Jan 22 '16 at 21:55
  • Perfect library for my case – Vitalii Del Vorobioff Mar 08 '16 at 14:20
  • @BradGunn I am not sure that your case is related to ES6. The only issue with using React ES6 Classes syntax is in autobinding. Check you "this" if it references to correct object – koorchik Mar 08 '16 at 22:56
  • [react-dnd-touch-backend](https://github.com/yahoo/react-dnd-touch-backend) does not seem to be supported anymore as heavily. https://github.com/atlassian/react-beautiful-dnd could be something to consider for touch support especially when using lists. – Kerem Nov 29 '18 at 09:32
6

You already mentioned react-dnd and I make PR that enable dnd for touch devices so you can try it

anton_byrna
  • 2,477
  • 1
  • 18
  • 31
  • true, the touchbackend works now and is already integrated in the project and mentioned on the website. One thing I noted though is that when I used the touchbackend, the mouse drag and drop didnt work anymore on my laptop which supports both mouse&touch. – Flion Dec 16 '15 at 19:54
  • @Flion There's a setting you can pass to the TouchBackend to enable mouse events. – Prashanth Chandra Apr 05 '16 at 01:28
2

I haven't found any answer to this yet. The accepted answer is not really an answer but it points to a github library. I am going to try to include here a complete answer using only react.

Here it goes, the code should be self explanatory, but a couple of words ahead of time. We need to use a lot of state variables to keep state between renders, otherwise any variables get reset out. To make the transitions smooth, I update the position once a render was completed using useEffect hook. I tested this in codesandbox, I'm including the link here for anybody to edit the code and play with it, just fork it. It workd with the MS Surface Book2 Pro and Android. It has a formatting problem with the iPhone IOS. Both for Safari and Chrome. If somebody fixes it that'd be great. For now I have what I need and claim success.

Here are the files under src in codesandbox.io:

App.js

import "./styles/index.pcss";

import "./styles/tailwind-pre-build.css";
import Photos from "./Photos.js";

export default function App() {
  return (
    <>
      <div className="flow-root bg-green-200">
        <div className="my-4 bg-blue-100 mb-20">
          Drag and Drop with touch screens
        </div>
      </div>
      <div className="flow-root bg-red-200">
        <div className="bg-blue-100">
          <Photos />
        </div>
      </div>
    </>
  );
}

Photos.js:

import React, { useState } from "react";

import "./styles/index.pcss";
import Image from "./image";

export default function Photos() {
  const [styleForNumber, setStyleForNumber] = useState({
    position: "relative",
    width: "58px",
    height: "58px"
  });

  const photosArray = [
    "https://spinelli.io/noderestshop/uploads/G.1natalie.1642116451444",
    "https://spinelli.io/noderestshop/uploads/G.2natalie.1642116452437",
    "https://spinelli.io/noderestshop/uploads/G.3natalie.1642116453418",
    "https://spinelli.io/noderestshop/uploads/G.4natalie.1642116454396",
    "https://spinelli.io/noderestshop/uploads/G.5natalie.1642116455384",
    "https://spinelli.io/noderestshop/uploads/G.6natalie.1642116456410",
    "https://spinelli.io/noderestshop/uploads/G.7natalie.1642116457466",
    "https://spinelli.io/noderestshop/uploads/G.8natalie.1642116458535",
    "https://spinelli.io/noderestshop/uploads/G.0natalie.1642116228246"
  ];

  return (
    <>
      <div
        className="w-1/2 bg-green-200"
        style={{
          display: "grid",
          gridTemplateColumns: "[first] 60px [second] 60px [third] 60px",
          gridTemplateRows: "60px 60px 60px",
          rowGap: "10px",
          columnGap: "20px",
          position: "relative",
          justifyContent: "center",
          placeItems: "center"
        }}
      >
        {photosArray.map((photo, i) => (
          <div
            className="relative z-1 h-full w-full flex flex-wrap content-center touch-none"
            key={i}
          >
            <div className="contents">
              <Image photo={photo} i={i} />
            </div>
          </div>
        ))}
      </div>
    </>
  );
}

Image.js:

import React, { useRef, useState, useEffect } from "react";

import "./styles/index.pcss";

export default function Image({ photo, i }) {
  const imgRef = useRef();
  const [top, setTop] = useState(0);
  const [left, setLeft] = useState(0);
  const [drag, setDrag] = useState(false);
  const [styleForImg, setStyleForImg] = useState({
    position: "absolute",
    width: "58px",
    height: "58px"
  });
  const [offsetTop, setOffsetTop] = useState(-40);
  const [offsetLeft, setOffsetLeft] = useState(0);
  const [xAtTouchPointStart, setXAtTouchPointStart] = useState(0);
  const [yAtTouchPointStart, setYAtTouchPointStart] = useState(0);

  useEffect(() => {
    if (drag) {
      setStyleForImg({
        position: "relative",
        width: "58px",
        height: "58px",
        top: top,
        left: left
      });
    } else {
      setStyleForImg({
        position: "relative",
        width: "58px",
        height: "58px"
      });
    }
    console.log("style: ", styleForImg);
  }, [drag, top, left]);

  const handleTouchStart = (e, i) => {
    e.preventDefault();
    let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
    let touch = evt.touches[0] || evt.changedTouches[0];
    const x = +touch.pageX;
    const y = +touch.pageY;
    console.log(
      "onTouchStart coordinates of icon @ start: X: " + x + " | Y: " + y
    );
    console.log("dragged from position n = ", i + 1);
    // get the mouse cursor position at startup:
    setXAtTouchPointStart(x);
    setYAtTouchPointStart(y);
    setDrag(true);
  };

  const handleTouchEnd = (e) => {
    // if (process.env.NODE_ENV === 'debug5' || process.env.NODE_ENV === 'development') {
    e.preventDefault();
    setDrag(false);
    console.log(
      new Date(),
      "onTouchEnd event, coordinates of icon @ end: X: " +
        e.changedTouches[0]?.clientX +
        " | Y: " +
        e.changedTouches[0]?.clientY +
        " | top: " +
        top +
        " | left: " +
        left
    );
  };

  const handleElementDrag = (e) => {
    e = e || window.event;
    e.preventDefault();
    let x = 0;
    let y = 0;

    //Get touch or click position
    //https://stackoverflow.com/a/41993300/5078983
    if (
      e.type === "touchstart" ||
      e.type === "touchmove" ||
      e.type === "touchend" ||
      e.type === "touchcancel"
    ) {
      let evt = typeof e.originalEvent === "undefined" ? e : e.originalEvent;
      let touch = evt.touches[0] || evt.changedTouches[0];
      x = +touch.pageX; // X Coordinate relative to the viewport of the touch point
      y = +touch.pageY; // same for Y
    } else if (
      e.type === "mousedown" ||
      e.type === "mouseup" ||
      e.type === "mousemove" ||
      e.type === "mouseover" ||
      e.type === "mouseout" ||
      e.type === "mouseenter" ||
      e.type === "mouseleave"
    ) {
      x = +e.clientX;
      y = +e.clientY;
    }
    console.log("x: ", x, "y: ", y);
    // calculate the new cursor position:
    const xRelativeToStart = x - xAtTouchPointStart;
    console.log(
      "xRel = ",
      x,
      " - ",
      xAtTouchPointStart,
      " = ",
      xRelativeToStart
    );
    const yRelativeToStart = y - yAtTouchPointStart;
    console.log(
      "yRel = ",
      y,
      " - ",
      yAtTouchPointStart,
      " = ",
      yRelativeToStart
    );
    // setXAtTouchPointStart(x); // Reseting relative point to current touch point
    // setYAtTouchPointStart(y);
    // set the element's new position:
    setTop(yRelativeToStart + "px");
    setLeft(xRelativeToStart + "px");
    console.log("top: ", yRelativeToStart + "px");
    console.log("Left: ", xRelativeToStart + "px");
  };

  const handleDragEnd = (e) => {
    // if (process.env.NODE_ENV === 'debug5' || process.env.NODE_ENV === 'development') {
    console.log(
      new Date(),
      "Coordinates of icon @ end X: " + e.clientX + " | Y: " + e.clientY
    );
  };

  const handleDragStart = (e, i) => {
    // From https://stackoverflow.com/a/69109382/15355839
    e.stopPropagation(); // let child take the drag
    e.dataTransfer.dropEffect = "move";
    e.dataTransfer.effectAllowed = "move";
    console.log(
      "Coordinates of icon @ start: X: " + e.clientX + " | Y: " + e.clientY
    );
    // console.log ('event: ', e)
    console.log("dragged from position n = ", i + 1);
  };
  return (
    <img
      ref={imgRef}
      className="hover:border-none border-4 border-solid border-green-600 mb-4"
      src={photo}
      alt="placeholder"
      style={styleForImg}
      onDragStart={(e) => handleDragStart(e, i)}
      onDragEnd={handleDragEnd}
      onTouchStart={(e) => handleTouchStart(e, i)}
      onTouchEnd={handleTouchEnd}
      onTouchMove={handleElementDrag}
    ></img>
  );
}

index.js:

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import "./styles/index.pcss";

import App from "./App";

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

styles.css:

.Main {
  font-family: sans-serif;
  text-align: center;
}

/styles/index.pcss:

@tailwind base;

@tailwind components;

@tailwind utilities;

I couldn't make tailwinds grid work, so I used the actual css inline styles. No idea why they didn't in codesandbox.

Julio Spinelli
  • 587
  • 3
  • 16