1010import type {
1111 ReactDOMResponderEvent ,
1212 ReactDOMResponderContext ,
13+ PointerType ,
1314} from 'shared/ReactDOMTypes' ;
1415import type { ReactEventResponderListener } from 'shared/ReactTypes' ;
1516
@@ -29,15 +30,14 @@ type HoverState = {
2930 hoverTarget : null | Element | Document ,
3031 isActiveHovered : boolean ,
3132 isHovered : boolean ,
32- isTouched : boolean ,
33- hoverStartTimeout : null | number ,
34- hoverEndTimeout : null | number ,
35- ignoreEmulatedMouseEvents : boolean ,
33+ isTouched ?: boolean ,
34+ ignoreEmulatedMouseEvents ?: boolean ,
3635} ;
3736
3837type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove' ;
3938
4039type HoverEvent = { |
40+ pointerType : PointerType ,
4141 target : Element | Document ,
4242 type : HoverEventType ,
4343 timeStamp : number ,
@@ -51,17 +51,8 @@ type HoverEvent = {|
5151 y : null | number ,
5252| } ;
5353
54- const targetEventTypes = [
55- 'pointerover' ,
56- 'pointermove' ,
57- 'pointerout' ,
58- 'pointercancel' ,
59- ] ;
60-
61- // If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
62- if ( typeof window !== 'undefined' && window . PointerEvent === undefined ) {
63- targetEventTypes . push ( 'touchstart' , 'mouseover' , 'mousemove' , 'mouseout' ) ;
64- }
54+ const hasPointerEvents =
55+ typeof window !== 'undefined' && window . PointerEvent != null ;
6556
6657function isFunction ( obj ) : boolean {
6758 return typeof obj === 'function' ;
@@ -79,13 +70,16 @@ function createHoverEvent(
7970 let pageY = null ;
8071 let screenX = null ;
8172 let screenY = null ;
73+ let pointerType = '' ;
8274
8375 if ( event ) {
8476 const nativeEvent = ( event . nativeEvent : any ) ;
77+ pointerType = event . pointerType ;
8578 ( { clientX, clientY, pageX, pageY, screenX, screenY} = nativeEvent ) ;
8679 }
8780
8881 return {
82+ pointerType,
8983 target,
9084 type,
9185 timeStamp : context . getTimeStamp ( ) ,
@@ -131,11 +125,6 @@ function dispatchHoverStartEvents(
131125
132126 state . isHovered = true ;
133127
134- if ( state . hoverEndTimeout !== null ) {
135- context . clearTimeout ( state . hoverEndTimeout ) ;
136- state . hoverEndTimeout = null ;
137- }
138-
139128 if ( ! state . isActiveHovered ) {
140129 state . isActiveHovered = true ;
141130 const onHoverStart = props . onHoverStart ;
@@ -152,6 +141,20 @@ function dispatchHoverStartEvents(
152141 }
153142}
154143
144+ function dispatchHoverMoveEvent ( event , context , props , state ) {
145+ const target = state . hoverTarget ;
146+ const onHoverMove = props . onHoverMove ;
147+ if ( isFunction ( onHoverMove ) ) {
148+ const syntheticEvent = createHoverEvent (
149+ event ,
150+ context ,
151+ 'hovermove' ,
152+ ( ( target : any ) : Element | Document ) ,
153+ ) ;
154+ context . dispatchEvent ( syntheticEvent , onHoverMove , UserBlockingEvent ) ;
155+ }
156+ }
157+
155158function dispatchHoverEndEvents (
156159 event : null | ReactDOMResponderEvent ,
157160 context : ReactDOMResponderContext ,
@@ -170,11 +173,6 @@ function dispatchHoverEndEvents(
170173
171174 state . isHovered = false ;
172175
173- if ( state . hoverStartTimeout !== null ) {
174- context . clearTimeout ( state . hoverStartTimeout ) ;
175- state . hoverStartTimeout = null ;
176- }
177-
178176 if ( state . isActiveHovered ) {
179177 state . isActiveHovered = false ;
180178 const onHoverEnd = props . onHoverEnd ;
@@ -189,7 +187,6 @@ function dispatchHoverEndEvents(
189187 }
190188 dispatchHoverChangeEvent ( event , context , props , state ) ;
191189 state . hoverTarget = null ;
192- state . ignoreEmulatedMouseEvents = false ;
193190 state . isTouched = false ;
194191 }
195192}
@@ -204,24 +201,17 @@ function unmountResponder(
204201 }
205202}
206203
207- function isEmulatedMouseEvent ( event , state ) {
208- const { type} = event ;
209- return (
210- state . ignoreEmulatedMouseEvents &&
211- ( type === 'mousemove' || type === 'mouseover' || type === 'mouseout' )
212- ) ;
213- }
214-
215204const hoverResponderImpl = {
216- targetEventTypes,
205+ targetEventTypes : [
206+ 'pointerover' ,
207+ 'pointermove' ,
208+ 'pointerout' ,
209+ 'pointercancel' ,
210+ ] ,
217211 getInitialState ( ) {
218212 return {
219213 isActiveHovered : false ,
220214 isHovered : false ,
221- isTouched : false ,
222- hoverStartTimeout : null ,
223- hoverEndTimeout : null ,
224- ignoreEmulatedMouseEvents : false ,
225215 } ;
226216 } ,
227217 allowMultipleHostChildren : false ,
@@ -237,95 +227,120 @@ const hoverResponderImpl = {
237227 if ( props . disabled ) {
238228 if ( state . isHovered ) {
239229 dispatchHoverEndEvents ( event , context , props , state ) ;
240- state . ignoreEmulatedMouseEvents = false ;
241- }
242- if ( state . isTouched ) {
243- state . isTouched = false ;
244230 }
245231 return ;
246232 }
247233
248234 switch ( type ) {
249235 // START
250- case 'pointerover' :
251- case 'mouseover' :
252- case 'touchstart' : {
253- if ( ! state . isHovered ) {
254- // Prevent hover events for touch
255- if ( state . isTouched || pointerType === 'touch' ) {
256- state . isTouched = true ;
257- return ;
258- }
259-
260- // Prevent hover events for emulated events
261- if ( isEmulatedMouseEvent ( event , state ) ) {
262- return ;
263- }
236+ case 'pointerover' : {
237+ if ( ! state . isHovered && pointerType !== 'touch' ) {
264238 state . hoverTarget = event . responderTarget ;
265- state . ignoreEmulatedMouseEvents = true ;
266239 dispatchHoverStartEvents ( event , context , props , state ) ;
267240 }
268- return ;
241+ break ;
269242 }
270243
271244 // MOVE
272- case 'pointermove' :
273- case 'mousemove' : {
274- if ( state . isHovered && ! isEmulatedMouseEvent ( event , state ) ) {
275- const onHoverMove = props . onHoverMove ;
276- if ( state . hoverTarget !== null && isFunction ( onHoverMove ) ) {
277- const syntheticEvent = createHoverEvent (
278- event ,
279- context ,
280- 'hovermove' ,
281- state . hoverTarget ,
282- ) ;
283- context . dispatchEvent (
284- syntheticEvent ,
285- onHoverMove ,
286- UserBlockingEvent ,
287- ) ;
288- }
245+ case 'pointermove' : {
246+ if ( state . isHovered && state . hoverTarget !== null ) {
247+ dispatchHoverMoveEvent ( event , context , props , state ) ;
289248 }
290- return ;
249+ break ;
291250 }
292251
293252 // END
294253 case 'pointerout' :
295- case 'pointercancel' :
296- case 'mouseout' :
297- case 'touchcancel' :
298- case 'touchend' : {
254+ case 'pointercancel' : {
299255 if ( state . isHovered ) {
300256 dispatchHoverEndEvents ( event , context , props , state ) ;
301- state . ignoreEmulatedMouseEvents = false ;
302257 }
303- if ( state . isTouched ) {
304- state . isTouched = false ;
305- }
306- return ;
258+ break ;
307259 }
308260 }
309261 } ,
310- onUnmount (
311- context : ReactDOMResponderContext ,
312- props : HoverProps ,
313- state : HoverState ,
314- ) {
315- unmountResponder ( context , props , state ) ;
262+ onUnmount: unmountResponder ,
263+ onOwnershipChange : unmountResponder ,
264+ } ;
265+
266+ const hoverResponderFallbackImpl = {
267+ targetEventTypes : [ 'mouseover' , 'mousemove' , 'mouseout' , 'touchstart' ] ,
268+ getInitialState ( ) {
269+ return {
270+ isActiveHovered : false ,
271+ isHovered : false ,
272+ isTouched : false ,
273+ ignoreEmulatedMouseEvents : false ,
274+ } ;
316275 } ,
317- onOwnershipChange (
276+ allowMultipleHostChildren : false ,
277+ allowEventHooks : true ,
278+ onEvent (
279+ event : ReactDOMResponderEvent ,
318280 context : ReactDOMResponderContext ,
319281 props : HoverProps ,
320282 state : HoverState ,
321- ) {
322- unmountResponder ( context , props , state ) ;
283+ ) : void {
284+ const { type} = event ;
285+
286+ if ( props . disabled ) {
287+ if ( state . isHovered ) {
288+ dispatchHoverEndEvents ( event , context , props , state ) ;
289+ state . ignoreEmulatedMouseEvents = false ;
290+ }
291+ state . isTouched = false ;
292+ return ;
293+ }
294+
295+ switch ( type ) {
296+ // START
297+ case 'mouseover' : {
298+ if ( ! state . isHovered && ! state . ignoreEmulatedMouseEvents ) {
299+ state . hoverTarget = event . responderTarget ;
300+ dispatchHoverStartEvents ( event , context , props , state ) ;
301+ }
302+ break ;
303+ }
304+
305+ // MOVE
306+ case 'mousemove' : {
307+ if (
308+ state . isHovered &&
309+ state . hoverTarget !== null &&
310+ ! state . ignoreEmulatedMouseEvents
311+ ) {
312+ dispatchHoverMoveEvent ( event , context , props , state ) ;
313+ } else if ( ! state . isHovered && type === 'mousemove' ) {
314+ state . ignoreEmulatedMouseEvents = false ;
315+ state . isTouched = false ;
316+ }
317+ break ;
318+ }
319+
320+ // END
321+ case 'mouseout' : {
322+ if ( state . isHovered ) {
323+ dispatchHoverEndEvents ( event , context , props , state ) ;
324+ }
325+ break ;
326+ }
327+
328+ case 'touchstart' : {
329+ if ( ! state . isHovered ) {
330+ state . isTouched = true ;
331+ state . ignoreEmulatedMouseEvents = true ;
332+ }
333+ break ;
334+ }
335+ }
323336 } ,
337+ onUnmount: unmountResponder ,
338+ onOwnershipChange : unmountResponder ,
324339} ;
325340
326341export const HoverResponder = React . unstable_createResponder (
327342 'Hover' ,
328- hoverResponderImpl ,
343+ hasPointerEvents ? hoverResponderImpl : hoverResponderFallbackImpl ,
329344) ;
330345
331346export function useHoverResponder (
0 commit comments