Search code examples
javascriptreactjstouch-eventarrow-keystouchstart

Touch support for arrow clicks on React app


I am working on a React web app where I have a div of a 4x4 grid which has 16 blocks rendered in them. Where I have arrow click events for right, left, up, and down arrow. I need to add the mobile touch support for those arrow clicks when it's accessed only on mobile devices. I have never implemented these touch/swipe mobile features hence it seems very confusing. Any help would be appreciated.

Component:

const Grid = () => {
  const [grid, setGrid] = useState(new Grid());

  const arrowLeftKey = 37;
  const arrowDownKey = 40;

  const handleKeyDown = (event) => {
    if (grid.hasWon()) {
      return;
    }

    if (event.keyCode >= arrowLeftKey && event.keyCode <= arrowDownKey) {
      let direction = event.keyCode - arrowLeftKey;
      let gridClone = Object.assign(
        Object.create(Object.getPrototypeOf(grid)),
        grid
      );
      let newGrid = gridClone.move(direction);
      setGrid(newGrid);
    }
  };

  useArrowKeyEvent('keydown',handleKeyDown); //hook

  const displayBlocks = grid.cells.map((row, rowIndex) => {
    return (
      <div key={rowIndex}>
        {row.map((col, colIndex) => {
          return <Block key={rowIndex + colIndex} />;
        })}
      </div>
    );
  });

  return (
    <div className="grid" id="gridId">            
      {displayBlocks}
    </div>   
);

I came to know from googling that I would need to use Touch Events, such as touchStart, touchMove, touchEnd. Looking at the touchevents documentation I added the following piece of code to my component. I changed the MouseEvents to ´KeyBoardevent´. Since it's a arrow key click/keydown event. But this is not working. Not sure where am I doing wrong.

   const onTouch = (evt) => {
    evt.preventDefault();
    if (evt.touches.length > 1 || (evt.type === "touchend" && evt.touches.length > 0))
      return;
  
    var newEvt = document.createEvent("KeyboardEvent");
    var type = null;
    var touch = null;
  
    // eslint-disable-next-line default-case
    switch (evt.type) {
      case "touchstart":
        type = "keydown";
        touch = evt.changedTouches[0];
        break;
      case "touchmove":
        type = "keydown";
        touch = evt.changedTouches[0];
        break;
      case "touchend":
        type = "keydown";
        touch = evt.changedTouches[0];
        break;
    }
  
    newEvt.initEvent(type, true, true, evt.originalTarget.ownerDocument.defaultView, 0,
      touch.screenX, touch.screenY, touch.clientX, touch.clientY,
      evt.keyCode('37'), evt.keyCode('39'), evt.keyCode('38'), evt.keyCode('40'), 0, null);
    evt.originalTarget.dispatchEvent(newEvt);
  }
  
document.addEventListener("touchstart", onTouch, true);
document.addEventListener("touchmove", onTouch, true);
document.addEventListener("touchend", onTouch, true);

I get the following error when I swipe right and expect for right arrow click:

TypeError: Cannot read property 'ownerDocument' of undefined

On the following line of code:

newEvt.initEvent(type, true, true, evt.originalTarget.ownerDocument.defaultView, 0,
      touch.screenX, touch.screenY, touch.clientX, touch.clientY,
      evt.keyCode('37'), evt.keyCode('39'), evt.keyCode('38'), evt.keyCode('40'), 0, null);

Version 2 Edit: : used react-swipeable after @sschwei1 suggested

I have added the following piece in the component :

  const swipeHandlers = useSwipeable({
    onSwipedLeft: useArrowKeyEvent('keydown',handleKeyDown),<<<<<problem 
    onSwipedRight: eventData => console.log("swiped right"),
    onSwipedUp: eventData => console.log("swiped up"),
    onSwipedDown: eventData => console.log("swiped down")
  });

and the return statement:

  <div className="grid" {...swipeHandlers}>
    {displayBlocks}   
  </div>

Problem: Can't use the hook as callback function.


Solution

  • React-swipeable is a library which handles swipes for you, it enables you to create handlers for different swipe directions, e.g onSwipedLeft or onSwipedUp and pretty much all other cases you can think of like onTap, onSwiping, onSwiped, and many more.

    In these handlers you can just re-use the logic of your arrow keys.


    The first solution I would think of (not the prettiest solution, but easy to use and understand) to create a wrapper function for swipes and call the according keyHandler function to it

    Here is an example of how these functions could look like:

    const handleTouch = (key) => {
      handleKeyDown({keyCode:key});
    }
    

    And in your touch handlers you can call this function with the according key

    const swipeHandlers = useSwipeable({
      onSwipedLeft: () => handleTouch('37'),
      onSwipedUp: () => handleTouch('38'),
      onSwipedRight: () => handleTouch('39'),
      onSwipedDown: () => handleTouch('40')
    });
    

    Since you are only using the keyCode in your handleKeyDown function, you can just pass an object with the keyCode property to the function and 'simulate' the key press