UNPKG

@wix/design-system

Version:

@wix/design-system

109 lines 3.96 kB
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; // Pointer-aware hover that ignores touch — without this, Stylable's JS-toggled // `:hovered` sticks on tap (mobile fires `mouseenter` but no `mouseleave`). // Mouse listeners are kept for unidriver test fakes; duplicate pointer/mouse // pairs from real browsers are deduped via WeakMap. const POINTER_TO_MOUSE_DEDUPE_MS = 50; const recentPointerEvents = new WeakMap(); const isHoverCapablePointer = (pointerType) => pointerType === 'mouse' || pointerType === 'pen'; const markPointerHandled = (target) => { if (target) { recentPointerEvents.set(target, Date.now()); } }; const recentlyHandledByPointer = (target) => { if (!target) return false; const at = recentPointerEvents.get(target); return at !== undefined && Date.now() - at < POINTER_TO_MOUSE_DEDUPE_MS; }; export const useHover = ({ isDisabled, onHoverStart, onHoverEnd, } = {}) => { const [hovered, setHovered] = useState(false); const isHoveredRef = useRef(false); const onHoverStartRef = useRef(onHoverStart); const onHoverEndRef = useRef(onHoverEnd); useEffect(() => { onHoverStartRef.current = onHoverStart; onHoverEndRef.current = onHoverEnd; }); const triggerHoverStart = useCallback((event) => { if (isHoveredRef.current) return; isHoveredRef.current = true; setHovered(true); onHoverStartRef.current?.(event); }, []); const triggerHoverEnd = useCallback((event) => { if (!isHoveredRef.current) return; isHoveredRef.current = false; setHovered(false); onHoverEndRef.current?.(event); }, []); useEffect(() => { if (isDisabled && isHoveredRef.current) { isHoveredRef.current = false; setHovered(false); } }, [isDisabled]); const hoverProps = useMemo(() => ({ onPointerEnter: (event) => { markPointerHandled(event.currentTarget); if (isDisabled || !isHoverCapablePointer(event.pointerType)) { return; } triggerHoverStart(event); }, onPointerLeave: (event) => { markPointerHandled(event.currentTarget); if (!isHoverCapablePointer(event.pointerType)) { return; } triggerHoverEnd(event); }, onMouseEnter: (event) => { if (recentlyHandledByPointer(event.currentTarget)) return; if (isDisabled) return; triggerHoverStart(event); }, onMouseLeave: (event) => { if (recentlyHandledByPointer(event.currentTarget)) return; triggerHoverEnd(event); }, }), [isDisabled, triggerHoverStart, triggerHoverEnd]); return { hovered, hoverProps }; }; // Stateless variant for components that already manage hover state externally // (e.g. per-item indices in DropdownLayout, RadarChart, FacesRatingBar). export const createHoverHandlers = ({ isDisabled, onHoverStart, onHoverEnd, } = {}) => ({ onPointerEnter: (event) => { markPointerHandled(event.currentTarget); if (isDisabled || !isHoverCapablePointer(event.pointerType)) { return; } onHoverStart?.(event); }, onPointerLeave: (event) => { markPointerHandled(event.currentTarget); if (!isHoverCapablePointer(event.pointerType)) { return; } onHoverEnd?.(event); }, onMouseEnter: (event) => { if (recentlyHandledByPointer(event.currentTarget)) return; if (isDisabled) return; onHoverStart?.(event); }, onMouseLeave: (event) => { if (recentlyHandledByPointer(event.currentTarget)) return; onHoverEnd?.(event); }, }); //# sourceMappingURL=useHover.js.map