@samitouri / QOS-React-2 / commits / 824e9bec7a

[Flare] Fix issues with touch + pointer interactions (#15997)

Dominic Gannaway committed Jun 26, 2019 at 22:25 UTC 824e9bec7a3f5aa290e5f4e7d7ad52261a7c5d40
2 files changed +125 -94
packages/react-events/src/dom/Press.js
+74 -87
@@ -75,6 +75,7 @@ type PressState = {
75 ignoreEmulatedMouseEvents: boolean,
76 activePointerId: null | number,
77 shouldPreventClick: boolean,
78 + touchEvent: null | Touch,
79 };
80
81 type PressEventType =
@@ -160,6 +161,7 @@ function createPressEvent(
161 target: Element | Document,
162 pointerType: PointerType,
163 event: ?ReactDOMResponderEvent,
164 + touchEvent: null | Touch,
165 defaultPrevented: boolean,
166 ): PressEvent {
167 const timeStamp = context.getTimeStamp();
@@ -180,11 +182,7 @@ function createPressEvent(
182 // Only check for one property, checking for all of them is costly. We can assume
183 // if clientX exists, so do the rest.
184 let eventObject;
183 - if (nativeEvent.clientX !== undefined) {
184 - eventObject = (nativeEvent: any);
185 - } else if (isNativeTouchEvent(nativeEvent)) {
186 - eventObject = getTouchFromPressEvent(nativeEvent);
187 - }
185 + eventObject = (touchEvent: any) || (nativeEvent: any);
186 if (eventObject) {
187 ({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject);
188 }
@@ -223,12 +221,14 @@ function dispatchEvent(
221 const defaultPrevented =
222 (event != null && event.nativeEvent.defaultPrevented === true) ||
223 (name === 'press' && state.shouldPreventClick);
224 + const touchEvent = state.touchEvent;
225 const syntheticEvent = createPressEvent(
226 context,
227 name,
228 target,
229 pointerType,
230 event,
231 + touchEvent,
232 defaultPrevented,
233 );
234 context.dispatchEvent(syntheticEvent, listener, eventPriority);
@@ -269,10 +269,10 @@ function dispatchLongPressChangeEvent(
269
270 function activate(event: ReactDOMResponderEvent, context, props, state) {
271 const nativeEvent: any = event.nativeEvent;
272 - const {x, y} = getEventViewportCoords(nativeEvent);
272 + const {clientX: x, clientY: y} = state.touchEvent || nativeEvent;
273 const wasActivePressed = state.isActivePressed;
274 state.isActivePressed = true;
275 - if (x !== null && y !== null) {
275 + if (x !== undefined && y !== undefined) {
276 state.activationPosition = {x, y};
277 }
278
@@ -433,6 +433,7 @@ function dispatchCancel(
433 props: PressProps,
434 state: PressState,
435 ): void {
436 + state.touchEvent = null;
437 if (state.isPressed) {
438 state.ignoreEmulatedMouseEvents = false;
439 dispatchPressEndEvents(event, context, props, state);
@@ -495,33 +496,12 @@ function calculateResponderRegion(
496 };
497 }
498
498 -function isNativeTouchEvent(nativeEvent: Event): boolean {
499 - const changedTouches = ((nativeEvent: any): TouchEvent).changedTouches;
500 - return changedTouches && typeof changedTouches.length === 'number';
501 -}
502 -
503 -function getTouchFromPressEvent(nativeEvent: TouchEvent): Touch {
504 - const {changedTouches, touches} = nativeEvent;
505 - return changedTouches.length > 0
506 - ? changedTouches[0]
507 - : touches.length > 0
508 - ? touches[0]
509 - : (nativeEvent: any);
510 -}
511 -
512 -function getEventViewportCoords(
513 - nativeEvent: Event,
514 -): {x: null | number, y: null | number} {
515 - let eventObject = (nativeEvent: any);
516 - if (isNativeTouchEvent(eventObject)) {
517 - eventObject = getTouchFromPressEvent(eventObject);
499 +function getTouchFromPressEvent(nativeEvent: TouchEvent): null | Touch {
500 + const targetTouches = nativeEvent.targetTouches;
501 + if (targetTouches.length > 0) {
502 + return targetTouches[0];
503 }
519 - const x = eventObject.clientX;
520 - const y = eventObject.clientY;
521 - return {
522 - x: x != null ? x : null,
523 - y: y != null ? y : null,
524 - };
504 + return null;
505 }
506
507 function unmountResponder(
@@ -575,54 +555,46 @@ function getTouchTarget(context: ReactDOMResponderContext, touchEvent: Touch) {
555 }
556
557 function updateIsPressWithinResponderRegion(
578 - target: Element | Document,
558 nativeEventOrTouchEvent: Event | Touch,
559 context: ReactDOMResponderContext,
560 props: PressProps,
561 state: PressState,
562 ): void {
584 - let isPressWithinResponderRegion = true;
585 - if (
586 - state.pressTarget != null &&
587 - !context.isTargetWithinElement(target, state.pressTarget)
588 - ) {
589 - // Calculate the responder region we use for deactivation if not
590 - // already done during move event.
591 - if (state.responderRegionOnDeactivation == null) {
592 - state.responderRegionOnDeactivation = calculateResponderRegion(
593 - context,
594 - state.pressTarget,
595 - props,
596 - );
597 - }
598 - const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
599 - let left, top, right, bottom;
600 -
601 - if (responderRegionOnActivation != null) {
602 - left = responderRegionOnActivation.left;
603 - top = responderRegionOnActivation.top;
604 - right = responderRegionOnActivation.right;
605 - bottom = responderRegionOnActivation.bottom;
606 -
607 - if (responderRegionOnDeactivation != null) {
608 - left = Math.min(left, responderRegionOnDeactivation.left);
609 - top = Math.min(top, responderRegionOnDeactivation.top);
610 - right = Math.max(right, responderRegionOnDeactivation.right);
611 - bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
612 - }
563 + // Calculate the responder region we use for deactivation if not
564 + // already done during move event.
565 + if (state.responderRegionOnDeactivation == null) {
566 + state.responderRegionOnDeactivation = calculateResponderRegion(
567 + context,
568 + ((state.pressTarget: any): Element),
569 + props,
570 + );
571 + }
572 + const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
573 + let left, top, right, bottom;
574 +
575 + if (responderRegionOnActivation != null) {
576 + left = responderRegionOnActivation.left;
577 + top = responderRegionOnActivation.top;
578 + right = responderRegionOnActivation.right;
579 + bottom = responderRegionOnActivation.bottom;
580 +
581 + if (responderRegionOnDeactivation != null) {
582 + left = Math.min(left, responderRegionOnDeactivation.left);
583 + top = Math.min(top, responderRegionOnDeactivation.top);
584 + right = Math.max(right, responderRegionOnDeactivation.right);
585 + bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
586 }
614 - const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any);
615 -
616 - isPressWithinResponderRegion =
617 - left != null &&
618 - right != null &&
619 - top != null &&
620 - bottom != null &&
621 - x !== null &&
622 - y !== null &&
623 - (x >= left && x <= right && y >= top && y <= bottom);
587 }
625 - state.isPressWithinResponderRegion = isPressWithinResponderRegion;
588 + const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any);
589 +
590 + state.isPressWithinResponderRegion =
591 + left != null &&
592 + right != null &&
593 + top != null &&
594 + bottom != null &&
595 + x !== null &&
596 + y !== null &&
597 + (x >= left && x <= right && y >= top && y <= bottom);
598 }
599
600 function handleStopPropagation(
@@ -650,7 +622,6 @@ const PressResponder: ReactDOMEventResponder = {
622 return {
623 activationPosition: null,
624 addedRootEvents: false,
653 - didDispatchEvent: false,
625 isActivePressed: false,
626 isActivePressStart: false,
627 isLongPressed: false,
@@ -666,6 +637,7 @@ const PressResponder: ReactDOMEventResponder = {
637 ignoreEmulatedMouseEvents: false,
638 activePointerId: null,
639 shouldPreventClick: false,
640 + touchEvent: null,
641 };
642 },
643 allowMultipleHostChildren: false,
@@ -731,6 +703,10 @@ const PressResponder: ReactDOMEventResponder = {
703 state.activePointerId = pointerId;
704 } else if (isTouchEvent) {
705 const touchEvent = getTouchFromPressEvent(nativeEvent);
706 + if (touchEvent === null) {
707 + return;
708 + }
709 + state.touchEvent = touchEvent;
710 state.activePointerId = touchEvent.identifier;
711 }
712
@@ -852,18 +828,23 @@ const PressResponder: ReactDOMEventResponder = {
828 if (touchEvent === null) {
829 return;
830 }
855 - target = getTouchTarget(context, touchEvent);
831 + state.touchEvent = touchEvent;
832 }
833
858 - // Calculate the responder region we use for deactivation, as the
859 - // element dimensions may have changed since activation.
860 - updateIsPressWithinResponderRegion(
861 - target,
862 - touchEvent || nativeEvent,
863 - context,
864 - props,
865 - state,
866 - );
834 + if (
835 + state.pressTarget !== null &&
836 + (pointerType !== 'mouse' ||
837 + !context.isTargetWithinElement(target, state.pressTarget))
838 + ) {
839 + // Calculate the responder region we use for deactivation, as the
840 + // element dimensions may have changed since activation.
841 + updateIsPressWithinResponderRegion(
842 + touchEvent || nativeEvent,
843 + context,
844 + props,
845 + state,
846 + );
847 + }
848
849 if (state.isPressWithinResponderRegion) {
850 if (isPressed) {
@@ -914,6 +895,7 @@ const PressResponder: ReactDOMEventResponder = {
895 if (touchEvent === null) {
896 return;
897 }
898 + state.touchEvent = touchEvent;
899 target = getTouchTarget(context, touchEvent);
900 } else if (type === 'keyup') {
901 // Ignore unrelated keyboard events
@@ -961,12 +943,16 @@ const PressResponder: ReactDOMEventResponder = {
943 dispatchPressEndEvents(event, context, props, state);
944
945 if (state.pressTarget !== null && props.onPress) {
964 - if (!isKeyboardEvent) {
946 + if (
947 + !isKeyboardEvent &&
948 + state.pressTarget !== null &&
949 + (pointerType !== 'mouse' ||
950 + !context.isTargetWithinElement(target, state.pressTarget))
951 + ) {
952 // If the event target isn't within the press target, check if we're still
953 // within the responder region. The region may have changed if the
954 // element's layout was modified after activation.
955 updateIsPressWithinResponderRegion(
969 - target,
956 touchEvent || nativeEvent,
957 context,
958 props,
@@ -992,6 +978,7 @@ const PressResponder: ReactDOMEventResponder = {
978 }
979 }
980 }
981 + state.touchEvent = null;
982 } else if (type === 'mouseup') {
983 state.ignoreEmulatedMouseEvents = false;
984 }
packages/react-events/src/dom/__tests__/Press-test.internal.js
+51 -7
@@ -36,6 +36,12 @@ function createTouchEvent(type, id, data) {
36 identifier: id,
37 },
38 ],
39 + targetTouches: [
40 + {
41 + ...data,
42 + identifier: id,
43 + },
44 + ],
45 });
46 }
47
@@ -618,14 +624,24 @@ describe('Event responder: Press', () => {
624 ref.current.dispatchEvent(
625 createTouchEvent('touchstart', 0, {
626 target: ref.current,
627 + clientX: 0,
628 + clientY: 0,
629 }),
630 );
631 ref.current.dispatchEvent(
632 createTouchEvent('touchend', 0, {
633 target: ref.current,
634 + clientX: 0,
635 + clientY: 0,
636 + }),
637 + );
638 + ref.current.dispatchEvent(
639 + createEvent('pointerup', {
640 + pointerType: 'pen',
641 + clientX: 0,
642 + clientY: 0,
643 }),
644 );
628 - ref.current.dispatchEvent(createEvent('pointerup', {pointerType: 'pen'}));
645 expect(onPress).toHaveBeenCalledTimes(1);
646 expect(onPress).toHaveBeenCalledWith(
647 expect.objectContaining({pointerType: 'pen', type: 'press'}),
@@ -2022,6 +2038,7 @@ describe('Event responder: Press', () => {
2038 document.elementFromPoint = () => ref.current;
2039 ref.current.dispatchEvent(
2040 createTouchEvent('touchend', 0, {
2041 + ...coordinatesInside,
2042 target: ref.current,
2043 }),
2044 );
@@ -2269,7 +2286,12 @@ describe('Event responder: Press', () => {
2286 ReactDOM.render(element, container);
2287
2288 ref.current.dispatchEvent(createEvent('pointerdown'));
2272 - ref.current.dispatchEvent(createEvent('pointerup'));
2289 + ref.current.dispatchEvent(
2290 + createEvent('pointerup', {
2291 + clientX: 0,
2292 + clientY: 0,
2293 + }),
2294 + );
2295 ref.current.dispatchEvent(createEvent('click', {preventDefault}));
2296 expect(preventDefault).toBeCalled();
2297 expect(onPress).toHaveBeenCalledWith(
@@ -2291,7 +2313,12 @@ describe('Event responder: Press', () => {
2313 ReactDOM.render(element, container);
2314
2315 buttonRef.current.dispatchEvent(createEvent('pointerdown'));
2294 - buttonRef.current.dispatchEvent(createEvent('pointerup'));
2316 + buttonRef.current.dispatchEvent(
2317 + createEvent('pointerup', {
2318 + clientX: 0,
2319 + clientY: 0,
2320 + }),
2321 + );
2322 buttonRef.current.dispatchEvent(createEvent('click', {preventDefault}));
2323 expect(preventDefault).toBeCalled();
2324 });
@@ -2310,7 +2337,12 @@ describe('Event responder: Press', () => {
2337 ReactDOM.render(element, container);
2338
2339 ref.current.dispatchEvent(createEvent('pointerdown'));
2313 - ref.current.dispatchEvent(createEvent('pointerup'));
2340 + ref.current.dispatchEvent(
2341 + createEvent('pointerup', {
2342 + clientX: 0,
2343 + clientY: 0,
2344 + }),
2345 + );
2346 ref.current.dispatchEvent(createEvent('click', {preventDefault}));
2347 expect(preventDefault).toBeCalled();
2348 expect(onPress).toHaveBeenCalledWith(
@@ -2334,7 +2366,11 @@ describe('Event responder: Press', () => {
2366 createEvent('pointerdown', {[modifierKey]: true}),
2367 );
2368 ref.current.dispatchEvent(
2337 - createEvent('pointerup', {[modifierKey]: true}),
2369 + createEvent('pointerup', {
2370 + [modifierKey]: true,
2371 + clientX: 0,
2372 + clientY: 0,
2373 + }),
2374 );
2375 ref.current.dispatchEvent(
2376 createEvent('click', {[modifierKey]: true, preventDefault}),
@@ -2358,7 +2394,12 @@ describe('Event responder: Press', () => {
2394 ReactDOM.render(element, container);
2395
2396 ref.current.dispatchEvent(createEvent('pointerdown'));
2361 - ref.current.dispatchEvent(createEvent('pointerup'));
2397 + ref.current.dispatchEvent(
2398 + createEvent('pointerup', {
2399 + clientX: 0,
2400 + clientY: 0,
2401 + }),
2402 + );
2403 ref.current.dispatchEvent(createEvent('click', {preventDefault}));
2404 expect(preventDefault).not.toBeCalled();
2405 expect(onPress).toHaveBeenCalledWith(
@@ -2694,7 +2735,10 @@ describe('Event responder: Press', () => {
2735 });
2736
2737 function dispatchEventWithTimeStamp(elem, name, timeStamp) {
2697 - const event = createEvent(name);
2738 + const event = createEvent(name, {
2739 + clientX: 0,
2740 + clientY: 0,
2741 + });
2742 Object.defineProperty(event, 'timeStamp', {
2743 value: timeStamp,
2744 });