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

This commit is contained in:
Dominic Gannaway
2019-06-26 22:25:49 +01:00
committed by GitHub
parent dd93357aa0
commit 824e9bec7a
2 changed files with 126 additions and 95 deletions
+75 -88
View File
@@ -75,6 +75,7 @@ type PressState = {
ignoreEmulatedMouseEvents: boolean,
activePointerId: null | number,
shouldPreventClick: boolean,
touchEvent: null | Touch,
};
type PressEventType =
@@ -160,6 +161,7 @@ function createPressEvent(
target: Element | Document,
pointerType: PointerType,
event: ?ReactDOMResponderEvent,
touchEvent: null | Touch,
defaultPrevented: boolean,
): PressEvent {
const timeStamp = context.getTimeStamp();
@@ -180,11 +182,7 @@ function createPressEvent(
// Only check for one property, checking for all of them is costly. We can assume
// if clientX exists, so do the rest.
let eventObject;
if (nativeEvent.clientX !== undefined) {
eventObject = (nativeEvent: any);
} else if (isNativeTouchEvent(nativeEvent)) {
eventObject = getTouchFromPressEvent(nativeEvent);
}
eventObject = (touchEvent: any) || (nativeEvent: any);
if (eventObject) {
({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject);
}
@@ -223,12 +221,14 @@ function dispatchEvent(
const defaultPrevented =
(event != null && event.nativeEvent.defaultPrevented === true) ||
(name === 'press' && state.shouldPreventClick);
const touchEvent = state.touchEvent;
const syntheticEvent = createPressEvent(
context,
name,
target,
pointerType,
event,
touchEvent,
defaultPrevented,
);
context.dispatchEvent(syntheticEvent, listener, eventPriority);
@@ -269,10 +269,10 @@ function dispatchLongPressChangeEvent(
function activate(event: ReactDOMResponderEvent, context, props, state) {
const nativeEvent: any = event.nativeEvent;
const {x, y} = getEventViewportCoords(nativeEvent);
const {clientX: x, clientY: y} = state.touchEvent || nativeEvent;
const wasActivePressed = state.isActivePressed;
state.isActivePressed = true;
if (x !== null && y !== null) {
if (x !== undefined && y !== undefined) {
state.activationPosition = {x, y};
}
@@ -433,6 +433,7 @@ function dispatchCancel(
props: PressProps,
state: PressState,
): void {
state.touchEvent = null;
if (state.isPressed) {
state.ignoreEmulatedMouseEvents = false;
dispatchPressEndEvents(event, context, props, state);
@@ -495,33 +496,12 @@ function calculateResponderRegion(
};
}
function isNativeTouchEvent(nativeEvent: Event): boolean {
const changedTouches = ((nativeEvent: any): TouchEvent).changedTouches;
return changedTouches && typeof changedTouches.length === 'number';
}
function getTouchFromPressEvent(nativeEvent: TouchEvent): Touch {
const {changedTouches, touches} = nativeEvent;
return changedTouches.length > 0
? changedTouches[0]
: touches.length > 0
? touches[0]
: (nativeEvent: any);
}
function getEventViewportCoords(
nativeEvent: Event,
): {x: null | number, y: null | number} {
let eventObject = (nativeEvent: any);
if (isNativeTouchEvent(eventObject)) {
eventObject = getTouchFromPressEvent(eventObject);
function getTouchFromPressEvent(nativeEvent: TouchEvent): null | Touch {
const targetTouches = nativeEvent.targetTouches;
if (targetTouches.length > 0) {
return targetTouches[0];
}
const x = eventObject.clientX;
const y = eventObject.clientY;
return {
x: x != null ? x : null,
y: y != null ? y : null,
};
return null;
}
function unmountResponder(
@@ -575,54 +555,46 @@ function getTouchTarget(context: ReactDOMResponderContext, touchEvent: Touch) {
}
function updateIsPressWithinResponderRegion(
target: Element | Document,
nativeEventOrTouchEvent: Event | Touch,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
let isPressWithinResponderRegion = true;
if (
state.pressTarget != null &&
!context.isTargetWithinElement(target, state.pressTarget)
) {
// Calculate the responder region we use for deactivation if not
// already done during move event.
if (state.responderRegionOnDeactivation == null) {
state.responderRegionOnDeactivation = calculateResponderRegion(
context,
state.pressTarget,
props,
);
}
const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
let left, top, right, bottom;
if (responderRegionOnActivation != null) {
left = responderRegionOnActivation.left;
top = responderRegionOnActivation.top;
right = responderRegionOnActivation.right;
bottom = responderRegionOnActivation.bottom;
if (responderRegionOnDeactivation != null) {
left = Math.min(left, responderRegionOnDeactivation.left);
top = Math.min(top, responderRegionOnDeactivation.top);
right = Math.max(right, responderRegionOnDeactivation.right);
bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
}
}
const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any);
isPressWithinResponderRegion =
left != null &&
right != null &&
top != null &&
bottom != null &&
x !== null &&
y !== null &&
(x >= left && x <= right && y >= top && y <= bottom);
// Calculate the responder region we use for deactivation if not
// already done during move event.
if (state.responderRegionOnDeactivation == null) {
state.responderRegionOnDeactivation = calculateResponderRegion(
context,
((state.pressTarget: any): Element),
props,
);
}
state.isPressWithinResponderRegion = isPressWithinResponderRegion;
const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
let left, top, right, bottom;
if (responderRegionOnActivation != null) {
left = responderRegionOnActivation.left;
top = responderRegionOnActivation.top;
right = responderRegionOnActivation.right;
bottom = responderRegionOnActivation.bottom;
if (responderRegionOnDeactivation != null) {
left = Math.min(left, responderRegionOnDeactivation.left);
top = Math.min(top, responderRegionOnDeactivation.top);
right = Math.max(right, responderRegionOnDeactivation.right);
bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
}
}
const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any);
state.isPressWithinResponderRegion =
left != null &&
right != null &&
top != null &&
bottom != null &&
x !== null &&
y !== null &&
(x >= left && x <= right && y >= top && y <= bottom);
}
function handleStopPropagation(
@@ -650,7 +622,6 @@ const PressResponder: ReactDOMEventResponder = {
return {
activationPosition: null,
addedRootEvents: false,
didDispatchEvent: false,
isActivePressed: false,
isActivePressStart: false,
isLongPressed: false,
@@ -666,6 +637,7 @@ const PressResponder: ReactDOMEventResponder = {
ignoreEmulatedMouseEvents: false,
activePointerId: null,
shouldPreventClick: false,
touchEvent: null,
};
},
allowMultipleHostChildren: false,
@@ -731,6 +703,10 @@ const PressResponder: ReactDOMEventResponder = {
state.activePointerId = pointerId;
} else if (isTouchEvent) {
const touchEvent = getTouchFromPressEvent(nativeEvent);
if (touchEvent === null) {
return;
}
state.touchEvent = touchEvent;
state.activePointerId = touchEvent.identifier;
}
@@ -852,18 +828,23 @@ const PressResponder: ReactDOMEventResponder = {
if (touchEvent === null) {
return;
}
target = getTouchTarget(context, touchEvent);
state.touchEvent = touchEvent;
}
// Calculate the responder region we use for deactivation, as the
// element dimensions may have changed since activation.
updateIsPressWithinResponderRegion(
target,
touchEvent || nativeEvent,
context,
props,
state,
);
if (
state.pressTarget !== null &&
(pointerType !== 'mouse' ||
!context.isTargetWithinElement(target, state.pressTarget))
) {
// Calculate the responder region we use for deactivation, as the
// element dimensions may have changed since activation.
updateIsPressWithinResponderRegion(
touchEvent || nativeEvent,
context,
props,
state,
);
}
if (state.isPressWithinResponderRegion) {
if (isPressed) {
@@ -914,6 +895,7 @@ const PressResponder: ReactDOMEventResponder = {
if (touchEvent === null) {
return;
}
state.touchEvent = touchEvent;
target = getTouchTarget(context, touchEvent);
} else if (type === 'keyup') {
// Ignore unrelated keyboard events
@@ -961,12 +943,16 @@ const PressResponder: ReactDOMEventResponder = {
dispatchPressEndEvents(event, context, props, state);
if (state.pressTarget !== null && props.onPress) {
if (!isKeyboardEvent) {
if (
!isKeyboardEvent &&
state.pressTarget !== null &&
(pointerType !== 'mouse' ||
!context.isTargetWithinElement(target, state.pressTarget))
) {
// If the event target isn't within the press target, check if we're still
// within the responder region. The region may have changed if the
// element's layout was modified after activation.
updateIsPressWithinResponderRegion(
target,
touchEvent || nativeEvent,
context,
props,
@@ -992,6 +978,7 @@ const PressResponder: ReactDOMEventResponder = {
}
}
}
state.touchEvent = null;
} else if (type === 'mouseup') {
state.ignoreEmulatedMouseEvents = false;
}
@@ -36,6 +36,12 @@ function createTouchEvent(type, id, data) {
identifier: id,
},
],
targetTouches: [
{
...data,
identifier: id,
},
],
});
}
@@ -618,14 +624,24 @@ describe('Event responder: Press', () => {
ref.current.dispatchEvent(
createTouchEvent('touchstart', 0, {
target: ref.current,
clientX: 0,
clientY: 0,
}),
);
ref.current.dispatchEvent(
createTouchEvent('touchend', 0, {
target: ref.current,
clientX: 0,
clientY: 0,
}),
);
ref.current.dispatchEvent(
createEvent('pointerup', {
pointerType: 'pen',
clientX: 0,
clientY: 0,
}),
);
ref.current.dispatchEvent(createEvent('pointerup', {pointerType: 'pen'}));
expect(onPress).toHaveBeenCalledTimes(1);
expect(onPress).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'pen', type: 'press'}),
@@ -2022,6 +2038,7 @@ describe('Event responder: Press', () => {
document.elementFromPoint = () => ref.current;
ref.current.dispatchEvent(
createTouchEvent('touchend', 0, {
...coordinatesInside,
target: ref.current,
}),
);
@@ -2269,7 +2286,12 @@ describe('Event responder: Press', () => {
ReactDOM.render(element, container);
ref.current.dispatchEvent(createEvent('pointerdown'));
ref.current.dispatchEvent(createEvent('pointerup'));
ref.current.dispatchEvent(
createEvent('pointerup', {
clientX: 0,
clientY: 0,
}),
);
ref.current.dispatchEvent(createEvent('click', {preventDefault}));
expect(preventDefault).toBeCalled();
expect(onPress).toHaveBeenCalledWith(
@@ -2291,7 +2313,12 @@ describe('Event responder: Press', () => {
ReactDOM.render(element, container);
buttonRef.current.dispatchEvent(createEvent('pointerdown'));
buttonRef.current.dispatchEvent(createEvent('pointerup'));
buttonRef.current.dispatchEvent(
createEvent('pointerup', {
clientX: 0,
clientY: 0,
}),
);
buttonRef.current.dispatchEvent(createEvent('click', {preventDefault}));
expect(preventDefault).toBeCalled();
});
@@ -2310,7 +2337,12 @@ describe('Event responder: Press', () => {
ReactDOM.render(element, container);
ref.current.dispatchEvent(createEvent('pointerdown'));
ref.current.dispatchEvent(createEvent('pointerup'));
ref.current.dispatchEvent(
createEvent('pointerup', {
clientX: 0,
clientY: 0,
}),
);
ref.current.dispatchEvent(createEvent('click', {preventDefault}));
expect(preventDefault).toBeCalled();
expect(onPress).toHaveBeenCalledWith(
@@ -2334,7 +2366,11 @@ describe('Event responder: Press', () => {
createEvent('pointerdown', {[modifierKey]: true}),
);
ref.current.dispatchEvent(
createEvent('pointerup', {[modifierKey]: true}),
createEvent('pointerup', {
[modifierKey]: true,
clientX: 0,
clientY: 0,
}),
);
ref.current.dispatchEvent(
createEvent('click', {[modifierKey]: true, preventDefault}),
@@ -2358,7 +2394,12 @@ describe('Event responder: Press', () => {
ReactDOM.render(element, container);
ref.current.dispatchEvent(createEvent('pointerdown'));
ref.current.dispatchEvent(createEvent('pointerup'));
ref.current.dispatchEvent(
createEvent('pointerup', {
clientX: 0,
clientY: 0,
}),
);
ref.current.dispatchEvent(createEvent('click', {preventDefault}));
expect(preventDefault).not.toBeCalled();
expect(onPress).toHaveBeenCalledWith(
@@ -2694,7 +2735,10 @@ describe('Event responder: Press', () => {
});
function dispatchEventWithTimeStamp(elem, name, timeStamp) {
const event = createEvent(name);
const event = createEvent(name, {
clientX: 0,
clientY: 0,
});
Object.defineProperty(event, 'timeStamp', {
value: timeStamp,
});