[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
});