Skip to content

Commit a7847b7

Browse files
committed
feat: add safe hover polygon handling
1 parent 220358d commit a7847b7

4 files changed

Lines changed: 538 additions & 15 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"@rc-component/motion": "^1.1.4",
4545
"@rc-component/portal": "^2.2.0",
4646
"@rc-component/resize-observer": "^1.1.1",
47-
"@rc-component/util": "^1.2.1",
47+
"@rc-component/util": "^1.11.1",
4848
"clsx": "^2.1.1"
4949
},
5050
"devDependencies": {

src/index.tsx

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import useAlign from './hooks/useAlign';
2121
import useDelay from './hooks/useDelay';
2222
import useWatch from './hooks/useWatch';
2323
import useWinClick from './hooks/useWinClick';
24+
import { isInSafeHoverArea } from './safeHover';
2425
import type { PortalProps } from '@rc-component/portal';
2526

2627
import type {
@@ -476,6 +477,80 @@ export function generateTrigger(
476477
isMobile,
477478
);
478479

480+
const safeHoverCleanupRef = React.useRef<VoidFunction>(() => {});
481+
482+
const stopSafeHoverMonitor = useEvent(() => {
483+
safeHoverCleanupRef.current();
484+
safeHoverCleanupRef.current = () => {};
485+
});
486+
487+
const startSafeHoverMonitor = useEvent(
488+
(event: React.MouseEvent | React.PointerEvent) => {
489+
stopSafeHoverMonitor();
490+
491+
const currentTargetEle = targetEle || externalForwardRef.current;
492+
const currentPopupEle = popupEle || externalPopupRef.current;
493+
494+
if (
495+
!currentTargetEle ||
496+
!currentPopupEle ||
497+
!openRef.current ||
498+
!mouseLeaveDelay
499+
) {
500+
return;
501+
}
502+
503+
const doc = currentTargetEle.ownerDocument;
504+
const originX = event.clientX;
505+
const originY = event.clientY;
506+
507+
const onMouseMove = (mouseEvent: MouseEvent) => {
508+
const nextTargetEle = targetEle || currentTargetEle;
509+
const nextPopupEle =
510+
popupEle || externalPopupRef.current || currentPopupEle;
511+
512+
if (!nextTargetEle || !nextPopupEle) {
513+
stopSafeHoverMonitor();
514+
return;
515+
}
516+
517+
const nextInSafeArea = isInSafeHoverArea({
518+
clientX: mouseEvent.clientX,
519+
clientY: mouseEvent.clientY,
520+
originX,
521+
originY,
522+
target: nextTargetEle,
523+
popup: nextPopupEle,
524+
align: alignInfo,
525+
});
526+
527+
if (nextInSafeArea) {
528+
triggerOpen(false, mouseLeaveDelay);
529+
} else {
530+
stopSafeHoverMonitor();
531+
triggerOpen(false, mouseLeaveDelay);
532+
}
533+
};
534+
535+
doc.addEventListener('mousemove', onMouseMove);
536+
safeHoverCleanupRef.current = () => {
537+
doc.removeEventListener('mousemove', onMouseMove);
538+
};
539+
},
540+
);
541+
542+
React.useEffect(() => {
543+
return () => {
544+
stopSafeHoverMonitor();
545+
};
546+
}, [stopSafeHoverMonitor]);
547+
548+
useLayoutEffect(() => {
549+
if (!mergedOpen) {
550+
stopSafeHoverMonitor();
551+
}
552+
}, [mergedOpen, stopSafeHoverMonitor]);
553+
479554
const [showActions, hideActions] = useAction(
480555
action,
481556
showAction,
@@ -688,6 +763,7 @@ export function generateTrigger(
688763
);
689764

690765
onPopupMouseEnter = (event) => {
766+
stopSafeHoverMonitor();
691767
// Only trigger re-open when popup is visible
692768
if (
693769
(mergedOpen || inMotion) &&
@@ -706,22 +782,24 @@ export function generateTrigger(
706782
}
707783

708784
if (hoverToHide) {
709-
wrapperAction(
710-
'onMouseLeave',
711-
false,
712-
mouseLeaveDelay,
713-
undefined,
714-
ignoreMouseTrigger,
715-
);
716-
wrapperAction(
717-
'onPointerLeave',
718-
false,
719-
mouseLeaveDelay,
720-
undefined,
721-
ignoreMouseTrigger,
722-
);
785+
const onTargetMouseLeave = (
786+
event: React.MouseEvent | React.PointerEvent,
787+
...args: any[]
788+
) => {
789+
if (!ignoreMouseTrigger()) {
790+
startSafeHoverMonitor(event);
791+
triggerOpen(false, mouseLeaveDelay);
792+
}
793+
originChildProps[
794+
event.type === 'pointerleave' ? 'onPointerLeave' : 'onMouseLeave'
795+
]?.(event, ...args);
796+
};
797+
798+
cloneProps.onMouseLeave = onTargetMouseLeave;
799+
cloneProps.onPointerLeave = onTargetMouseLeave;
723800

724801
onPopupMouseLeave = () => {
802+
stopSafeHoverMonitor();
725803
triggerOpen(false, mouseLeaveDelay);
726804
};
727805
}

0 commit comments

Comments
 (0)