From 997b598de88e86e9bd58a3041c09a03677df754f Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Thu, 14 Sep 2017 17:05:18 -0700 Subject: [PATCH] [fix] event normalization Work better with simulated events, and avoid crashing if 'nativeEvent.target' isn't an element node. Close #597 --- .../__snapshots__/index-test.js.snap | 3 + .../createElement/__tests__/index-test.js | 12 +- .../__snapshots__/index-test.js.snap | 124 ++++++++++++++++++ .../__tests__/index-test.js | 82 ++++++++++++ src/modules/normalizeNativeEvent/index.js | 80 ++++++++--- 5 files changed, 275 insertions(+), 26 deletions(-) create mode 100644 src/modules/normalizeNativeEvent/__tests__/__snapshots__/index-test.js.snap create mode 100644 src/modules/normalizeNativeEvent/__tests__/index-test.js diff --git a/src/modules/createElement/__tests__/__snapshots__/index-test.js.snap b/src/modules/createElement/__tests__/__snapshots__/index-test.js.snap index 5f7a4df7..71a1ea2f 100644 --- a/src/modules/createElement/__tests__/__snapshots__/index-test.js.snap +++ b/src/modules/createElement/__tests__/__snapshots__/index-test.js.snap @@ -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], diff --git a/src/modules/createElement/__tests__/index-test.js b/src/modules/createElement/__tests__/index-test.js index 671dcb12..c42bfde8 100644 --- a/src/modules/createElement/__tests__/index-test.js +++ b/src/modules/createElement/__tests__/index-test.js @@ -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 }); diff --git a/src/modules/normalizeNativeEvent/__tests__/__snapshots__/index-test.js.snap b/src/modules/normalizeNativeEvent/__tests__/__snapshots__/index-test.js.snap new file mode 100644 index 00000000..3d9b5744 --- /dev/null +++ b/src/modules/normalizeNativeEvent/__tests__/__snapshots__/index-test.js.snap @@ -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 [], +} +`; diff --git a/src/modules/normalizeNativeEvent/__tests__/index-test.js b/src/modules/normalizeNativeEvent/__tests__/index-test.js new file mode 100644 index 00000000..de07ff87 --- /dev/null +++ b/src/modules/normalizeNativeEvent/__tests__/index-test.js @@ -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(); + }); + }); +}); diff --git a/src/modules/normalizeNativeEvent/index.js b/src/modules/normalizeNativeEvent/index.js index 4ffda873..0af0a142 100644 --- a/src/modules/normalizeNativeEvent/index.js +++ b/src/modules/normalizeNativeEvent/index.js @@ -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;