import { useCallback, useEffect, useRef, useState } from 'react' // Constants for better maintainability const POPOVER_CLOSE_DELAY = 1500 const FORCE_CLOSE_COOLDOWN = 500 const HOVER_OPEN_DELAY = 300 export const useHoverAndTouchSidebar = ( sidebarRef: React.RefObject, ) => { const [hovered, setHovered] = useState(false) const [isUseable, setIsUseable] = useState(false) const isUseableTimeoutRef = useRef>(undefined) const mouseLeaveTimeoutRef = useRef>(undefined) const forceCloseTimeoutRef = useRef>(undefined) const hoverOpenTimeoutRef = useRef>(undefined) const isInForceCloseCooldown = useRef(false) const closeSidebar = useCallback((force = false) => { // Clear any pending timeouts when explicitly closing if (force || mouseLeaveTimeoutRef.current) { if (mouseLeaveTimeoutRef.current) { clearTimeout(mouseLeaveTimeoutRef.current) mouseLeaveTimeoutRef.current = undefined } } // Clear any pending hover open timeout if (hoverOpenTimeoutRef.current) { clearTimeout(hoverOpenTimeoutRef.current) hoverOpenTimeoutRef.current = undefined } // If this is a forced close (e.g., from user selection), add a cooldown period if (force) { isInForceCloseCooldown.current = true forceCloseTimeoutRef.current = setTimeout(() => { isInForceCloseCooldown.current = false }, FORCE_CLOSE_COOLDOWN) } setHovered(false) }, []) const handleMouseEnter = useCallback(() => { // Don't reopen immediately if we're in a force close cooldown period if (isInForceCloseCooldown.current) { return } // Clear any pending mouse leave timeout if (mouseLeaveTimeoutRef.current) { clearTimeout(mouseLeaveTimeoutRef.current) mouseLeaveTimeoutRef.current = undefined } // Clear any existing hover open timeout to prevent multiple pending delays if (hoverOpenTimeoutRef.current) { clearTimeout(hoverOpenTimeoutRef.current) hoverOpenTimeoutRef.current = undefined } // Delay opening the sidebar by 300ms hoverOpenTimeoutRef.current = setTimeout(() => { setHovered(true) setIsUseable(true) hoverOpenTimeoutRef.current = undefined }, HOVER_OPEN_DELAY) }, []) const handleMouseLeave = useCallback( (event: MouseEvent) => { // Check if we're leaving to enter a popover/dropdown const target = event.relatedTarget as Element | null // Check if any popovers are currently open const hasOpenPopover = document.querySelector( '[data-testid="popover-floating"]:not([style*="display: none"])', ) if (target) { // Check if the target is a popover or dropdown element const isPopoverElement = target.closest('[data-testid="popover-floating"]') || target.closest('[data-placement]') || target.closest('.tippy-box') || target.closest('[role="dialog"]') if (isPopoverElement || hasOpenPopover) { // Don't close immediately if there's an open popover mouseLeaveTimeoutRef.current = setTimeout(() => { // Double-check if popover is still open before closing const stillHasOpenPopover = document.querySelector( '[data-testid="popover-floating"]:not([style*="display: none"])', ) if (!stillHasOpenPopover) { closeSidebar() } }, POPOVER_CLOSE_DELAY) return } } // If there's an open popover, delay closing if (hasOpenPopover) { mouseLeaveTimeoutRef.current = setTimeout(() => { const stillHasOpenPopover = document.querySelector( '[data-testid="popover-floating"]:not([style*="display: none"])', ) if (!stillHasOpenPopover) { closeSidebar() } }, POPOVER_CLOSE_DELAY) return } // Normal mouse leave behavior closeSidebar() }, [closeSidebar], ) useEffect(() => { const handleTouchEndOutside = (event: TouchEvent) => { const sidebarElement = sidebarRef.current if (sidebarElement && !sidebarElement.contains(event.target as Node)) { closeSidebar() } } const handleTouchStart = () => { if (!hovered) { setIsUseable(false) setHovered(true) isUseableTimeoutRef.current = setTimeout(() => { setIsUseable(true) }, 200) } document.addEventListener('touchend', handleTouchEndOutside) } const sidebarElement = sidebarRef.current if (sidebarElement) { sidebarElement.addEventListener('mouseenter', handleMouseEnter) sidebarElement.addEventListener('mouseleave', handleMouseLeave) sidebarElement.addEventListener('touchstart', handleTouchStart, { passive: true, }) return () => { sidebarElement.removeEventListener('mouseenter', handleMouseEnter) sidebarElement.removeEventListener('mouseleave', handleMouseLeave) sidebarElement.removeEventListener('touchstart', handleTouchStart) } } }, [handleMouseEnter, handleMouseLeave, hovered, closeSidebar, sidebarRef]) useEffect(() => { return () => { if (isUseableTimeoutRef.current) { clearTimeout(isUseableTimeoutRef.current) } if (mouseLeaveTimeoutRef.current) { clearTimeout(mouseLeaveTimeoutRef.current) } if (forceCloseTimeoutRef.current) { clearTimeout(forceCloseTimeoutRef.current) } if (hoverOpenTimeoutRef.current) { clearTimeout(hoverOpenTimeoutRef.current) } } }, []) return [ hovered, isUseable, closeSidebar as (force?: boolean) => void, setHovered, ] as const }