React Events: cancel onLongPress for large enough moves (#15562)
Implements the behaviour from React Native's Pressability.
Nicolas Gallagher committed
May 3, 2019 at 12:59 UTC
307e0a7d7d6a03a228c1df7ab8a5391b8f766268
2 files changed
+67
-16
packages/react-events/src/Press.js
+50
-16
@@ -44,6 +44,10 @@ type PressProps = {
44
type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';
45
46
type PressState = {
47
+ activationPosition: null | $ReadOnly<{|
48
+ pageX: number,
49
+ pageY: number,
50
+ |}>,
51
addedRootEvents: boolean,
52
isActivePressed: boolean,
53
isActivePressStart: boolean,
@@ -174,9 +178,18 @@ function dispatchLongPressChangeEvent(
178
dispatchEvent(context, state, 'longpresschange', listener);
179
}
180
177
-function activate(context, props, state) {
181
+function activate(event, context, props, state) {
182
+ const nativeEvent: any = event.nativeEvent;
183
+ const pageX = nativeEvent.pageX;
184
+ const pageY = nativeEvent.pageY;
185
const wasActivePressed = state.isActivePressed;
186
state.isActivePressed = true;
187
+ if (pageX != null && pageY != null) {
188
+ state.activationPosition = {
189
+ pageX: nativeEvent.pageX,
190
+ pageY: nativeEvent.pageY,
191
+ };
192
+ }
193
194
if (props.onPressStart) {
195
dispatchEvent(context, state, 'pressstart', props.onPressStart);
@@ -203,6 +216,7 @@ function deactivate(context, props, state) {
216
}
217
218
function dispatchPressStartEvents(
219
+ event: ReactResponderEvent,
220
context: ReactResponderContext,
221
props: PressProps,
222
state: PressState,
@@ -216,7 +230,7 @@ function dispatchPressStartEvents(
230
231
const dispatch = () => {
232
state.isActivePressStart = true;
219
- activate(context, props, state);
233
+ activate(event, context, props, state);
234
235
if (
236
(props.onLongPress || props.onLongPressChange) &&
@@ -258,6 +272,7 @@ function dispatchPressStartEvents(
272
}
273
274
function dispatchPressEndEvents(
275
+ event: ?ReactResponderEvent,
276
context: ReactResponderContext,
277
props: PressProps,
278
state: PressState,
@@ -277,9 +292,9 @@ function dispatchPressEndEvents(
292
context.clearTimeout(state.pressStartTimeout);
293
state.pressStartTimeout = null;
294
// don't activate if a press has moved beyond the responder region
280
- if (state.isPressWithinResponderRegion) {
295
+ if (state.isPressWithinResponderRegion && event != null) {
296
// if we haven't yet activated (due to delays), activate now
282
- activate(context, props, state);
297
+ activate(event, context, props, state);
298
activationWasForced = true;
299
}
300
}
@@ -396,24 +411,26 @@ function unmountResponder(
411
): void {
412
if (state.isPressed) {
413
removeRootEventTypes(context, state);
399
- dispatchPressEndEvents(context, props, state);
414
+ dispatchPressEndEvents(null, context, props, state);
415
}
416
}
417
418
function dispatchCancel(
404
- type: string,
405
- nativeEvent: $PropertyType<ReactResponderEvent, 'nativeEvent'>,
419
+ event: ReactResponderEvent,
420
context: ReactResponderContext,
421
props: PressProps,
422
state: PressState,
423
): void {
424
+ const nativeEvent: any = event.nativeEvent;
425
+ const type = event.type;
426
+
427
if (state.isPressed) {
428
if (type === 'contextmenu' && props.preventDefault !== false) {
412
- (nativeEvent: any).preventDefault();
429
+ nativeEvent.preventDefault();
430
} else {
431
state.ignoreEmulatedMouseEvents = false;
432
removeRootEventTypes(context, state);
416
- dispatchPressEndEvents(context, props, state);
433
+ dispatchPressEndEvents(event, context, props, state);
434
}
435
} else if (state.allowPressReentry) {
436
removeRootEventTypes(context, state);
@@ -445,6 +462,7 @@ const PressResponder = {
462
targetEventTypes,
463
createInitialState(): PressState {
464
return {
465
+ activationPosition: null,
466
addedRootEvents: false,
467
didDispatchEvent: false,
468
isActivePressed: false,
@@ -474,7 +492,7 @@ const PressResponder = {
492
493
if (props.disabled) {
494
removeRootEventTypes(context, state);
477
- dispatchPressEndEvents(context, props, state);
495
+ dispatchPressEndEvents(event, context, props, state);
496
state.ignoreEmulatedMouseEvents = false;
497
return;
498
}
@@ -527,7 +545,7 @@ const PressResponder = {
545
props,
546
);
547
state.isPressWithinResponderRegion = true;
530
- dispatchPressStartEvents(context, props, state);
548
+ dispatchPressStartEvents(event, context, props, state);
549
addRootEventTypes(context, state);
550
} else {
551
// Prevent spacebar press from scrolling the window
@@ -540,7 +558,7 @@ const PressResponder = {
558
559
// CANCEL
560
case 'contextmenu': {
543
- dispatchCancel(type, nativeEvent, context, props, state);
561
+ dispatchCancel(event, context, props, state);
562
break;
563
}
564
@@ -603,18 +621,34 @@ const PressResponder = {
621
discrete: false,
622
});
623
}
624
+ if (
625
+ state.activationPosition != null &&
626
+ state.longPressTimeout != null
627
+ ) {
628
+ const deltaX =
629
+ state.activationPosition.pageX - nativeEvent.pageX;
630
+ const deltaY =
631
+ state.activationPosition.pageY - nativeEvent.pageY;
632
+ if (
633
+ Math.hypot(deltaX, deltaY) > 10 &&
634
+ state.longPressTimeout != null
635
+ ) {
636
+ context.clearTimeout(state.longPressTimeout);
637
+ }
638
+ }
639
} else {
607
- dispatchPressStartEvents(context, props, state);
640
+ dispatchPressStartEvents(event, context, props, state);
641
}
642
} else {
643
if (!state.allowPressReentry) {
644
removeRootEventTypes(context, state);
645
}
613
- dispatchPressEndEvents(context, props, state);
646
+ dispatchPressEndEvents(event, context, props, state);
647
}
648
}
649
break;
650
}
651
+
652
// END
653
case 'pointerup':
654
case 'keyup':
@@ -650,7 +684,7 @@ const PressResponder = {
684
685
const wasLongPressed = state.isLongPressed;
686
removeRootEventTypes(context, state);
653
- dispatchPressEndEvents(context, props, state);
687
+ dispatchPressEndEvents(event, context, props, state);
688
689
if (state.pressTarget !== null && props.onPress) {
690
if (state.isPressWithinResponderRegion) {
@@ -677,7 +711,7 @@ const PressResponder = {
711
case 'pointercancel':
712
case 'scroll':
713
case 'touchcancel': {
680
- dispatchCancel(type, nativeEvent, context, props, state);
714
+ dispatchCancel(event, context, props, state);
715
}
716
}
717
},
packages/react-events/src/__tests__/Press-test.internal.js
+17
@@ -611,6 +611,23 @@ describe('Event responder: Press', () => {
611
expect(onLongPress).not.toBeCalled();
612
});
613
614
+ it('is not called when a large enough move occurs before delay', () => {
615
+ ref.current.getBoundingClientRect = () => ({
616
+ top: 0,
617
+ left: 0,
618
+ bottom: 100,
619
+ right: 100,
620
+ });
621
+ ref.current.dispatchEvent(
622
+ createPointerEvent('pointerdown', {pageX: 10, pageY: 10}),
623
+ );
624
+ ref.current.dispatchEvent(
625
+ createPointerEvent('pointermove', {pageX: 50, pageY: 50}),
626
+ );
627
+ jest.runAllTimers();
628
+ expect(onLongPress).not.toBeCalled();
629
+ });
630
+
631
describe('delayLongPress', () => {
632
it('can be configured', () => {
633
const element = (