diff --git a/src/shared/hooks/use-double-click.ts b/src/shared/hooks/use-double-click.ts index e4eb9f3d4..35808d181 100644 --- a/src/shared/hooks/use-double-click.ts +++ b/src/shared/hooks/use-double-click.ts @@ -1,45 +1,102 @@ import { useCallback, useEffect, useRef } from 'react'; export const useDoubleClick = ({ - latency = 180, + doubleClickLatency = 300, onDoubleClick = () => null, onSingleClick = () => null, + singleClickLatency = 20, }: { - latency?: number; + doubleClickLatency?: number; onDoubleClick?: (e: any) => void; onSingleClick?: (e: any) => void; + singleClickLatency?: number; }) => { const clickCountRef = useRef(0); - const timeoutRef = useRef(null); + const singleClickTimeoutRef = useRef(null); + const doubleClickTimeoutRef = useRef(null); + const singleClickFiredRef = useRef(false); + const lastClickEventRef = useRef(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( (e: any) => { clickCountRef.current += 1; + lastClickEventRef.current = e; - // Clear any existing timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } + if (clickCountRef.current === 1) { + // First click: fire single click optimistically after short delay + singleClickFiredRef.current = false; - // Set a new timeout to determine if it's a single or double click - timeoutRef.current = setTimeout(() => { - if (clickCountRef.current === 1) { - onSingleClick(e); - } else if (clickCountRef.current === 2) { - onDoubleClick(e); + // 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) { + // 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; - }, 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(() => { return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); + if (singleClickTimeoutRef.current) { + clearTimeout(singleClickTimeoutRef.current); + } + if (doubleClickTimeoutRef.current) { + clearTimeout(doubleClickTimeoutRef.current); } }; }, []);