Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cancel onClick events when swiping?

I have a list of items that the user can select from. If the user clicks on an item the item is selected. If the user long presses on any item it activates a multi-select mode, where the user can select multiple items, I'm using a custom useLongPress hook from this question. However, I would like to let the user swipe to scroll through the list without selecting any item, I'm using react-swipeable for swipe events, but I don't know how I can cancel the onClick events if the user is swiping, I'm still selecting items even if I'm swiping.

https://codesandbox.io/s/hungry-kilby-vlxmc?file=/src/App.js

App.js

import "./styles.css";
import React, { useState } from "react";
import useLongPress from "./useLongPress";
import { useSwipeable } from "react-swipeable";

const list = [
  "apple",
  "banana",
  "orange",
  "mango",
  "kiwi",
  "lemon",
  "watermelon",
  "avocado",
  "grapes",
  "strawberry",
  "passion fruit",
  "papaya",
  "pomegranate",
  "blueberry",
  "pear",
  "pineapple",
  "jackfruit"
];

export default function App() {
  const [MultiSelect, setMultiSelect] = useState(false);
  const [MultiSelectArray, setMultiSelectArray] = useState([]);
  const [IsSwiping, setIsSwiping] = useState(false);
  const swipe = useSwipeable({
    onSwipeStart: (e) => {
      setIsSwiping(true);
      console.log("swiping", e);
    },
    onSwiped: (e) => {
      setTimeout(setIsSwiping(false), 5000);
      console.log("swiped", e);
    }
  });

  /* onClick and LongPress setup */

  const onLongPress = (e) => {
    setMultiSelect(true);
    setMultiSelectArray((oldArray) => [...oldArray, e.target.innerHTML]);
  };

  const onClick = (e) => {
    if (!IsSwiping) {
      if (MultiSelect) {
        if (MultiSelectArray.includes(e.target.innerHTML)) {
          //if element is already selected, deselects it
          const newArray = MultiSelectArray.filter(
            (element) => element !== e.target.innerHTML
          );
          setMultiSelectArray(newArray);
        } else {
          setMultiSelectArray((oldArray) => [...oldArray, e.target.innerHTML]);
        }
      } else {
        console.log("selected", e.target.innerHTML);
      }
    }
  };

  const defaultOptions = {
    shouldPreventDefault: true,
    delay: 500
  };

  const longPressEvent = useLongPress(onLongPress, onClick, defaultOptions);

  /* onClick and LongPress setup */

  return (
    <>
      <ul className="add-list" {...swipe}>
        {list.map((fruit, index) => {
          return (
            <li
              {...longPressEvent}
              className={
                MultiSelectArray.includes(fruit) ? "selected-exercise" : ""
              }
            >
              {fruit}
            </li>
          );
        })}
      </ul>

      {MultiSelect && (
        <div className="multiselect-buttons">
          <span
            className={"cancel "}
            onClick={(e) => {
              setMultiSelectArray([]);
              setMultiSelect(false);
            }}
          >
            X
          </span>
          <span
            className={"check"}
            onClick={(e) => {
              console.log("multi-select", MultiSelectArray);
            }}
          >
            OK
          </span>
        </div>
      )}
    </>
  );
}

useLongPress.js

import { useCallback, useRef, useState } from "react";

const useLongPress = (
  onLongPress,
  onClick,
  { shouldPreventDefault = true, delay = 300 } = {}
) => {
  const [longPressTriggered, setLongPressTriggered] = useState(false);
  const timeout = useRef();
  const target = useRef();

  const start = useCallback(
    (event) => {
      if (shouldPreventDefault && event.target) {
        event.target.addEventListener("touchend", preventDefault, {
          passive: false
        });
        target.current = event.target;
      }
      timeout.current = setTimeout(() => {
        onLongPress(event);
        setLongPressTriggered(true);
      }, delay);
    },
    [onLongPress, delay, shouldPreventDefault]
  );

  const clear = useCallback(
    (event, shouldTriggerClick = true) => {
      timeout.current && clearTimeout(timeout.current);
      shouldTriggerClick && !longPressTriggered && onClick(event);
      setLongPressTriggered(false);
      if (shouldPreventDefault && target.current) {
        target.current.removeEventListener("touchend", preventDefault);
      }
    },
    [shouldPreventDefault, onClick, longPressTriggered]
  );

  return {
    onMouseDown: (e) => start(e),
    onTouchStart: (e) => start(e),
    onMouseUp: (e) => clear(e),
    onMouseLeave: (e) => clear(e, false),
    onTouchEnd: (e) => clear(e)
  };
};

const isTouchEvent = (event) => {
  return "touches" in event;
};

const preventDefault = (event) => {
  if (!isTouchEvent(event)) return;

  if (event.touches.length < 2 && event.cancelable) {
    event.preventDefault();
  }
};

export default useLongPress;

like image 622
João Pedro Avatar asked Oct 26 '22 13:10

João Pedro


1 Answers

setState is asynchronous as well as it triggers a re-render. You don't want to do that during an interaction like swipe.

Try using a ref using useRef

const isSwiping = useRef(false);

and use it like

onst onClick = (e) => {
  if (!isSwiping.current) {

You can see that useLongPress.js is also using useRef for the same reason.

like image 68
T J Avatar answered Nov 15 '22 13:11

T J