@samitouri / QOS-React-2 / commits / 307e0a7d7d

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 = (