@wix/design-system
Version:
@wix/design-system
109 lines • 3.96 kB
JavaScript
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