Estou escrevendo um gancho que me permitirá detectar ações de deslizar em um elemento. O problema é que recebo um arrastar vertical irritante ao tentar deslizar horizontalmente. Consegui atingir o comportamento desejado bloqueando a rolagem vertical do elemento pai após detectar que o deslizar horizontal se moveu 5px para a esquerda ou direita.
Mas recebo isso no console ao tentar rolar o elemento:
[Intervention] Ignored attempt to cancel a touchmove event with cancelable=false, for example because scrolling is in progress and cannot be interrupted.
Tentei verificar a veracidade, e.cancelable
mas o gancho parou de funcionar. Como posso corrigir isso?
Aqui está o gancho:
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 };
};
Eu uso funções retornadas nas propriedades do elemento filho com o mesmo nome e adiciono um ouvinte de eventos ao pai.
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>
Resumo
{ passive: false }
— isso é tarde demais.e.preventDefault()
no início do ciclo de vida do evento durantetouchmove
.Math.abs(deltaX) > Math.abs(deltaY)
para detectar direção.Do MDN em addEventListener
Você está tentando cancelar um
touchmove
evento depois que ele já começou, e o navegador não permite isso se o evento estiver marcado como passivo. Isso acontece porque os navegadores modernos tratamtouchmove
os ouvintes como passivos por padrão, o que significa quepreventDefault()
não tem efeito a menos que você os torne explicitamente não passivos durante a vinculação inicial do evento. Portanto, tentar adicioná-lo dinamicamente após o início do deslizamento é tarde demais.Se você estiver usando ouvintes de eventos passivos em outro lugar, certifique-se de que eles não estejam anulando seu esforço. Por exemplo, se o contêiner pai tiver um
touchmove
ouvinte marcado como passivo, isso poderá interferir.Tente algo assim:
Acho que consegui fazer funcionar alterando
cancelTouch
a função: