I am writing a hook that will allow me to listen for swipe actions on an element. Problem is that I get an annoying vertical pull drag when trying to swipe horizontally. I've managed to achieve the desired behavior by locking vertical scroll of the parent after detecting that horizontal swipe moved 5px left or right.
But I get this in the console when trying to scroll the element:
[Intervention] Ignored attempt to cancel a touchmove event with cancelable=false, for example because scrolling is in progress and cannot be interrupted.
I tried checking for truthy e.cancelable
but then the hook stops working.
How can I fix this?
Here's the hook:
import { useRef } from "react";
export const useSwipe = () => {
const touchStartX = useRef<number | null>(null);
const touchEndX = useRef<number | null>(null);
const touchStartY = useRef<number | null>(null);
const touchEndY = useRef<number | null>(null);
const minSwipeDistance = 100;
const box = document.getElementById("scrollable-cards-box");
const cancelTouch = (e: TouchEvent) => e.preventDefault();
const onTouchStart = (e: React.TouchEvent) => {
touchEndX.current = null;
touchEndY.current = null;
touchStartX.current = e.targetTouches[0].clientX;
touchStartY.current = e.targetTouches[0].clientY;
};
const onTouchMove = (e: React.TouchEvent) => {
touchEndX.current = e.targetTouches[0].clientX;
touchEndY.current = e.targetTouches[0].clientY;
if (!touchStartY.current || !touchEndY.current) return;
if (!touchStartX.current || !touchEndX.current) return;
//something here causes the error
if (
touchEndX.current > touchStartX.current + 5 ||
touchEndX.current < touchStartX.current - 5
) {
if (box) {
box.addEventListener("touchmove", cancelTouch, {
passive: false,
});
box.style.touchAction = "none";
console.log("set touch to none");
}
}
};
const onTouchEnd = () => {
if (!touchStartX.current || !touchEndX.current) return;
const distanceX = touchStartX.current - touchEndX.current;
const isLeftSwipe = distanceX > minSwipeDistance;
const isRightSwipe = distanceX < -minSwipeDistance;
if (box) {
box.style.touchAction = "auto";
console.log("set touch to autoo");
box.removeEventListener("touchmove", cancelTouch);
}
if (isRightSwipe) return "right";
if (isLeftSwipe) return "left";
};
return { onTouchStart, onTouchMove, onTouchEnd };
};
I use returned functions on the child element props of the same name and add an event listener to the parent.
const {
useRef
} = React;
const useSwipe = () => {
const touchStartX = useRef(null);
const touchEndX = useRef(null);
const touchStartY = useRef(null);
const touchEndY = useRef(null);
const minSwipeDistance = 100;
const lockAfterX = 10;
const box = document.getElementById("scrollable-cards-box")
const cancelTouch = (e: TouchEvent) => e.preventDefault();
const onTouchStart = (e: React.TouchEvent) => {
touchEndX.current = null;
touchEndY.current = null;
touchStartX.current = e.targetTouches[0].clientX;
touchStartY.current = e.targetTouches[0].clientY;
};
//something happens here
const onTouchMove = (e: React.TouchEvent) => {
touchEndX.current = e.targetTouches[0].clientX;
touchEndY.current = e.targetTouches[0].clientY;
if (!touchStartY.current || !touchEndY.current) return;
if (!touchStartX.current || !touchEndX.current) return;
if (
touchEndX.current > touchStartX.current + lockAfterX ||
touchEndX.current < touchStartX.current - lockAfterX
) {
if (box) {
box.style.touchAction = "none";
box.addEventListener("touchmove", cancelTouch, {
passive: false,
});
console.log("set touch to none");
}
}
};
const onTouchEnd = () => {
if (!touchStartX.current || !touchEndX.current) return;
const distanceX = touchStartX.current - touchEndX.current;
const isLeftSwipe = distanceX > minSwipeDistance;
const isRightSwipe = distanceX < -minSwipeDistance;
if (box) {
box.style.touchAction = "auto";
console.log("set touch to autoo");
box.removeEventListener("touchmove", cancelTouch);
}
if (isRightSwipe) return "right";
if (isLeftSwipe) return "left";
};
return { onTouchStart, onTouchMove, onTouchEnd };
};
const List = () => {
const { onTouchStart, onTouchMove, onTouchEnd } = useSwipe();
const handleTouchEnd = () => {
const swipeDirection = onTouchEnd();
if (swipeDirection === "left") console.log("left swipe");
if (swipeDirection === "right") console.log("right swipe");
};
const uls = Array(30).fill("");
return (
uls.map((ul, index) => (
<ul
className="item"
key={index}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={handleTouchEnd}
>
{index + ul}
</ul>
))
);
};
ReactDOM.createRoot(
document.getElementById("scrollable-cards-box")
).render( <
List / >
);
.container {
background-color: gray;
}
.item {
background-color: skyblue;
padding: 15px 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js"></script>
<div id="root">
<div className="container" id="scrollable-cards-box">
</div>
</div>
I think I got it to work by changing cancelTouch
function:
const cancelTouch = (e: TouchEvent) => e.cancelable && e.preventDefault();