@@ -15,141 +15,101 @@ type OnMouseEvents = Record<MouseEvents, (e: MouseEvent) => void>;
15
15
16
16
function ActiveHoverable ( { onHoverIn, onHoverOut, shouldHandleScroll, shouldFreezeCapture, children} : ActiveHoverableProps , outerRef : Ref < HTMLElement > ) {
17
17
const [ isHovered , setIsHovered ] = useState ( false ) ;
18
-
19
18
const elementRef = useRef < HTMLElement | null > ( null ) ;
20
19
const isScrollingRef = useRef ( false ) ;
21
20
const isHoveredRef = useRef ( false ) ;
22
- const isVisibiltyHidden = useRef ( false ) ;
21
+ const isVisibilityHidden = useRef ( false ) ;
23
22
24
23
const updateIsHovered = useCallback (
25
24
( hovered : boolean ) => {
25
+ if ( shouldFreezeCapture ) {
26
+ return ;
27
+ }
28
+
26
29
isHoveredRef . current = hovered ;
27
- // Nullish coalescing operator (`??`) wouldn't be appropriate here because
28
- // it's not a matter of providing a default when encountering `null` or `undefined`
29
- // but rather making a decision based on the truthy nature of the complete expressions.
30
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
31
- if ( ( shouldHandleScroll && isScrollingRef . current ) || shouldFreezeCapture ) {
30
+ isVisibilityHidden . current = false ;
31
+
32
+ if ( shouldHandleScroll && isScrollingRef . current ) {
32
33
return ;
33
34
}
35
+
34
36
setIsHovered ( hovered ) ;
37
+
38
+ if ( hovered ) {
39
+ onHoverIn ?.( ) ;
40
+ } else {
41
+ onHoverOut ?.( ) ;
42
+ }
35
43
} ,
36
- [ shouldHandleScroll , shouldFreezeCapture ] ,
44
+ [ shouldHandleScroll , shouldFreezeCapture , onHoverIn , onHoverOut ] ,
37
45
) ;
38
46
39
- useEffect ( ( ) => {
40
- if ( isHovered ) {
41
- onHoverIn ?.( ) ;
42
- } else {
43
- onHoverOut ?.( ) ;
44
- }
45
- } , [ isHovered , onHoverIn , onHoverOut ] ) ;
46
-
47
47
useEffect ( ( ) => {
48
48
if ( ! shouldHandleScroll ) {
49
49
return ;
50
50
}
51
51
52
52
const scrollingListener = DeviceEventEmitter . addListener ( CONST . EVENTS . SCROLLING , ( scrolling : boolean ) => {
53
53
isScrollingRef . current = scrolling ;
54
- if ( ! isScrollingRef . current ) {
55
- setIsHovered ( isHoveredRef . current ) ;
54
+ if ( scrolling && isHovered ) {
55
+ setIsHovered ( false ) ;
56
+ onHoverOut ?.( ) ;
57
+ } else if ( ! scrolling && elementRef . current ?. matches ( ':hover' ) ) {
58
+ setIsHovered ( true ) ;
59
+ onHoverIn ?.( ) ;
56
60
}
57
61
} ) ;
58
62
59
63
return ( ) => scrollingListener . remove ( ) ;
60
- } , [ shouldHandleScroll ] ) ;
64
+ } , [ shouldHandleScroll , isHovered , onHoverIn , onHoverOut ] ) ;
61
65
62
66
useEffect ( ( ) => {
63
- // Do not mount a listener if the component is not hovered
64
- if ( ! isHovered ) {
65
- return ;
66
- }
67
-
68
- /**
69
- * Checks the hover state of a component and updates it based on the event target.
70
- * This is necessary to handle cases where the hover state might get stuck due to an unreliable mouseleave trigger,
71
- * such as when an element is removed before the mouseleave event is triggered.
72
- * @param event The hover event object.
73
- */
74
- const unsetHoveredIfOutside = ( event : MouseEvent ) => {
75
- // We're also returning early if shouldFreezeCapture is true in order
76
- // to not update the hover state but keep it frozen.
77
- if ( ! elementRef . current || elementRef . current . contains ( event . target as Node ) || shouldFreezeCapture ) {
78
- return ;
67
+ const handleVisibilityChange = ( ) => {
68
+ if ( document . visibilityState === 'hidden' ) {
69
+ isVisibilityHidden . current = true ;
70
+ setIsHovered ( false ) ;
71
+ } else {
72
+ isVisibilityHidden . current = false ;
79
73
}
80
-
81
- setIsHovered ( false ) ;
82
74
} ;
83
75
84
- document . addEventListener ( 'mouseover' , unsetHoveredIfOutside , true ) ;
85
-
86
- return ( ) => document . removeEventListener ( 'mouseover' , unsetHoveredIfOutside ) ;
87
- } , [ isHovered , elementRef , shouldFreezeCapture ] ) ;
76
+ document . addEventListener ( 'visibilitychange' , handleVisibilityChange ) ;
77
+ return ( ) => document . removeEventListener ( 'visibilitychange' , handleVisibilityChange ) ;
78
+ } , [ ] ) ;
88
79
89
- useEffect ( ( ) => {
90
- const unsetHoveredWhenDocumentIsHidden = ( ) => {
91
- if ( document . visibilityState !== 'hidden' ) {
80
+ const handleMouseEvents = useCallback (
81
+ ( type : 'enter' | 'leave' | 'blur' ) => ( ) => {
82
+ if ( shouldFreezeCapture ) {
92
83
return ;
93
84
}
94
85
95
- isVisibiltyHidden . current = true ;
96
- setIsHovered ( false ) ;
97
- } ;
98
-
99
- document . addEventListener ( 'visibilitychange' , unsetHoveredWhenDocumentIsHidden ) ;
86
+ const newHoverState = type === 'enter' ;
87
+ isHoveredRef . current = newHoverState ;
88
+ isVisibilityHidden . current = false ;
100
89
101
- return ( ) => document . removeEventListener ( 'visibilitychange' , unsetHoveredWhenDocumentIsHidden ) ;
102
- } , [ ] ) ;
90
+ updateIsHovered ( newHoverState ) ;
91
+ } ,
92
+ [ shouldFreezeCapture , updateIsHovered ] ,
93
+ ) ;
103
94
104
- const child = useMemo ( ( ) => getReturnValue ( children , ! isScrollingRef . current && isHovered ) , [ children , isHovered ] ) ;
95
+ const child = useMemo ( ( ) => getReturnValue ( children , isHovered ) , [ children , isHovered ] ) ;
105
96
106
- const { onMouseEnter, onMouseLeave, onMouseMove , onBlur} = child . props as OnMouseEvents ;
97
+ const { onMouseEnter, onMouseLeave, onBlur} = child . props as OnMouseEvents ;
107
98
108
- const hoverAndForwardOnMouseEnter = useCallback (
109
- ( e : MouseEvent ) => {
110
- isVisibiltyHidden . current = false ;
111
- updateIsHovered ( true ) ;
99
+ return cloneElement ( child , {
100
+ ref : mergeRefs ( elementRef , outerRef , child . ref ) ,
101
+ onMouseEnter : ( e : MouseEvent ) => {
102
+ handleMouseEvents ( 'enter' ) ( ) ;
112
103
onMouseEnter ?.( e ) ;
113
104
} ,
114
- [ updateIsHovered , onMouseEnter ] ,
115
- ) ;
116
-
117
- const unhoverAndForwardOnMouseLeave = useCallback (
118
- ( e : MouseEvent ) => {
119
- updateIsHovered ( false ) ;
105
+ onMouseLeave : ( e : MouseEvent ) => {
106
+ handleMouseEvents ( 'leave' ) ( ) ;
120
107
onMouseLeave ?.( e ) ;
121
108
} ,
122
- [ updateIsHovered , onMouseLeave ] ,
123
- ) ;
124
-
125
- const unhoverAndForwardOnBlur = useCallback (
126
- ( event : MouseEvent ) => {
127
- // Check if the blur event occurred due to clicking outside the element
128
- // and the wrapperView contains the element that caused the blur and reset isHovered
129
- if ( ! elementRef . current ?. contains ( event . target as Node ) && ! elementRef . current ?. contains ( event . relatedTarget as Node ) && ! shouldFreezeCapture ) {
130
- setIsHovered ( false ) ;
131
- }
132
-
133
- onBlur ?.( event ) ;
134
- } ,
135
- [ onBlur , shouldFreezeCapture ] ,
136
- ) ;
137
-
138
- const handleAndForwardOnMouseMove = useCallback (
139
- ( e : MouseEvent ) => {
140
- isVisibiltyHidden . current = false ;
141
- updateIsHovered ( true ) ;
142
- onMouseMove ?.( e ) ;
109
+ onBlur : ( e : MouseEvent ) => {
110
+ handleMouseEvents ( 'blur' ) ( ) ;
111
+ onBlur ?.( e ) ;
143
112
} ,
144
- [ updateIsHovered , onMouseMove ] ,
145
- ) ;
146
-
147
- return cloneElement ( child , {
148
- ref : mergeRefs ( elementRef , outerRef , child . ref ) ,
149
- onMouseEnter : hoverAndForwardOnMouseEnter ,
150
- onMouseLeave : unhoverAndForwardOnMouseLeave ,
151
- onBlur : unhoverAndForwardOnBlur ,
152
- ...( isVisibiltyHidden . current ? { onMouseMove : handleAndForwardOnMouseMove } : { } ) ,
153
113
} ) ;
154
114
}
155
115
0 commit comments