@@ -21,6 +21,7 @@ import useAlign from './hooks/useAlign';
2121import useDelay from './hooks/useDelay' ;
2222import useWatch from './hooks/useWatch' ;
2323import useWinClick from './hooks/useWinClick' ;
24+ import { isInSafeHoverArea } from './safeHover' ;
2425import type { PortalProps } from '@rc-component/portal' ;
2526
2627import 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