2026-03-08 18:55:45 +01:00
|
|
|
import { useRef, useCallback } from 'react';
|
|
|
|
|
|
|
|
|
|
interface UseSwipeProps {
|
2026-03-12 11:30:45 +00:00
|
|
|
onSwipeLeft: () => void;
|
|
|
|
|
onSwipeRight: () => void;
|
|
|
|
|
threshold?: number;
|
2026-10-02 18:14:23 +02:00
|
|
|
/**
|
|
|
|
|
* False while a swipe must not navigate — the lightbox while zoomed, where
|
|
|
|
|
* one finger pans the photo (#467). Read when the touch starts and when it
|
|
|
|
|
* ends, so a gesture that began zoomed never turns into a swipe.
|
|
|
|
|
*/
|
|
|
|
|
enabled?: boolean;
|
2026-03-08 18:55:45 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-02 18:14:23 +02:00
|
|
|
/**
|
|
|
|
|
* A one-finger horizontal swipe.
|
|
|
|
|
*
|
|
|
|
|
* Anything with a second finger is not a swipe: a pinch that drifts sideways
|
|
|
|
|
* used to be read as one and jump to the next photo, because only the first
|
|
|
|
|
* touch was ever looked at. Once a second finger lands, the whole touch
|
|
|
|
|
* sequence is ignored, up to the moment the last finger lifts.
|
|
|
|
|
*/
|
|
|
|
|
export function useSwipe({
|
|
|
|
|
onSwipeLeft,
|
|
|
|
|
onSwipeRight,
|
|
|
|
|
threshold = 60,
|
|
|
|
|
enabled = true,
|
|
|
|
|
}: UseSwipeProps) {
|
2026-03-12 11:30:45 +00:00
|
|
|
const touchStartX = useRef<number>(0);
|
|
|
|
|
const touchStartY = useRef<number>(0);
|
2026-10-02 18:14:23 +02:00
|
|
|
/** The current touch sequence is out: multi-touch, or started while disabled. */
|
|
|
|
|
const ignored = useRef(false);
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-10-02 18:14:23 +02:00
|
|
|
const handleTouchStart = useCallback(
|
|
|
|
|
(e: React.TouchEvent | TouchEvent) => {
|
|
|
|
|
if (e.touches.length > 1) {
|
|
|
|
|
ignored.current = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
ignored.current = !enabled;
|
|
|
|
|
touchStartX.current = e.touches[0].clientX;
|
|
|
|
|
touchStartY.current = e.touches[0].clientY;
|
|
|
|
|
},
|
|
|
|
|
[enabled],
|
|
|
|
|
);
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
const handleTouchEnd = useCallback(
|
|
|
|
|
(e: React.TouchEvent | TouchEvent) => {
|
2026-10-02 18:14:23 +02:00
|
|
|
// Fingers still down: the sequence is not over yet.
|
|
|
|
|
if (e.touches.length > 0) return;
|
|
|
|
|
const skip = ignored.current || !enabled;
|
|
|
|
|
ignored.current = false;
|
|
|
|
|
if (skip) return;
|
|
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
const dx = e.changedTouches[0].clientX - touchStartX.current;
|
|
|
|
|
const dy = e.changedTouches[0].clientY - touchStartY.current;
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
if (Math.abs(dx) > threshold && Math.abs(dx) > Math.abs(dy)) {
|
|
|
|
|
if (dx > 0) onSwipeRight();
|
|
|
|
|
else onSwipeLeft();
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-10-02 18:14:23 +02:00
|
|
|
[onSwipeLeft, onSwipeRight, threshold, enabled],
|
2026-03-12 11:30:45 +00:00
|
|
|
);
|
2026-03-08 18:55:45 +01:00
|
|
|
|
2026-03-12 11:30:45 +00:00
|
|
|
return { handleTouchStart, handleTouchEnd };
|
2026-03-08 18:55:45 +01:00
|
|
|
}
|