forked from akai/readest
182 lines
4.8 KiB
TypeScript
182 lines
4.8 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
interface UseLongPressOptions {
|
|
onTap?: () => void;
|
|
onLongPress?: () => void;
|
|
onContextMenu?: () => void;
|
|
onCancel?: () => void;
|
|
threshold?: number;
|
|
moveThreshold?: number;
|
|
}
|
|
|
|
interface UseLongPressResult {
|
|
pressing: boolean;
|
|
handlers: {
|
|
onPointerDown: (e: React.PointerEvent) => void;
|
|
onPointerUp: (e: React.PointerEvent) => void;
|
|
onPointerMove: (e: React.PointerEvent) => void;
|
|
onPointerCancel: (e: React.PointerEvent) => void;
|
|
onPointerLeave: (e: React.PointerEvent) => void;
|
|
onClick: (e: React.MouseEvent) => void;
|
|
onContextMenu: (e: React.MouseEvent) => void;
|
|
};
|
|
}
|
|
|
|
export const useLongPress = (
|
|
{
|
|
onTap,
|
|
onLongPress,
|
|
onContextMenu,
|
|
onCancel,
|
|
threshold = 500,
|
|
moveThreshold = 10,
|
|
}: UseLongPressOptions,
|
|
deps: React.DependencyList,
|
|
): UseLongPressResult => {
|
|
const [pressing, setPressing] = useState(false);
|
|
const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
const pressDelayRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
const startPosRef = useRef<{ x: number; y: number } | null>(null);
|
|
const pointerId = useRef<number | null>(null);
|
|
const hasPointerEventsRef = useRef(false);
|
|
const pointerEventTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
|
const isLongPressTriggered = useRef(false);
|
|
|
|
const reset = useCallback(() => {
|
|
setPressing(false);
|
|
isLongPressTriggered.current = false;
|
|
startPosRef.current = null;
|
|
pointerId.current = null;
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
}
|
|
if (pressDelayRef.current) {
|
|
clearTimeout(pressDelayRef.current);
|
|
pressDelayRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
const handlePointerDown = useCallback(
|
|
(e: React.PointerEvent) => {
|
|
if (e.pointerType === 'mouse' && e.button !== 0) {
|
|
return;
|
|
}
|
|
|
|
hasPointerEventsRef.current = true;
|
|
if (pointerEventTimeoutRef.current) {
|
|
clearTimeout(pointerEventTimeoutRef.current);
|
|
}
|
|
|
|
pointerId.current = e.pointerId;
|
|
startPosRef.current = { x: e.clientX, y: e.clientY };
|
|
isLongPressTriggered.current = false;
|
|
|
|
pressDelayRef.current = setTimeout(() => {
|
|
setPressing(true);
|
|
}, 100);
|
|
|
|
timerRef.current = setTimeout(() => {
|
|
if (startPosRef.current) {
|
|
isLongPressTriggered.current = true;
|
|
onLongPress?.();
|
|
setPressing(false);
|
|
}
|
|
}, threshold);
|
|
},
|
|
[onLongPress, threshold],
|
|
);
|
|
|
|
const handlePointerMove = useCallback(
|
|
(e: React.PointerEvent) => {
|
|
if (e.pointerId !== pointerId.current || !startPosRef.current) return;
|
|
|
|
const deltaX = Math.abs(e.clientX - startPosRef.current.x);
|
|
const deltaY = Math.abs(e.clientY - startPosRef.current.y);
|
|
|
|
if (deltaX > moveThreshold || deltaY > moveThreshold) {
|
|
onCancel?.();
|
|
reset();
|
|
}
|
|
},
|
|
[moveThreshold, onCancel, reset],
|
|
);
|
|
|
|
const handlePointerUp = useCallback(
|
|
(e: React.PointerEvent) => {
|
|
if (e.pointerId !== pointerId.current) return;
|
|
|
|
if (!isLongPressTriggered.current && startPosRef.current) {
|
|
const deltaX = Math.abs(e.clientX - startPosRef.current.x);
|
|
const deltaY = Math.abs(e.clientY - startPosRef.current.y);
|
|
|
|
if (deltaX <= moveThreshold && deltaY <= moveThreshold) {
|
|
onTap?.();
|
|
}
|
|
}
|
|
|
|
reset();
|
|
|
|
pointerEventTimeoutRef.current = setTimeout(() => {
|
|
hasPointerEventsRef.current = false;
|
|
}, 200);
|
|
},
|
|
[onTap, moveThreshold, reset],
|
|
);
|
|
|
|
const handleCancel = useCallback(
|
|
(e: React.PointerEvent) => {
|
|
if (e.pointerId !== pointerId.current) return;
|
|
onCancel?.();
|
|
reset();
|
|
|
|
pointerEventTimeoutRef.current = setTimeout(() => {
|
|
hasPointerEventsRef.current = false;
|
|
}, 200);
|
|
},
|
|
[onCancel, reset],
|
|
);
|
|
|
|
const handleClick = useCallback(() => {
|
|
// This is only for aria activation, if the user has used pointer events, we ignore the click event
|
|
if (!hasPointerEventsRef.current) {
|
|
onTap?.();
|
|
}
|
|
}, [onTap]);
|
|
|
|
const handleContextMenu = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
if (onContextMenu) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setTimeout(() => {
|
|
onContextMenu();
|
|
}, 100);
|
|
}
|
|
reset();
|
|
},
|
|
[onContextMenu, reset],
|
|
);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
}
|
|
};
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, deps);
|
|
|
|
return {
|
|
pressing,
|
|
handlers: {
|
|
onPointerDown: handlePointerDown,
|
|
onPointerUp: handlePointerUp,
|
|
onPointerMove: handlePointerMove,
|
|
onPointerCancel: handleCancel,
|
|
onPointerLeave: handleCancel,
|
|
onClick: handleClick,
|
|
onContextMenu: handleContextMenu,
|
|
},
|
|
};
|
|
};
|