[fix] event normalization

Work better with simulated events, and avoid crashing if
'nativeEvent.target' isn't an element node.

Close #597
This commit is contained in:
Nicolas Gallagher
2017-09-14 17:05:18 -07:00
parent 6fe796f9da
commit 997b598de8
5 changed files with 275 additions and 26 deletions
@@ -4,6 +4,9 @@ exports[`modules/createElement it normalizes event.nativeEvent 1`] = `
Object {
"_normalized": true,
"changedTouches": Array [],
"identifier": undefined,
"locationX": undefined,
"locationY": undefined,
"pageX": undefined,
"pageY": undefined,
"preventDefault": [Function],
@@ -19,11 +19,7 @@ describe('modules/createElement', () => {
};
const component = shallow(createElement('span', { onClick }));
component.find('span').simulate('click', {
nativeEvent: {
preventDefault() {},
stopImmediatePropagation() {},
stopPropagation() {}
}
nativeEvent: {}
});
});
@@ -38,11 +34,7 @@ describe('modules/createElement', () => {
);
component.find('span').simulate('keyPress', {
isDefaultPrevented() {},
nativeEvent: {
preventDefault() {},
stopImmediatePropagation() {},
stopPropagation() {}
},
nativeEvent: {},
preventDefault() {},
which
});
@@ -0,0 +1,124 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`modules/normalizeNativeEvent mouse events simulated event 1`] = `
Object {
"_normalized": true,
"changedTouches": Array [
Object {
"_normalized": true,
"clientX": undefined,
"clientY": undefined,
"force": undefined,
"identifier": 0,
"locationX": undefined,
"locationY": undefined,
"pageX": undefined,
"pageY": undefined,
"screenX": undefined,
"screenY": undefined,
"target": undefined,
"timestamp": 1496876171255,
},
],
"identifier": 0,
"locationX": undefined,
"locationY": undefined,
"pageX": undefined,
"pageY": undefined,
"preventDefault": [Function],
"stopImmediatePropagation": [Function],
"stopPropagation": [Function],
"target": undefined,
"timestamp": 1496876171255,
"touches": Array [],
}
`;
exports[`modules/normalizeNativeEvent mouse events synthetic event 1`] = `
Object {
"_normalized": true,
"changedTouches": Array [
Object {
"_normalized": true,
"clientX": 100,
"clientY": 100,
"force": false,
"identifier": 0,
"locationX": 100,
"locationY": 100,
"pageX": 300,
"pageY": 300,
"screenX": 400,
"screenY": 400,
"target": undefined,
"timestamp": 1496876171255,
},
],
"identifier": 0,
"locationX": 200,
"locationY": 200,
"pageX": 300,
"pageY": 300,
"preventDefault": [Function],
"stopImmediatePropagation": [Function],
"stopPropagation": [Function],
"target": undefined,
"timestamp": 1496876171255,
"touches": Array [],
}
`;
exports[`modules/normalizeNativeEvent touch events simulated event 1`] = `
Object {
"_normalized": true,
"changedTouches": Array [],
"identifier": undefined,
"locationX": undefined,
"locationY": undefined,
"pageX": undefined,
"pageY": undefined,
"preventDefault": [Function],
"stopImmediatePropagation": [Function],
"stopPropagation": [Function],
"target": undefined,
"timestamp": 1496876171255,
"touches": Array [],
}
`;
exports[`modules/normalizeNativeEvent touch events synthetic event 1`] = `
Object {
"_normalized": true,
"changedTouches": Array [
Object {
"_normalized": true,
"clientX": 100,
"clientY": 100,
"force": false,
"identifier": undefined,
"locationX": undefined,
"locationY": undefined,
"pageX": 300,
"pageY": 300,
"radiusX": 10,
"radiusY": 10,
"rotationAngle": 45,
"screenX": 400,
"screenY": 400,
"target": undefined,
"timestamp": 1496876171255,
},
],
"identifier": undefined,
"locationX": undefined,
"locationY": undefined,
"pageX": 300,
"pageY": 300,
"preventDefault": [Function],
"stopImmediatePropagation": [Function],
"stopPropagation": [Function],
"target": undefined,
"timestamp": 1496876171255,
"touches": Array [],
}
`;
@@ -0,0 +1,82 @@
/* eslint-env jasmine, jest */
import normalizeNativeEvent from '..';
const normalizeEvent = (nativeEvent) => {
const result = normalizeNativeEvent(nativeEvent);
result.timestamp = 1496876171255;
if (result.changedTouches && result.changedTouches[0]) {
result.changedTouches[0].timestamp = 1496876171255;
}
if (result.touches && result.touches[0]) {
result.touches[0].timestamp = 1496876171255;
}
return result;
}
describe('modules/normalizeNativeEvent', () => {
describe('mouse events', () => {
test('simulated event', () => {
const nativeEvent = {
type: 'mouseup'
};
const result = normalizeEvent(nativeEvent);
expect(result).toMatchSnapshot();
});
test('synthetic event', () => {
const nativeEvent = {
type: 'mouseup',
clientX: 100,
clientY: 100,
force: false,
offsetX: 200,
offsetY: 200,
pageX: 300,
pageY: 300,
screenX: 400,
screenY: 400
};
const result = normalizeEvent(nativeEvent);
expect(result).toMatchSnapshot();
});
});
describe('touch events', () => {
test('simulated event', () => {
const nativeEvent = {
type: 'touchstart'
};
const result = normalizeEvent(nativeEvent);
expect(result).toMatchSnapshot();
});
test('synthetic event', () => {
const nativeEvent = {
type: 'touchstart',
changedTouches: [
{
clientX: 100,
clientY: 100,
force: false,
pageX: 300,
pageY: 300,
radiusX: 10,
radiusY: 10,
rotationAngle: 45,
screenX: 400,
screenY: 400
}
],
pageX: 300,
pageY: 300
};
const result = normalizeEvent(nativeEvent);
expect(result).toMatchSnapshot();
});
});
});
+64 -16
View File
@@ -5,19 +5,31 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*
* @noflow
* @flow
*/
const emptyArray = [];
const emptyFunction = () => {};
// Mobile Safari re-uses touch objects, so we copy the properties we want and normalize the identifier
const normalizeTouches = (touches = emptyArray) =>
Array.prototype.slice.call(touches).map(touch => {
const identifier = touch.identifier > 20 ? touch.identifier % 20 : touch.identifier;
const normalizeTouches = touches => {
if (!touches) {
return emptyArray;
}
const rect = touch.target && touch.target.getBoundingClientRect();
const locationX = touch.pageX - rect.left;
const locationY = touch.pageY - rect.top;
return Array.prototype.slice.call(touches).map(touch => {
const identifier = touch.identifier > 20 ? touch.identifier % 20 : touch.identifier;
let locationX, locationY;
const node = touch.target;
if (node) {
const isElement = node.nodeType === 1 /* Node.ELEMENT_NODE */;
if (isElement && typeof node.getBoundingClientRect === 'function') {
const rect = node.getBoundingClientRect();
locationX = touch.pageX - rect.left;
locationY = touch.pageY - rect.top;
}
}
return {
_normalized: true,
@@ -40,19 +52,36 @@ const normalizeTouches = (touches = emptyArray) =>
timestamp: Date.now()
};
});
};
function normalizeTouchEvent(nativeEvent) {
const changedTouches = normalizeTouches(nativeEvent.changedTouches);
const touches = normalizeTouches(nativeEvent.touches);
const preventDefault =
typeof nativeEvent.preventDefault === 'function'
? nativeEvent.preventDefault.bind(nativeEvent)
: emptyFunction;
const stopImmediatePropagation =
typeof nativeEvent.stopImmediatePropagation === 'function'
? nativeEvent.stopImmediatePropagation.bind(nativeEvent)
: emptyFunction;
const stopPropagation =
typeof nativeEvent.stopPropagation === 'function'
? nativeEvent.stopPropagation.bind(nativeEvent)
: emptyFunction;
const event = {
_normalized: true,
changedTouches,
identifier: undefined,
locationX: undefined,
locationY: undefined,
pageX: nativeEvent.pageX,
pageY: nativeEvent.pageY,
preventDefault: nativeEvent.preventDefault.bind(nativeEvent),
stopImmediatePropagation: nativeEvent.stopImmediatePropagation.bind(nativeEvent),
stopPropagation: nativeEvent.stopPropagation.bind(nativeEvent),
preventDefault,
stopImmediatePropagation,
stopPropagation,
target: nativeEvent.target,
// normalize the timestamp
// https://stackoverflow.com/questions/26177087/ios-8-mobile-safari-wrong-timestamp-on-touch-events
@@ -89,6 +118,20 @@ function normalizeMouseEvent(nativeEvent) {
timestamp: Date.now()
}
];
const preventDefault =
typeof nativeEvent.preventDefault === 'function'
? nativeEvent.preventDefault.bind(nativeEvent)
: emptyFunction;
const stopImmediatePropagation =
typeof nativeEvent.stopImmediatePropagation === 'function'
? nativeEvent.stopImmediatePropagation.bind(nativeEvent)
: emptyFunction;
const stopPropagation =
typeof nativeEvent.stopPropagation === 'function'
? nativeEvent.stopPropagation.bind(nativeEvent)
: emptyFunction;
return {
_normalized: true,
changedTouches: touches,
@@ -97,22 +140,27 @@ function normalizeMouseEvent(nativeEvent) {
locationY: nativeEvent.offsetY,
pageX: nativeEvent.pageX,
pageY: nativeEvent.pageY,
preventDefault: nativeEvent.preventDefault.bind(nativeEvent),
stopImmediatePropagation: nativeEvent.stopImmediatePropagation.bind(nativeEvent),
stopPropagation: nativeEvent.stopPropagation.bind(nativeEvent),
preventDefault,
stopImmediatePropagation,
stopPropagation,
target: nativeEvent.target,
timestamp: touches[0].timestamp,
touches: nativeEvent.type === 'mouseup' ? emptyArray : touches
};
}
function normalizeNativeEvent(nativeEvent) {
if (nativeEvent._normalized) {
// TODO: how to best handle keyboard events?
function normalizeNativeEvent(nativeEvent: Object) {
if (!nativeEvent || nativeEvent._normalized) {
return nativeEvent;
}
const eventType = nativeEvent.type || '';
const mouse = eventType.indexOf('mouse') >= 0;
return mouse ? normalizeMouseEvent(nativeEvent) : normalizeTouchEvent(nativeEvent);
if (mouse) {
return normalizeMouseEvent(nativeEvent);
} else {
return normalizeTouchEvent(nativeEvent);
}
}
export default normalizeNativeEvent;