From 1655f4fea8384a4dac97b007b1f654965ef4a4de Mon Sep 17 00:00:00 2001 From: Nathan Hunzaker Date: Mon, 28 May 2018 10:37:43 -0400 Subject: [PATCH 1/3] Always use event capturing. Remove media events. This commit changes event subscription such that all events are attached with capturing. This prevents the need to eagerly attach media event listeners to video, audio, and source tags, and simplifies the event subscription process. I think we can probably extend this to more event types; requiring much more testing. Until that time, this commit lays the foundation for that work by demonstrating that capturing is applicable in cases where bubbling does not occur. --- .../src/__tests__/ReactDOMComponent-test.js | 19 +-- .../__tests__/ReactDOMEventListener-test.js | 116 ++++++++++-------- .../src/client/ReactDOMFiberComponent.js | 60 +++------ .../src/events/DOMTopLevelEventTypes.js | 29 ----- .../react-dom/src/events/EventListener.js | 8 -- .../src/events/ReactBrowserEventEmitter.js | 28 ++--- .../src/events/ReactDOMEventListener.js | 32 +---- 7 files changed, 105 insertions(+), 187 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 441545fc0a154..cf955d4d6d142 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -966,26 +966,29 @@ describe('ReactDOMComponent', () => { }); it('should work error event on element', () => { - spyOnDevAndProd(console, 'log'); + const onError = jest.fn(); const container = document.createElement('div'); ReactDOM.render( , container, ); - const errorEvent = document.createEvent('Event'); - errorEvent.initEvent('error', false, false); - container.getElementsByTagName('source')[0].dispatchEvent(errorEvent); + try { + document.body.appendChild(container); - if (__DEV__) { - expect(console.log).toHaveBeenCalledTimes(1); - expect(console.log.calls.argsFor(0)[0]).toContain('onError called'); + const errorEvent = document.createEvent('Event'); + errorEvent.initEvent('error', false, false); + container.getElementsByTagName('source')[0].dispatchEvent(errorEvent); + + expect(onError).toHaveBeenCalledTimes(1); + } finally { + container.remove(); } }); diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js index f34688661c391..9cc52a9af8c97 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js @@ -271,49 +271,7 @@ describe('ReactDOMEventListener', () => { document.body.removeChild(container); }); - it('should dispatch loadstart only for media elements', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - - const imgRef = React.createRef(); - const videoRef = React.createRef(); - - const handleImgLoadStart = jest.fn(); - const handleVideoLoadStart = jest.fn(); - ReactDOM.render( -
- -
, - container, - ); - - // Note for debugging: loadstart currently doesn't fire in Chrome. - // https://bugs.chromium.org/p/chromium/issues/detail?id=458851 - imgRef.current.dispatchEvent( - new ProgressEvent('loadstart', { - bubbles: false, - }), - ); - // Historically, we happened to not support onLoadStart - // on , and this test documents that lack of support. - // If we decide to support it in the future, we should change - // this line to expect 1 call. Note that fixing this would - // be simple but would require attaching a handler to each - // . So far nobody asked us for it. - expect(handleImgLoadStart).toHaveBeenCalledTimes(0); - - videoRef.current.dispatchEvent( - new ProgressEvent('loadstart', { - bubbles: false, - }), - ); - expect(handleVideoLoadStart).toHaveBeenCalledTimes(1); - - document.body.removeChild(container); - }); - - it('should not attempt to listen to unnecessary events on the top level', () => { + it('should dispatch media events', () => { const container = document.createElement('div'); document.body.appendChild(container); @@ -345,13 +303,6 @@ describe('ReactDOMEventListener', () => { onWaiting() {}, }; - const originalAddEventListener = document.addEventListener; - document.addEventListener = function(type) { - throw new Error( - `Did not expect to add a top-level listener for the "${type}" event.`, - ); - }; - try { // We expect that mounting this tree will // *not* attach handlers for any top-level events. @@ -374,8 +325,71 @@ describe('ReactDOMEventListener', () => { ); expect(handleVideoPlay).toHaveBeenCalledTimes(1); } finally { - document.addEventListener = originalAddEventListener; document.body.removeChild(container); } }); + + it('does not double dispatch scroll events at the deepest leaf', () => { + const top = jest.fn(); + const middle = jest.fn(); + const bottom = jest.fn(); + const container = document.createElement('div'); + + ReactDOM.render( +
+
+
+
+
, + container, + ); + + const target = container.querySelector('#bottom'); + const event = document.createEvent('Event'); + + try { + document.body.appendChild(container); + + event.initEvent('scroll', true, true); + target.dispatchEvent(event); + + expect(top).toHaveBeenCalledTimes(1); + expect(middle).toHaveBeenCalledTimes(1); + expect(bottom).toHaveBeenCalledTimes(1); + } finally { + container.remove(); + } + }); + + it('does not double dispatch scroll events at the middle leaf', () => { + const top = jest.fn(); + const middle = jest.fn(); + const bottom = jest.fn(); + const container = document.createElement('div'); + + ReactDOM.render( +
+
+
+
+
, + container, + ); + + const target = container.querySelector('#middle'); + const event = document.createEvent('Event'); + + try { + document.body.appendChild(container); + + event.initEvent('scroll', true, true); + target.dispatchEvent(event); + + expect(top).toHaveBeenCalledTimes(1); + expect(middle).toHaveBeenCalledTimes(1); + expect(bottom).toHaveBeenCalledTimes(0); + } finally { + container.remove(); + } + }); }); diff --git a/packages/react-dom/src/client/ReactDOMFiberComponent.js b/packages/react-dom/src/client/ReactDOMFiberComponent.js index e089b33098520..536fab6627d7b 100644 --- a/packages/react-dom/src/client/ReactDOMFiberComponent.js +++ b/packages/react-dom/src/client/ReactDOMFiberComponent.js @@ -29,8 +29,7 @@ import { TOP_SUBMIT, TOP_TOGGLE, } from '../events/DOMTopLevelEventTypes'; -import {listenTo, trapBubbledEvent} from '../events/ReactBrowserEventEmitter'; -import {mediaEventTypes} from '../events/DOMTopLevelEventTypes'; +import {listenTo, trapEvent} from '../events/ReactBrowserEventEmitter'; import * as CSSPropertyOperations from '../shared/CSSPropertyOperations'; import {Namespaces, getIntrinsicNamespace} from '../shared/DOMNamespaces'; import { @@ -452,41 +451,29 @@ export function setInitialProperties( switch (tag) { case 'iframe': case 'object': - trapBubbledEvent(TOP_LOAD, domElement); - props = rawProps; - break; - case 'video': - case 'audio': - // Create listener for each media event - for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement); - } - props = rawProps; - break; - case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + trapEvent(TOP_LOAD, domElement); props = rawProps; break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + trapEvent(TOP_ERROR, domElement); + trapEvent(TOP_LOAD, domElement); props = rawProps; break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + trapEvent(TOP_RESET, domElement); + trapEvent(TOP_SUBMIT, domElement); props = rawProps; break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + trapEvent(TOP_TOGGLE, domElement); props = rawProps; break; case 'input': ReactDOMFiberInput.initWrapperState(domElement, rawProps); props = ReactDOMFiberInput.getHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -498,7 +485,7 @@ export function setInitialProperties( case 'select': ReactDOMFiberSelect.initWrapperState(domElement, rawProps); props = ReactDOMFiberSelect.getHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -506,7 +493,7 @@ export function setInitialProperties( case 'textarea': ReactDOMFiberTextarea.initWrapperState(domElement, rawProps); props = ReactDOMFiberTextarea.getHostProps(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -843,34 +830,27 @@ export function diffHydratedProperties( switch (tag) { case 'iframe': case 'object': - trapBubbledEvent(TOP_LOAD, domElement); - break; - case 'video': - case 'audio': - // Create listener for each media event - for (let i = 0; i < mediaEventTypes.length; i++) { - trapBubbledEvent(mediaEventTypes[i], domElement); - } + trapEvent(TOP_LOAD, domElement); break; case 'source': - trapBubbledEvent(TOP_ERROR, domElement); + trapEvent(TOP_ERROR, domElement); break; case 'img': case 'image': case 'link': - trapBubbledEvent(TOP_ERROR, domElement); - trapBubbledEvent(TOP_LOAD, domElement); + trapEvent(TOP_ERROR, domElement); + trapEvent(TOP_LOAD, domElement); break; case 'form': - trapBubbledEvent(TOP_RESET, domElement); - trapBubbledEvent(TOP_SUBMIT, domElement); + trapEvent(TOP_RESET, domElement); + trapEvent(TOP_SUBMIT, domElement); break; case 'details': - trapBubbledEvent(TOP_TOGGLE, domElement); + trapEvent(TOP_TOGGLE, domElement); break; case 'input': ReactDOMFiberInput.initWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); @@ -880,14 +860,14 @@ export function diffHydratedProperties( break; case 'select': ReactDOMFiberSelect.initWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); break; case 'textarea': ReactDOMFiberTextarea.initWrapperState(domElement, rawProps); - trapBubbledEvent(TOP_INVALID, domElement); + trapEvent(TOP_INVALID, domElement); // For controlled components we always need to ensure we're listening // to onChange. Even if there is no listener. ensureListeningTo(rootContainerElement, 'onChange'); diff --git a/packages/react-dom/src/events/DOMTopLevelEventTypes.js b/packages/react-dom/src/events/DOMTopLevelEventTypes.js index dc526a0ef00b7..1ee1bb8e75da0 100644 --- a/packages/react-dom/src/events/DOMTopLevelEventTypes.js +++ b/packages/react-dom/src/events/DOMTopLevelEventTypes.js @@ -148,35 +148,6 @@ export const TOP_VOLUME_CHANGE = unsafeCastStringToDOMTopLevelType( export const TOP_WAITING = unsafeCastStringToDOMTopLevelType('waiting'); export const TOP_WHEEL = unsafeCastStringToDOMTopLevelType('wheel'); -// List of events that need to be individually attached to media elements. -// Note that events in this list will *not* be listened to at the top level -// unless they're explicitly whitelisted in `ReactBrowserEventEmitter.listenTo`. -export const mediaEventTypes = [ - TOP_ABORT, - TOP_CAN_PLAY, - TOP_CAN_PLAY_THROUGH, - TOP_DURATION_CHANGE, - TOP_EMPTIED, - TOP_ENCRYPTED, - TOP_ENDED, - TOP_ERROR, - TOP_LOADED_DATA, - TOP_LOADED_METADATA, - TOP_LOAD_START, - TOP_PAUSE, - TOP_PLAY, - TOP_PLAYING, - TOP_PROGRESS, - TOP_RATE_CHANGE, - TOP_SEEKED, - TOP_SEEKING, - TOP_STALLED, - TOP_SUSPEND, - TOP_TIME_UPDATE, - TOP_VOLUME_CHANGE, - TOP_WAITING, -]; - export function getRawEventName(topLevelType: DOMTopLevelEventType): string { return unsafeCastDOMTopLevelTypeToString(topLevelType); } diff --git a/packages/react-dom/src/events/EventListener.js b/packages/react-dom/src/events/EventListener.js index 8bec245b13fb7..ee640d37c9999 100644 --- a/packages/react-dom/src/events/EventListener.js +++ b/packages/react-dom/src/events/EventListener.js @@ -7,14 +7,6 @@ * @flow */ -export function addEventBubbleListener( - element: Document | Element, - eventType: string, - listener: Function, -): void { - element.addEventListener(eventType, listener, false); -} - export function addEventCaptureListener( element: Document | Element, eventType: string, diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js index 3dd2e452f569b..f5f7d84d96875 100644 --- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js +++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js @@ -15,17 +15,10 @@ import { TOP_FOCUS, TOP_INVALID, TOP_RESET, - TOP_SCROLL, TOP_SUBMIT, getRawEventName, - mediaEventTypes, } from './DOMTopLevelEventTypes'; -import { - setEnabled, - isEnabled, - trapBubbledEvent, - trapCapturedEvent, -} from './ReactDOMEventListener'; +import {setEnabled, isEnabled, trapEvent} from './ReactDOMEventListener'; import isEventSupported from './isEventSupported'; /** @@ -134,13 +127,10 @@ export function listenTo( const dependency = dependencies[i]; if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { switch (dependency) { - case TOP_SCROLL: - trapCapturedEvent(TOP_SCROLL, mountAt); - break; case TOP_FOCUS: case TOP_BLUR: - trapCapturedEvent(TOP_FOCUS, mountAt); - trapCapturedEvent(TOP_BLUR, mountAt); + trapEvent(TOP_FOCUS, mountAt); + trapEvent(TOP_BLUR, mountAt); // We set the flag for a single dependency later in this function, // but this ensures we mark both as attached rather than just one. isListening[TOP_BLUR] = true; @@ -149,7 +139,7 @@ export function listenTo( case TOP_CANCEL: case TOP_CLOSE: if (isEventSupported(getRawEventName(dependency), true)) { - trapCapturedEvent(dependency, mountAt); + trapEvent(dependency, mountAt); } break; case TOP_INVALID: @@ -159,12 +149,8 @@ export function listenTo( // Some of them bubble so we don't want them to fire twice. break; default: - // By default, listen on the top level to all non-media events. - // Media events don't bubble so adding the listener wouldn't do anything. - const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; - if (!isMediaEvent) { - trapBubbledEvent(dependency, mountAt); - } + // By default, listen on the top level to all events. + trapEvent(dependency, mountAt); break; } isListening[dependency] = true; @@ -187,4 +173,4 @@ export function isListeningToAllDependencies( return true; } -export {setEnabled, isEnabled, trapBubbledEvent, trapCapturedEvent}; +export {setEnabled, isEnabled, trapEvent}; diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 43c5cb4e9fb19..bc88d821e6232 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -16,7 +16,7 @@ import {runExtractedEventsInBatch} from 'events/EventPluginHub'; import {isFiberMounted} from 'react-reconciler/reflection'; import {HostRoot} from 'shared/ReactTypeOfWork'; -import {addEventBubbleListener, addEventCaptureListener} from './EventListener'; +import {addEventCaptureListener} from './EventListener'; import getEventTarget from './getEventTarget'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; import SimpleEventPlugin from './SimpleEventPlugin'; @@ -125,34 +125,6 @@ export function isEnabled() { return _enabled; } -/** - * Traps top-level events by using event bubbling. - * - * @param {number} topLevelType Number from `TopLevelEventTypes`. - * @param {object} element Element on which to attach listener. - * @return {?object} An object with a remove function which will forcefully - * remove the listener. - * @internal - */ -export function trapBubbledEvent( - topLevelType: DOMTopLevelEventType, - element: Document | Element, -) { - if (!element) { - return null; - } - const dispatch = isInteractiveTopLevelEventType(topLevelType) - ? dispatchInteractiveEvent - : dispatchEvent; - - addEventBubbleListener( - element, - getRawEventName(topLevelType), - // Check if interactive and wrap in interactiveUpdates - dispatch.bind(null, topLevelType), - ); -} - /** * Traps a top-level event by using event capturing. * @@ -162,7 +134,7 @@ export function trapBubbledEvent( * remove the listener. * @internal */ -export function trapCapturedEvent( +export function trapEvent( topLevelType: DOMTopLevelEventType, element: Document | Element, ) { From 3b74469598a38aa70dc20d8872f006455747a8fd Mon Sep 17 00:00:00 2001 From: Nathan Hunzaker Date: Mon, 28 May 2018 11:16:55 -0400 Subject: [PATCH 2/3] Remove bubbled listener from EventListener-www.js --- .../src/events/forks/EventListener-www.js | 8 - scripts/rollup/results.json | 144 +++++++++--------- 2 files changed, 72 insertions(+), 80 deletions(-) diff --git a/packages/react-dom/src/events/forks/EventListener-www.js b/packages/react-dom/src/events/forks/EventListener-www.js index 233c2ba56c5da..55a909097d15b 100644 --- a/packages/react-dom/src/events/forks/EventListener-www.js +++ b/packages/react-dom/src/events/forks/EventListener-www.js @@ -12,14 +12,6 @@ const EventListenerWWW = require('EventListener'); import typeof * as EventListenerType from '../EventListener'; import typeof * as EventListenerShimType from './EventListener-www'; -export function addEventBubbleListener( - element: Element, - eventType: string, - listener: Function, -): void { - EventListenerWWW.listen(element, eventType, listener); -} - export function addEventCaptureListener( element: Element, eventType: string, diff --git a/scripts/rollup/results.json b/scripts/rollup/results.json index b04285dbe9ab0..f308714bba2c5 100644 --- a/scripts/rollup/results.json +++ b/scripts/rollup/results.json @@ -46,29 +46,29 @@ "filename": "react-dom.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 636515, - "gzip": 148265 + "size": 636456, + "gzip": 148191 }, { "filename": "react-dom.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-dom", - "size": 95960, - "gzip": 31067 + "size": 95547, + "gzip": 30941 }, { "filename": "react-dom.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 620506, - "gzip": 144162 + "size": 620447, + "gzip": 144122 }, { "filename": "react-dom.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-dom", - "size": 94422, - "gzip": 30018 + "size": 94013, + "gzip": 29882 }, { "filename": "ReactDOM-dev.js", @@ -88,8 +88,8 @@ "filename": "react-dom-test-utils.development.js", "bundleType": "UMD_DEV", "packageName": "react-dom", - "size": 46355, - "gzip": 12768 + "size": 46122, + "gzip": 12662 }, { "filename": "react-dom-test-utils.production.min.js", @@ -102,8 +102,8 @@ "filename": "react-dom-test-utils.development.js", "bundleType": "NODE_DEV", "packageName": "react-dom", - "size": 41092, - "gzip": 11309 + "size": 40859, + "gzip": 11203 }, { "filename": "react-dom-test-utils.production.min.js", @@ -221,29 +221,29 @@ "filename": "react-art.development.js", "bundleType": "UMD_DEV", "packageName": "react-art", - "size": 413567, - "gzip": 92395 + "size": 416004, + "gzip": 92806 }, { "filename": "react-art.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-art", - "size": 82648, - "gzip": 25481 + "size": 82821, + "gzip": 25516 }, { "filename": "react-art.development.js", "bundleType": "NODE_DEV", "packageName": "react-art", - "size": 337640, - "gzip": 73051 + "size": 340077, + "gzip": 73406 }, { "filename": "react-art.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-art", - "size": 46153, - "gzip": 14348 + "size": 46325, + "gzip": 14365 }, { "filename": "ReactART-dev.js", @@ -291,29 +291,29 @@ "filename": "react-test-renderer.development.js", "bundleType": "UMD_DEV", "packageName": "react-test-renderer", - "size": 347431, - "gzip": 75093 + "size": 348566, + "gzip": 75374 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-test-renderer", - "size": 46664, - "gzip": 14400 + "size": 46908, + "gzip": 14528 }, { "filename": "react-test-renderer.development.js", "bundleType": "NODE_DEV", "packageName": "react-test-renderer", - "size": 338042, - "gzip": 72285 + "size": 339177, + "gzip": 72575 }, { "filename": "react-test-renderer.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-test-renderer", - "size": 45745, - "gzip": 13915 + "size": 45991, + "gzip": 14065 }, { "filename": "ReactTestRenderer-dev.js", @@ -375,29 +375,29 @@ "filename": "react-reconciler.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 331575, - "gzip": 70231 + "size": 332072, + "gzip": 70324 }, { "filename": "react-reconciler.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 46245, - "gzip": 13814 + "size": 46262, + "gzip": 13819 }, { "filename": "react-reconciler-persistent.development.js", "bundleType": "NODE_DEV", "packageName": "react-reconciler", - "size": 330095, - "gzip": 69615 + "size": 330592, + "gzip": 69703 }, { "filename": "react-reconciler-persistent.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-reconciler", - "size": 46256, - "gzip": 13819 + "size": 46273, + "gzip": 13824 }, { "filename": "react-reconciler-reflection.development.js", @@ -515,22 +515,22 @@ "filename": "ReactDOM-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 643334, - "gzip": 146983 + "size": 643129, + "gzip": 146879 }, { "filename": "ReactDOM-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-dom", - "size": 280306, - "gzip": 52771 + "size": 279501, + "gzip": 52628 }, { "filename": "ReactTestUtils-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-dom", - "size": 42513, - "gzip": 11507 + "size": 42280, + "gzip": 11404 }, { "filename": "ReactDOMUnstableNativeDependencies-dev.js", @@ -564,78 +564,78 @@ "filename": "ReactART-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-art", - "size": 343294, - "gzip": 72413 + "size": 345713, + "gzip": 72778 }, { "filename": "ReactART-prod.js", "bundleType": "FB_WWW_PROD", "packageName": "react-art", - "size": 150369, - "gzip": 25956 + "size": 151089, + "gzip": 26031 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 468415, - "gzip": 102416 + "size": 468774, + "gzip": 102457 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 210769, - "gzip": 36828 + "size": 210616, + "gzip": 36800 }, { "filename": "ReactNativeRenderer-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 468069, - "gzip": 102344 + "size": 468428, + "gzip": 102391 }, { "filename": "ReactNativeRenderer-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 198322, - "gzip": 34717 + "size": 198313, + "gzip": 34718 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_FB_DEV", "packageName": "react-native-renderer", - "size": 459114, - "gzip": 100116 + "size": 459473, + "gzip": 100166 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_FB_PROD", "packageName": "react-native-renderer", - "size": 190386, - "gzip": 33327 + "size": 190377, + "gzip": 33325 }, { "filename": "ReactFabric-dev.js", "bundleType": "RN_OSS_DEV", "packageName": "react-native-renderer", - "size": 459151, - "gzip": 100132 + "size": 459510, + "gzip": 100184 }, { "filename": "ReactFabric-prod.js", "bundleType": "RN_OSS_PROD", "packageName": "react-native-renderer", - "size": 190422, - "gzip": 33346 + "size": 190413, + "gzip": 33343 }, { "filename": "ReactTestRenderer-dev.js", "bundleType": "FB_WWW_DEV", "packageName": "react-test-renderer", - "size": 344028, - "gzip": 71858 + "size": 345219, + "gzip": 72155 }, { "filename": "ReactShallowRenderer-dev.js", @@ -662,29 +662,29 @@ "filename": "react-scheduler.development.js", "bundleType": "UMD_DEV", "packageName": "react-scheduler", - "size": 14094, - "gzip": 4681 + "size": 16042, + "gzip": 4949 }, { "filename": "react-scheduler.production.min.js", "bundleType": "UMD_PROD", "packageName": "react-scheduler", - "size": 2142, - "gzip": 1068 + "size": 2323, + "gzip": 1100 }, { "filename": "react-scheduler.development.js", "bundleType": "NODE_DEV", "packageName": "react-scheduler", - "size": 13898, - "gzip": 4632 + "size": 15846, + "gzip": 4900 }, { "filename": "react-scheduler.production.min.js", "bundleType": "NODE_PROD", "packageName": "react-scheduler", - "size": 2230, - "gzip": 1097 + "size": 2417, + "gzip": 1121 }, { "filename": "SimpleCacheProvider-dev.js", From fec5dd3634c37b3744eebbfe399d49c02aefbf7d Mon Sep 17 00:00:00 2001 From: Nathan Hunzaker Date: Wed, 6 Jun 2018 07:53:36 -0400 Subject: [PATCH 3/3] Add test case to cover proper capture/bubble order --- .../src/__tests__/ReactDOMComponent-test.js | 97 +++++++++++++------ 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index cf955d4d6d142..9569617c266e4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -965,33 +965,6 @@ describe('ReactDOMComponent', () => { }; }); - it('should work error event on element', () => { - const onError = jest.fn(); - const container = document.createElement('div'); - ReactDOM.render( - , - container, - ); - - try { - document.body.appendChild(container); - - const errorEvent = document.createEvent('Event'); - errorEvent.initEvent('error', false, false); - container.getElementsByTagName('source')[0].dispatchEvent(errorEvent); - - expect(onError).toHaveBeenCalledTimes(1); - } finally { - container.remove(); - } - }); - it('should not duplicate uppercased selfclosing tags', () => { class Container extends React.Component { render() { @@ -2443,4 +2416,74 @@ describe('ReactDOMComponent', () => { expect(node.getAttribute('onx')).toBe('bar'); }); }); + + describe('when dispatching events', () => { + it('should receive error events on elements', () => { + const onError = jest.fn(); + const container = document.createElement('div'); + + ReactDOM.render( + , + container, + ); + + try { + document.body.appendChild(container); + + const errorEvent = document.createEvent('Event'); + errorEvent.initEvent('error', false, false); + container.getElementsByTagName('source')[0].dispatchEvent(errorEvent); + + expect(onError).toHaveBeenCalledTimes(1); + } finally { + container.remove(); + } + }); + + it('receives events in the correct order', () => { + let eventOrder = []; + let track = tag => () => eventOrder.push(tag); + + class TestCase extends React.Component { + componentDidMount() { + document.addEventListener('click', track('document bubble')); + document.addEventListener('click', track('document capture'), true); + } + render() { + return ( +
+ ); + } + } + + const container = document.createElement('div'); + ReactDOM.render(, container); + + try { + document.body.appendChild(container); + + const errorEvent = document.createEvent('Event'); + errorEvent.initEvent('click', true, true); + container.querySelector('div').dispatchEvent(errorEvent); + + expect(eventOrder).toEqual([ + 'document capture', + 'element capture', + 'element bubble', + 'document bubble', + ]); + } finally { + container.remove(); + } + }); + }); });