@@ -24,8 +24,14 @@ type PressProps = {
24
24
onPress : ( e : PressEvent ) => void ,
25
25
onPressChange : boolean => void ,
26
26
onPressEnd : ( e : PressEvent ) => void ,
27
+ onPressMove : ( e : PressEvent ) => void ,
27
28
onPressStart : ( e : PressEvent ) => void ,
28
- pressRententionOffset : Object ,
29
+ pressRetentionOffset : {
30
+ top : number ,
31
+ right : number ,
32
+ bottom : number ,
33
+ left : number ,
34
+ } ,
29
35
} ;
30
36
31
37
type PressState = {
@@ -35,15 +41,23 @@ type PressState = {
35
41
isAnchorTouched : boolean ,
36
42
isLongPressed : boolean ,
37
43
isPressed : boolean ,
44
+ isPressWithinResponderRegion : boolean ,
38
45
longPressTimeout : null | TimeoutID ,
39
46
pressTarget : null | Element | Document ,
40
47
pressEndTimeout : null | TimeoutID ,
41
48
pressStartTimeout : null | TimeoutID ,
49
+ responderRegion : null | $ReadOnly < { |
50
+ bottom : number ,
51
+ left : number ,
52
+ right : number ,
53
+ top : number ,
54
+ | } > ,
42
55
shouldSkipMouseAfterTouch : boolean ,
43
56
} ;
44
57
45
58
type PressEventType =
46
59
| 'press'
60
+ | 'pressmove'
47
61
| 'pressstart'
48
62
| 'pressend'
49
63
| 'presschange'
@@ -59,6 +73,12 @@ type PressEvent = {|
59
73
const DEFAULT_PRESS_END_DELAY_MS = 0 ;
60
74
const DEFAULT_PRESS_START_DELAY_MS = 0 ;
61
75
const DEFAULT_LONG_PRESS_DELAY_MS = 500 ;
76
+ const DEFAULT_PRESS_RETENTION_OFFSET = {
77
+ bottom : 20 ,
78
+ top : 20 ,
79
+ left : 20 ,
80
+ right : 20 ,
81
+ } ;
62
82
63
83
const targetEventTypes = [
64
84
{ name : 'click' , passive : false } ,
@@ -70,13 +90,18 @@ const targetEventTypes = [
70
90
const rootEventTypes = [
71
91
{ name : 'keyup' , passive : false } ,
72
92
{ name : 'pointerup' , passive : false } ,
93
+ 'pointermove' ,
73
94
'scroll' ,
74
95
] ;
75
96
76
97
// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
77
98
if ( typeof window !== 'undefined' && window . PointerEvent === undefined ) {
78
- targetEventTypes . push ( 'touchstart' , 'touchend' , 'mousedown' , 'touchcancel' ) ;
79
- rootEventTypes . push ( { name : 'mouseup' , passive : false } ) ;
99
+ targetEventTypes . push ( 'touchstart' , 'touchend' , 'touchcancel' , 'mousedown' ) ;
100
+ rootEventTypes . push (
101
+ { name : 'mouseup' , passive : false } ,
102
+ 'touchmove' ,
103
+ 'mousemove' ,
104
+ ) ;
80
105
}
81
106
82
107
function createPressEvent (
@@ -232,8 +257,11 @@ function dispatchPressEndEvents(
232
257
if ( ! wasActivePressStart && state . pressStartTimeout !== null ) {
233
258
clearTimeout ( state . pressStartTimeout ) ;
234
259
state . pressStartTimeout = null ;
235
- // if we haven't yet activated (due to delays), activate now
236
- activate ( context , props , state ) ;
260
+ // don't activate if a press has moved beyond the responder region
261
+ if ( state . isPressWithinResponderRegion ) {
262
+ // if we haven't yet activated (due to delays), activate now
263
+ activate ( context , props , state ) ;
264
+ }
237
265
}
238
266
239
267
if ( state . isActivePressed ) {
@@ -267,6 +295,59 @@ function calculateDelayMS(delay: ?number, min = 0, fallback = 0) {
267
295
return Math . max ( min , maybeNumber != null ? maybeNumber : fallback ) ;
268
296
}
269
297
298
+ // TODO: account for touch hit slop
299
+ function calculateResponderRegion ( target , props ) {
300
+ const pressRetentionOffset = {
301
+ ...DEFAULT_PRESS_RETENTION_OFFSET ,
302
+ ...props . pressRetentionOffset ,
303
+ } ;
304
+
305
+ const clientRect = target . getBoundingClientRect ( ) ;
306
+
307
+ let bottom = clientRect . bottom ;
308
+ let left = clientRect . left ;
309
+ let right = clientRect . right ;
310
+ let top = clientRect . top ;
311
+
312
+ if ( pressRetentionOffset ) {
313
+ if ( pressRetentionOffset . bottom != null ) {
314
+ bottom += pressRetentionOffset . bottom ;
315
+ }
316
+ if ( pressRetentionOffset . left != null ) {
317
+ left -= pressRetentionOffset . left ;
318
+ }
319
+ if ( pressRetentionOffset . right != null ) {
320
+ right += pressRetentionOffset . right ;
321
+ }
322
+ if ( pressRetentionOffset . top != null ) {
323
+ top -= pressRetentionOffset . top ;
324
+ }
325
+ }
326
+
327
+ return {
328
+ bottom,
329
+ top,
330
+ left,
331
+ right,
332
+ } ;
333
+ }
334
+
335
+ function isPressWithinResponderRegion (
336
+ nativeEvent : $PropertyType < ResponderEvent , 'nativeEvent' > ,
337
+ state : PressState ,
338
+ ) : boolean {
339
+ const { responderRegion } = state ;
340
+ const event = ( nativeEvent : any ) ;
341
+
342
+ return (
343
+ responderRegion != null &&
344
+ ( event . pageX >= responderRegion . left &&
345
+ event . pageX <= responderRegion . right &&
346
+ event . pageY >= responderRegion . top &&
347
+ event . pageY <= responderRegion . bottom )
348
+ ) ;
349
+ }
350
+
270
351
function unmountResponder (
271
352
context : ReactResponderContext ,
272
353
props : PressProps ,
@@ -288,10 +369,12 @@ const PressResponder = {
288
369
isAnchorTouched : false ,
289
370
isLongPressed : false ,
290
371
isPressed : false ,
372
+ isPressWithinResponderRegion : true ,
291
373
longPressTimeout : null ,
292
374
pressEndTimeout : null ,
293
375
pressStartTimeout : null ,
294
376
pressTarget : null ,
377
+ responderRegion : null ,
295
378
shouldSkipMouseAfterTouch : false ,
296
379
} ;
297
380
} ,
@@ -333,11 +416,46 @@ const PressResponder = {
333
416
}
334
417
}
335
418
state . pressTarget = target ;
419
+ state . isPressWithinResponderRegion = true ;
336
420
dispatchPressStartEvents ( context , props , state ) ;
337
421
context . addRootEventTypes ( target . ownerDocument , rootEventTypes ) ;
338
422
}
339
423
break ;
340
424
}
425
+ case 'pointermove' :
426
+ case 'mousemove ':
427
+ case 'touchmove ': {
428
+ if ( state . isPressed ) {
429
+ if ( state . shouldSkipMouseAfterTouch ) {
430
+ return ;
431
+ }
432
+
433
+ if ( state . responderRegion == null ) {
434
+ let currentTarget = ( target : any ) ;
435
+ while (
436
+ currentTarget . parentNode &&
437
+ context . isTargetWithinEventComponent ( currentTarget . parentNode )
438
+ ) {
439
+ currentTarget = currentTarget . parentNode ;
440
+ }
441
+ state . responderRegion = calculateResponderRegion (
442
+ currentTarget ,
443
+ props ,
444
+ ) ;
445
+ }
446
+
447
+ if ( isPressWithinResponderRegion ( nativeEvent , state ) ) {
448
+ state . isPressWithinResponderRegion = true ;
449
+ if ( props . onPressMove ) {
450
+ dispatchEvent ( context , state , 'pressmove' , props . onPressMove ) ;
451
+ }
452
+ } else {
453
+ state . isPressWithinResponderRegion = false ;
454
+ dispatchPressEndEvents ( context , props , state ) ;
455
+ }
456
+ }
457
+ break ;
458
+ }
341
459
case 'pointerup' :
342
460
case 'mouseup ': {
343
461
if ( state . isPressed ) {
@@ -373,6 +491,7 @@ const PressResponder = {
373
491
context . removeRootEventTypes ( rootEventTypes ) ;
374
492
}
375
493
state . isAnchorTouched = false ;
494
+ state . shouldSkipMouseAfterTouch = false ;
376
495
break ;
377
496
}
378
497
@@ -389,6 +508,7 @@ const PressResponder = {
389
508
return ;
390
509
}
391
510
state . pressTarget = target ;
511
+ state . isPressWithinResponderRegion = true ;
392
512
dispatchPressStartEvents ( context , props , state ) ;
393
513
context . addRootEventTypes ( target . ownerDocument , rootEventTypes ) ;
394
514
}
0 commit comments