diff --git a/packages/react-dom/src/__tests__/EventDispatchOrder-test.js b/packages/react-dom/src/__tests__/EventDispatchOrder-test.js
new file mode 100644
index 00000000000000..528b6fb8016e9e
--- /dev/null
+++ b/packages/react-dom/src/__tests__/EventDispatchOrder-test.js
@@ -0,0 +1,126 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+const React = require('react');
+const ReactDOM = require('react-dom');
+const ReactDOMServer = require('react-dom/server');
+const ReactTestUtils = require('react-dom/test-utils');
+
+describe('EventDispatchOrder', () => {
+ let container;
+
+ // The order we receive here is not ideal since it is expected that the
+ // capture listener fire before all bubble listeners. Other React apps
+ // might depend on this.
+ //
+ // @see https://github.com/facebook/react/pull/12919#issuecomment-395224674
+ const expectedOrder = [
+ 'document capture',
+ 'inner capture',
+ 'inner bubble',
+ 'outer capture',
+ 'outer bubble',
+ 'document bubble',
+ ];
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ container.remove();
+ });
+
+ function produceDispatchOrder(event, reactEvent) {
+ const eventOrder = [];
+ const track = tag => () => eventOrder.push(tag);
+ const outerRef = React.createRef();
+ const innerRef = React.createRef();
+
+ function OuterReactApp() {
+ return React.createElement('div', {
+ ref: outerRef,
+ [reactEvent]: track('outer bubble'),
+ [reactEvent + 'Capture']: track('outer capture'),
+ });
+ }
+
+ function InnerReactApp() {
+ return React.createElement('div', {
+ ref: innerRef,
+ [reactEvent]: track('inner bubble'),
+ [reactEvent + 'Capture']: track('inner capture'),
+ });
+ }
+
+ ReactDOM.unmountComponentAtNode(container);
+ ReactDOM.render(, container);
+
+ ReactDOM.unmountComponentAtNode(outerRef.current);
+ ReactDOM.render(, outerRef.current);
+
+ const trackDocBubble = track('document bubble');
+ const trackDocCapture = track('document capture');
+
+ document.addEventListener(event, trackDocBubble);
+ document.addEventListener(event, trackDocCapture, true);
+
+ innerRef.current.dispatchEvent(new Event(event, {bubbles: true}));
+
+ document.removeEventListener(event, trackDocBubble);
+ document.removeEventListener(event, trackDocCapture, true);
+
+ return eventOrder;
+ }
+
+ it('dispatches standard events in the correct order', () => {
+ expect(produceDispatchOrder('click', 'onClick')).toEqual(expectedOrder);
+ });
+
+ // Scroll and Wheel are attached as captured events directly to the element
+ describe('scrolling events', () => {
+ it('dispatches scroll events in the correct order', () => {
+ expect(produceDispatchOrder('scroll', 'onScroll')).toEqual(expectedOrder);
+ });
+
+ it('dispatches scroll events in the correct order', () => {
+ expect(produceDispatchOrder('wheel', 'onWheel')).toEqual(expectedOrder);
+ });
+ });
+
+ // Touch events are attached as bubbled events directly to the element
+ describe('touch events', () => {
+ it('dispatches touchstart in the correct order', () => {
+ expect(produceDispatchOrder('touchstart', 'onTouchStart')).toEqual(
+ expectedOrder,
+ );
+ });
+
+ it('dispatches touchend in the correct order', () => {
+ expect(produceDispatchOrder('touchend', 'onTouchEnd')).toEqual(
+ expectedOrder,
+ );
+ });
+
+ it('dispatches touchmove in the correct order', () => {
+ expect(produceDispatchOrder('touchmove', 'onTouchMove')).toEqual(
+ expectedOrder,
+ );
+ });
+
+ it('dispatches touchmove in the correct order', () => {
+ expect(produceDispatchOrder('touchcancel', 'onTouchCancel')).toEqual(
+ expectedOrder,
+ );
+ });
+ });
+});
diff --git a/packages/react-dom/src/__tests__/LocalEventListeners-test.js b/packages/react-dom/src/__tests__/LocalEventListeners-test.js
index 38633643937461..5f96cf850c6d82 100644
--- a/packages/react-dom/src/__tests__/LocalEventListeners-test.js
+++ b/packages/react-dom/src/__tests__/LocalEventListeners-test.js
@@ -192,4 +192,60 @@ describe('Local event listeners', () => {
expect(middle).toHaveBeenCalledTimes(1);
expect(bottom).toHaveBeenCalledTimes(1);
});
+
+ it('receives events in specific order', () => {
+ let eventOrder = [];
+ let track = tag => () => eventOrder.push(tag);
+ let outerRef = React.createRef();
+ let innerRef = React.createRef();
+
+ function OuterReactApp() {
+ return (
+
+ );
+ }
+
+ function InnerReactApp() {
+ return (
+
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ try {
+ ReactDOM.render(, container);
+ ReactDOM.render(, outerRef.current);
+
+ document.addEventListener('click', track('document bubble'));
+ document.addEventListener('click', track('document capture'), true);
+
+ innerRef.current.click();
+
+ // The order we receive here is not ideal since it is expected that the
+ // capture listener fire before all bubble listeners. Other React apps
+ // might depend on this.
+ //
+ // @see https://github.com/facebook/react/pull/12919#issuecomment-395224674
+ expect(eventOrder).toEqual([
+ 'document capture',
+ 'inner capture',
+ 'inner bubble',
+ 'outer capture',
+ 'outer bubble',
+ 'document bubble',
+ ]);
+ } finally {
+ document.body.removeChild(container);
+ }
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
index 4dc613805c94f4..b83ce5d5e1f36c 100644
--- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
@@ -2603,60 +2603,4 @@ describe('ReactDOMComponent', () => {
expect(node.getAttribute('onx')).toBe('bar');
});
});
-
- it('receives events in specific order', () => {
- let eventOrder = [];
- let track = tag => () => eventOrder.push(tag);
- let outerRef = React.createRef();
- let innerRef = React.createRef();
-
- function OuterReactApp() {
- return (
-
- );
- }
-
- function InnerReactApp() {
- return (
-
- );
- }
-
- const container = document.createElement('div');
- document.body.appendChild(container);
-
- try {
- ReactDOM.render(, container);
- ReactDOM.render(, outerRef.current);
-
- document.addEventListener('click', track('document bubble'));
- document.addEventListener('click', track('document capture'), true);
-
- innerRef.current.click();
-
- // The order we receive here is not ideal since it is expected that the
- // capture listener fire before all bubble listeners. Other React apps
- // might depend on this.
- //
- // @see https://github.com/facebook/react/pull/12919#issuecomment-395224674
- expect(eventOrder).toEqual([
- 'document capture',
- 'inner capture',
- 'inner bubble',
- 'outer capture',
- 'outer bubble',
- 'document bubble',
- ]);
- } finally {
- document.body.removeChild(container);
- }
- });
});
diff --git a/packages/react-dom/src/events/ReactBrowserEventEmitter.js b/packages/react-dom/src/events/ReactBrowserEventEmitter.js
index 2e000b938a20af..eeb280549e560b 100644
--- a/packages/react-dom/src/events/ReactBrowserEventEmitter.js
+++ b/packages/react-dom/src/events/ReactBrowserEventEmitter.js
@@ -135,6 +135,7 @@ export function listenTo(
) {
const mountAtListeners = getListenerTrackingFor(mountAt);
const dependencies = registrationNameDependencies[registrationName];
+ let elementListeners;
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
@@ -142,8 +143,7 @@ export function listenTo(
switch (dependency) {
case TOP_SCROLL:
case TOP_WHEEL:
- const elementListeners = getListenerTrackingFor(element);
-
+ elementListeners = getListenerTrackingFor(element);
if (!elementListeners.hasOwnProperty(dependency)) {
trapCapturedEvent(dependency, element);
elementListeners[dependency] = true;
@@ -153,8 +153,7 @@ export function listenTo(
case TOP_TOUCH_END:
case TOP_TOUCH_MOVE:
case TOP_TOUCH_CANCEL:
- const elementListeners = getListenerTrackingFor(element);
-
+ elementListeners = getListenerTrackingFor(element);
if (!elementListeners.hasOwnProperty(dependency)) {
trapBubbledEvent(dependency, element);
elementListeners[dependency] = true;