@@ -20,9 +20,12 @@ type HoverProps = {
20
20
} ;
21
21
22
22
type HoverState = {
23
+ isActiveHovered : boolean ,
23
24
isHovered : boolean ,
24
25
isInHitSlop : boolean ,
25
26
isTouched : boolean ,
27
+ hoverStartTimeout : null | TimeoutID ,
28
+ hoverEndTimeout : null | TimeoutID ,
26
29
} ;
27
30
28
31
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' ;
@@ -60,65 +63,131 @@ function createHoverEvent(
60
63
} ;
61
64
}
62
65
66
+ function dispatchHoverChangeEvent (
67
+ event : ResponderEvent ,
68
+ context : ResponderContext ,
69
+ props : HoverProps ,
70
+ state : HoverState ,
71
+ ) : void {
72
+ const listener = ( ) => {
73
+ props . onHoverChange ( state . isActiveHovered ) ;
74
+ } ;
75
+ const syntheticEvent = createHoverEvent (
76
+ 'hoverchange' ,
77
+ event . target ,
78
+ listener ,
79
+ ) ;
80
+ context . dispatchEvent ( syntheticEvent , { discrete : true } ) ;
81
+ }
82
+
63
83
function dispatchHoverStartEvents (
64
84
event : ResponderEvent ,
65
85
context : ResponderContext ,
66
86
props : HoverProps ,
87
+ state : HoverState ,
67
88
) : void {
68
89
const { nativeEvent, target} = event ;
69
90
if ( context . isTargetWithinEventComponent ( ( nativeEvent : any ) . relatedTarget ) ) {
70
91
return ;
71
92
}
72
- if ( props . onHoverStart ) {
73
- const syntheticEvent = createHoverEvent (
74
- 'hoverstart' ,
75
- target ,
76
- props . onHoverStart ,
77
- ) ;
78
- context . dispatchEvent ( syntheticEvent , { discrete : true } ) ;
93
+
94
+ state . isHovered = true ;
95
+
96
+ if ( state . hoverEndTimeout !== null ) {
97
+ clearTimeout ( state . hoverEndTimeout ) ;
98
+ state . hoverEndTimeout = null ;
79
99
}
80
- if ( props . onHoverChange ) {
81
- const listener = ( ) => {
82
- props . onHoverChange ( true ) ;
83
- } ;
84
- const syntheticEvent = createHoverEvent ( 'hoverchange' , target , listener ) ;
85
- context . dispatchEvent ( syntheticEvent , { discrete : true } ) ;
100
+
101
+ const dispatch = ( ) => {
102
+ state . isActiveHovered = true ;
103
+
104
+ if ( props . onHoverStart ) {
105
+ const syntheticEvent = createHoverEvent (
106
+ 'hoverstart' ,
107
+ target ,
108
+ props . onHoverStart ,
109
+ ) ;
110
+ context . dispatchEvent ( syntheticEvent , { discrete : true } ) ;
111
+ }
112
+ if ( props . onHoverChange ) {
113
+ dispatchHoverChangeEvent ( event , context , props , state ) ;
114
+ }
115
+ } ;
116
+
117
+ if ( ! state . isActiveHovered ) {
118
+ const delay = calculateDelayMS ( props . delayHoverStart , 0 , 0 ) ;
119
+ if ( delay > 0 ) {
120
+ state . hoverStartTimeout = context . setTimeout ( ( ) => {
121
+ state . hoverStartTimeout = null ;
122
+ dispatch ( ) ;
123
+ } , delay ) ;
124
+ } else {
125
+ dispatch ( ) ;
126
+ }
86
127
}
87
128
}
88
129
89
130
function dispatchHoverEndEvents (
90
131
event : ResponderEvent ,
91
132
context : ResponderContext ,
92
133
props : HoverProps ,
134
+ state : HoverState ,
93
135
) {
94
136
const { nativeEvent, target} = event ;
95
137
if ( context . isTargetWithinEventComponent ( ( nativeEvent : any ) . relatedTarget ) ) {
96
138
return ;
97
139
}
98
- if ( props . onHoverEnd ) {
99
- const syntheticEvent = createHoverEvent (
100
- 'hoverend' ,
101
- target ,
102
- props . onHoverEnd ,
103
- ) ;
104
- context . dispatchEvent ( syntheticEvent , { discrete : true } ) ;
140
+
141
+ state . isHovered = false ;
142
+
143
+ if ( state . hoverStartTimeout !== null ) {
144
+ clearTimeout ( state . hoverStartTimeout ) ;
145
+ state . hoverStartTimeout = null ;
105
146
}
106
- if ( props . onHoverChange ) {
107
- const listener = ( ) => {
108
- props . onHoverChange ( false ) ;
109
- } ;
110
- const syntheticEvent = createHoverEvent ( 'hoverchange' , target , listener ) ;
111
- context . dispatchEvent ( syntheticEvent , { discrete : true } ) ;
147
+
148
+ const dispatch = ( ) => {
149
+ state . isActiveHovered = false ;
150
+
151
+ if ( props . onHoverEnd ) {
152
+ const syntheticEvent = createHoverEvent (
153
+ 'hoverend' ,
154
+ target ,
155
+ props . onHoverEnd ,
156
+ ) ;
157
+ context . dispatchEvent ( syntheticEvent , { discrete : true } ) ;
158
+ }
159
+ if ( props . onHoverChange ) {
160
+ dispatchHoverChangeEvent ( event , context , props , state ) ;
161
+ }
162
+ } ;
163
+
164
+ if ( state . isActiveHovered ) {
165
+ const delay = calculateDelayMS ( props . delayHoverEnd , 0 , 0 ) ;
166
+ if ( delay > 0 ) {
167
+ state . hoverEndTimeout = context . setTimeout ( ( ) => {
168
+ dispatch ( ) ;
169
+ } , delay ) ;
170
+ } else {
171
+ dispatch ( ) ;
172
+ }
112
173
}
113
174
}
114
175
176
+ function calculateDelayMS ( delay : ?number , min = 0 , fallback = 0 ) {
177
+ const maybeNumber = delay == null ? null : delay ;
178
+ return Math . max ( min , maybeNumber != null ? maybeNumber : fallback ) ;
179
+ }
180
+
115
181
const HoverResponder = {
116
182
targetEventTypes,
117
183
createInitialState ( ) {
118
184
return {
185
+ isActiveHovered : false ,
119
186
isHovered : false ,
120
187
isInHitSlop : false ,
121
188
isTouched : false ,
189
+ hoverStartTimeout : null ,
190
+ hoverEndTimeout : null ,
122
191
} ;
123
192
} ,
124
193
onEvent (
@@ -156,32 +225,30 @@ const HoverResponder = {
156
225
state . isInHitSlop = true ;
157
226
return ;
158
227
}
159
- dispatchHoverStartEvents ( event , context , props ) ;
160
- state . isHovered = true ;
228
+ dispatchHoverStartEvents ( event , context , props , state ) ;
161
229
}
162
230
break ;
163
231
}
164
232
case 'pointerout' :
165
233
case 'mouseout' : {
166
234
if ( state . isHovered && ! state . isTouched ) {
167
- dispatchHoverEndEvents ( event , context , props ) ;
168
- state . isHovered = false ;
235
+ dispatchHoverEndEvents ( event , context , props , state ) ;
169
236
}
170
237
state . isInHitSlop = false ;
171
238
state . isTouched = false ;
172
239
break ;
173
240
}
241
+
174
242
case 'pointermove' : {
175
- if ( ! state . isTouched ) {
243
+ if ( state . isHovered && ! state . isTouched ) {
176
244
if ( state . isInHitSlop ) {
177
245
if (
178
246
! context . isPositionWithinTouchHitTarget (
179
247
( nativeEvent : any ) . x ,
180
248
( nativeEvent : any ) . y ,
181
249
)
182
250
) {
183
- dispatchHoverStartEvents ( event , context , props ) ;
184
- state . isHovered = true ;
251
+ dispatchHoverStartEvents ( event , context , props , state ) ;
185
252
state . isInHitSlop = false ;
186
253
}
187
254
} else if (
@@ -191,17 +258,16 @@ const HoverResponder = {
191
258
( nativeEvent : any ) . y ,
192
259
)
193
260
) {
194
- dispatchHoverEndEvents ( event , context , props ) ;
195
- state . isHovered = false ;
261
+ dispatchHoverEndEvents ( event , context , props , state ) ;
196
262
state . isInHitSlop = true ;
197
263
}
198
264
}
199
265
break ;
200
266
}
267
+
201
268
case 'pointercancel' : {
202
269
if ( state . isHovered && ! state . isTouched ) {
203
- dispatchHoverEndEvents ( event , context , props ) ;
204
- state . isHovered = false ;
270
+ dispatchHoverEndEvents ( event , context , props , state ) ;
205
271
state . isTouched = false ;
206
272
}
207
273
break ;
0 commit comments