refactor double click handler to add quicker single click

This commit is contained in:
jeffvli
2025-11-15 14:07:11 -08:00
parent 81d3d2e620
commit 60cc564743
+74 -17
View File
@@ -1,45 +1,102 @@
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
export const useDoubleClick = ({ export const useDoubleClick = ({
latency = 180, doubleClickLatency = 300,
onDoubleClick = () => null, onDoubleClick = () => null,
onSingleClick = () => null, onSingleClick = () => null,
singleClickLatency = 20,
}: { }: {
latency?: number; doubleClickLatency?: number;
onDoubleClick?: (e: any) => void; onDoubleClick?: (e: any) => void;
onSingleClick?: (e: any) => void; onSingleClick?: (e: any) => void;
singleClickLatency?: number;
}) => { }) => {
const clickCountRef = useRef(0); const clickCountRef = useRef(0);
const timeoutRef = useRef<NodeJS.Timeout | null>(null); const singleClickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const doubleClickTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const singleClickFiredRef = useRef(false);
const lastClickEventRef = useRef<any>(null);
// Use latency for backward compatibility, but prefer doubleClickLatency
const effectiveDoubleClickLatency = doubleClickLatency;
const effectiveSingleClickLatency = singleClickLatency ?? 50;
const clearSingleClick = useCallback(() => {
if (singleClickTimeoutRef.current) {
clearTimeout(singleClickTimeoutRef.current);
singleClickTimeoutRef.current = null;
}
}, []);
const handleClick = useCallback( const handleClick = useCallback(
(e: any) => { (e: any) => {
clickCountRef.current += 1; clickCountRef.current += 1;
lastClickEventRef.current = e;
// Clear any existing timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Set a new timeout to determine if it's a single or double click
timeoutRef.current = setTimeout(() => {
if (clickCountRef.current === 1) { if (clickCountRef.current === 1) {
onSingleClick(e); // First click: fire single click optimistically after short delay
singleClickFiredRef.current = false;
// Set double-click detection window first
doubleClickTimeoutRef.current = setTimeout(() => {
clickCountRef.current = 0;
singleClickFiredRef.current = false;
}, effectiveDoubleClickLatency);
// Fire single click after delay (defaults to 0 for immediate response)
if (effectiveSingleClickLatency > 0) {
singleClickTimeoutRef.current = setTimeout(() => {
// Only fire if still a single click and double click hasn't been detected
if (clickCountRef.current === 1 && !singleClickFiredRef.current) {
singleClickFiredRef.current = true;
onSingleClick(lastClickEventRef.current);
}
}, effectiveSingleClickLatency);
} else {
// Fire immediately if latency is 0
// Note: If double click comes immediately after, both may fire
// For best UX, use a small delay (e.g., 50ms) instead of 0
singleClickFiredRef.current = true;
onSingleClick(lastClickEventRef.current);
}
} else if (clickCountRef.current === 2) { } else if (clickCountRef.current === 2) {
onDoubleClick(e); // Second click detected within double-click latency
// Cancel single click if it hasn't fired yet
if (!singleClickFiredRef.current) {
clearSingleClick();
} }
// Fire double click
onDoubleClick(e);
// Reset state
clickCountRef.current = 0; clickCountRef.current = 0;
}, latency); singleClickFiredRef.current = false;
// Clear double-click timeout
if (doubleClickTimeoutRef.current) {
clearTimeout(doubleClickTimeoutRef.current);
doubleClickTimeoutRef.current = null;
}
}
}, },
[latency, onDoubleClick, onSingleClick], [
effectiveDoubleClickLatency,
effectiveSingleClickLatency,
onDoubleClick,
onSingleClick,
clearSingleClick,
],
); );
// Cleanup timeout on unmount // Cleanup timeouts on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
if (timeoutRef.current) { if (singleClickTimeoutRef.current) {
clearTimeout(timeoutRef.current); clearTimeout(singleClickTimeoutRef.current);
}
if (doubleClickTimeoutRef.current) {
clearTimeout(doubleClickTimeoutRef.current);
} }
}; };
}, []); }, []);