mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Flare] Fix issues with touch + pointer interactions (#15997)
This commit is contained in:
+75
-88
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user