diff --git a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js
index 64099b4b8fed4..44c94a4cff623 100644
--- a/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js
@@ -17,6 +17,7 @@ let ReactDOMComponentTree;
let listenToEvent;
let ReactDOMEventListener;
let ReactTestUtils;
+let ReactFeatureFlags;
let idCallOrder;
const recordID = function(id) {
@@ -60,13 +61,20 @@ describe('ReactBrowserEventEmitter', () => {
jest.resetModules();
LISTENER.mockClear();
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
EventPluginGetListener = require('legacy-events/getListener').default;
EventPluginRegistry = require('legacy-events/EventPluginRegistry');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMComponentTree = require('../client/ReactDOMComponentTree');
- listenToEvent = require('../events/DOMLegacyEventPluginSystem')
- .legacyListenToEvent;
+ if (ReactFeatureFlags.enableModernEventSystem) {
+ listenToEvent = require('../events/DOMModernPluginEventSystem')
+ .listenToEvent;
+ } else {
+ listenToEvent = require('../events/DOMLegacyEventPluginSystem')
+ .legacyListenToEvent;
+ }
+
ReactDOMEventListener = require('../events/ReactDOMEventListener');
ReactTestUtils = require('react-dom/test-utils');
diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
index a1cd76874beb7..729044b6b4b91 100644
--- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
@@ -12,6 +12,7 @@
describe('ReactDOMEventListener', () => {
let React;
let ReactDOM;
+ let ReactFeatureFlags = require('shared/ReactFeatureFlags');
beforeEach(() => {
jest.resetModules();
@@ -19,29 +20,33 @@ describe('ReactDOMEventListener', () => {
ReactDOM = require('react-dom');
});
- it('should dispatch events from outside React tree', () => {
- const mock = jest.fn();
+ // We attached events to roots with the modern system,
+ // so this test is no longer valid.
+ if (!ReactFeatureFlags.enableModernEventSystem) {
+ it('should dispatch events from outside React tree', () => {
+ const mock = jest.fn();
- const container = document.createElement('div');
- const node = ReactDOM.render(
, container);
- const otherNode = document.createElement('h1');
- document.body.appendChild(container);
- document.body.appendChild(otherNode);
+ const container = document.createElement('div');
+ const node = ReactDOM.render(, container);
+ const otherNode = document.createElement('h1');
+ document.body.appendChild(container);
+ document.body.appendChild(otherNode);
- try {
- otherNode.dispatchEvent(
- new MouseEvent('mouseout', {
- bubbles: true,
- cancelable: true,
- relatedTarget: node,
- }),
- );
- expect(mock).toBeCalled();
- } finally {
- document.body.removeChild(container);
- document.body.removeChild(otherNode);
- }
- });
+ try {
+ otherNode.dispatchEvent(
+ new MouseEvent('mouseout', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: node,
+ }),
+ );
+ expect(mock).toBeCalled();
+ } finally {
+ document.body.removeChild(container);
+ document.body.removeChild(otherNode);
+ }
+ });
+ }
describe('Propagation', () => {
it('should propagate events one level down', () => {
@@ -189,9 +194,25 @@ describe('ReactDOMEventListener', () => {
// The first call schedules a render of '1' into the 'Child'.
// However, we're batching so it isn't flushed yet.
expect(mock.mock.calls[0][0]).toBe('Child');
- // The first call schedules a render of '2' into the 'Child'.
- // We're still batching so it isn't flushed yet either.
- expect(mock.mock.calls[1][0]).toBe('Child');
+ if (ReactFeatureFlags.enableModernEventSystem) {
+ // As we have two roots, it means we have two event listeners.
+ // This also means we enter the event batching phase twice,
+ // flushing the child to be 1.
+
+ // We don't have any good way of knowing if another event will
+ // occur because another event handler might invoke
+ // stopPropagation() along the way. After discussions internally
+ // with Sebastian, it seems that for now over-flushing should
+ // be fine, especially as the new event system is a breaking
+ // change anyway. We can maybe revisit this later as part of
+ // the work to refine this in the scheduler (maybe by leveraging
+ // isInputPending?).
+ expect(mock.mock.calls[1][0]).toBe('1');
+ } else {
+ // The first call schedules a render of '2' into the 'Child'.
+ // We're still batching so it isn't flushed yet either.
+ expect(mock.mock.calls[1][0]).toBe('Child');
+ }
// By the time we leave the handler, the second update is flushed.
expect(childNode.textContent).toBe('2');
} finally {
@@ -362,13 +383,25 @@ describe('ReactDOMEventListener', () => {
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);
+ if (ReactFeatureFlags.enableModernEventSystem) {
+ // As of the modern event system refactor, we now support
+ // this on . The reason for this, is because we now
+ // attach all media events to the "root" or "portal" in the
+ // capture phase, rather than the bubble phase. This allows
+ // us to assign less event listeners to individual elements,
+ // which also nicely allows us to support more without needing
+ // to add more individual code paths to support various
+ // events that do not bubble.
+ expect(handleImgLoadStart).toHaveBeenCalledTimes(1);
+ } else {
+ // 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', {
diff --git a/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js b/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js
index 11428dd429017..5b0379827f0ee 100644
--- a/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js
+++ b/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js
@@ -11,6 +11,7 @@
let React;
let ReactDOM;
+let ReactFeatureFlags = require('shared/ReactFeatureFlags');
const ChildComponent = ({id, eventHandler}) => (
{
expect(mockFn.mock.calls).toEqual(expectedCalls);
});
- it('should enter from the window', () => {
- const enterNode = document.getElementById('P_P1_C1__DIV');
-
- const expectedCalls = [
- ['P', 'mouseenter'],
- ['P_P1', 'mouseenter'],
- ['P_P1_C1__DIV', 'mouseenter'],
- ];
-
- outerNode1.dispatchEvent(
- new MouseEvent('mouseout', {
- bubbles: true,
- cancelable: true,
- relatedTarget: enterNode,
- }),
- );
-
- expect(mockFn.mock.calls).toEqual(expectedCalls);
- });
-
- it('should enter from the window to the shallowest', () => {
- const enterNode = document.getElementById('P');
-
- const expectedCalls = [['P', 'mouseenter']];
-
- outerNode1.dispatchEvent(
- new MouseEvent('mouseout', {
- bubbles: true,
- cancelable: true,
- relatedTarget: enterNode,
- }),
- );
-
- expect(mockFn.mock.calls).toEqual(expectedCalls);
- });
+ // This will not work with the modern event system that
+ // attaches event listeners to roots as the event below
+ // is being triggered on a node that React does not listen
+ // to any more. Instead we should fire mouseover.
+ if (ReactFeatureFlags.enableModernEventSystem) {
+ it('should enter from the window', () => {
+ const enterNode = document.getElementById('P_P1_C1__DIV');
+
+ const expectedCalls = [
+ ['P', 'mouseenter'],
+ ['P_P1', 'mouseenter'],
+ ['P_P1_C1__DIV', 'mouseenter'],
+ ];
+
+ enterNode.dispatchEvent(
+ new MouseEvent('mouseover', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: outerNode1,
+ }),
+ );
+
+ expect(mockFn.mock.calls).toEqual(expectedCalls);
+ });
+ } else {
+ it('should enter from the window', () => {
+ const enterNode = document.getElementById('P_P1_C1__DIV');
+
+ const expectedCalls = [
+ ['P', 'mouseenter'],
+ ['P_P1', 'mouseenter'],
+ ['P_P1_C1__DIV', 'mouseenter'],
+ ];
+
+ outerNode1.dispatchEvent(
+ new MouseEvent('mouseout', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: enterNode,
+ }),
+ );
+
+ expect(mockFn.mock.calls).toEqual(expectedCalls);
+ });
+ }
+
+ // This will not work with the modern event system that
+ // attaches event listeners to roots as the event below
+ // is being triggered on a node that React does not listen
+ // to any more. Instead we should fire mouseover.
+ if (ReactFeatureFlags.enableModernEventSystem) {
+ it('should enter from the window to the shallowest', () => {
+ const enterNode = document.getElementById('P');
+
+ const expectedCalls = [['P', 'mouseenter']];
+
+ enterNode.dispatchEvent(
+ new MouseEvent('mouseover', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: outerNode1,
+ }),
+ );
+
+ expect(mockFn.mock.calls).toEqual(expectedCalls);
+ });
+ } else {
+ it('should enter from the window to the shallowest', () => {
+ const enterNode = document.getElementById('P');
+
+ const expectedCalls = [['P', 'mouseenter']];
+
+ outerNode1.dispatchEvent(
+ new MouseEvent('mouseout', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: enterNode,
+ }),
+ );
+
+ expect(mockFn.mock.calls).toEqual(expectedCalls);
+ });
+ }
it('should leave to the window', () => {
const leaveNode = document.getElementById('P_P1_C1__DIV');
diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js
index aeeba5933d6a6..b8c07cc4e914f 100644
--- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js
+++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js
@@ -11,10 +11,16 @@ import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
+import type {PluginModule} from 'legacy-events/PluginModuleType';
+import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
+import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
+import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils';
+import {plugins} from 'legacy-events/EventPluginRegistry';
import {trapEventForPluginEventSystem} from './ReactDOMEventListener';
+import getEventTarget from './getEventTarget';
import {getListenerMapForElement} from './DOMEventListenerMap';
import {
TOP_FOCUS,
@@ -87,6 +93,49 @@ const capturePhaseEvents = new Set([
TOP_WAITING,
]);
+const isArray = Array.isArray;
+
+function dispatchEventsForPlugins(
+ topLevelType: DOMTopLevelEventType,
+ eventSystemFlags: EventSystemFlags,
+ nativeEvent: AnyNativeEvent,
+ targetInst: null | Fiber,
+ rootContainer: Element | Document,
+): void {
+ const nativeEventTarget = getEventTarget(nativeEvent);
+ const syntheticEvents: Array = [];
+
+ for (let i = 0; i < plugins.length; i++) {
+ const possiblePlugin: PluginModule = plugins[i];
+ if (possiblePlugin !== undefined) {
+ const extractedEvents = possiblePlugin.extractEvents(
+ topLevelType,
+ targetInst,
+ nativeEvent,
+ nativeEventTarget,
+ eventSystemFlags,
+ rootContainer,
+ );
+ if (isArray(extractedEvents)) {
+ // Flow complains about @@iterator being missing in ReactSyntheticEvent,
+ // so we cast to avoid the Flow error.
+ const arrOfExtractedEvents = ((extractedEvents: any): Array);
+ syntheticEvents.push(...arrOfExtractedEvents);
+ } else if (extractedEvents != null) {
+ syntheticEvents.push(extractedEvents);
+ }
+ }
+ }
+ for (let i = 0; i < syntheticEvents.length; i++) {
+ const syntheticEvent = syntheticEvents[i];
+ executeDispatchesInOrder(syntheticEvent);
+ // Release the event from the pool if needed
+ if (!syntheticEvent.isPersistent()) {
+ syntheticEvent.constructor.release(syntheticEvent);
+ }
+ }
+}
+
export function listenToTopLevelEvent(
topLevelType: DOMTopLevelEventType,
rootContainerElement: Element,
@@ -123,5 +172,15 @@ export function dispatchEventForPluginEventSystem(
targetInst: null | Fiber,
rootContainer: Document | Element,
): void {
- // TODO
+ let ancestorInst = targetInst;
+
+ batchedEventUpdates(() =>
+ dispatchEventsForPlugins(
+ topLevelType,
+ eventSystemFlags,
+ nativeEvent,
+ ancestorInst,
+ rootContainer,
+ ),
+ );
}
diff --git a/packages/react-dom/src/events/EnterLeaveEventPlugin.js b/packages/react-dom/src/events/EnterLeaveEventPlugin.js
index de9b130462fb0..4130cd709ad20 100644
--- a/packages/react-dom/src/events/EnterLeaveEventPlugin.js
+++ b/packages/react-dom/src/events/EnterLeaveEventPlugin.js
@@ -22,6 +22,7 @@ import {
} from '../client/ReactDOMComponentTree';
import {HostComponent, HostText} from 'shared/ReactWorkTags';
import {getNearestMountedFiber} from 'react-reconciler/reflection';
+import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
const eventTypes = {
mouseEnter: {
@@ -64,16 +65,26 @@ const EnterLeaveEventPlugin = {
const isOutEvent =
topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT;
- if (
- isOverEvent &&
- (eventSystemFlags & IS_REPLAYED) === 0 &&
- (nativeEvent.relatedTarget || nativeEvent.fromElement)
- ) {
- // If this is an over event with a target, then we've already dispatched
- // the event in the out event of the other target. If this is replayed,
- // then it's because we couldn't dispatch against this target previously
- // so we have to do it now instead.
- return null;
+ if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0) {
+ const related = nativeEvent.relatedTarget || nativeEvent.fromElement;
+ if (related) {
+ if (enableModernEventSystem) {
+ // Due to the fact we don't add listeners to the document with the
+ // modern event system and instead attach listeners to roots, we
+ // need to handle the over event case. To ensure this, we just need to
+ // make sure the node that we're coming from is managed by React.
+ const inst = getClosestInstanceFromNode(related);
+ if (inst !== null) {
+ return null;
+ }
+ } else {
+ // If this is an over event with a target, then we've already dispatched
+ // the event in the out event of the other target. If this is replayed,
+ // then it's because we couldn't dispatch against this target previously
+ // so we have to do it now instead.
+ return null;
+ }
+ }
}
if (!isOutEvent && !isOverEvent) {
@@ -163,11 +174,13 @@ const EnterLeaveEventPlugin = {
accumulateEnterLeaveDispatches(leave, enter, from, to);
- // If we are not processing the first ancestor, then we
- // should not process the same nativeEvent again, as we
- // will have already processed it in the first ancestor.
- if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) {
- return [leave];
+ if (!enableModernEventSystem) {
+ // If we are not processing the first ancestor, then we
+ // should not process the same nativeEvent again, as we
+ // will have already processed it in the first ancestor.
+ if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) {
+ return [leave];
+ }
}
return [leave, enter];
diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
new file mode 100644
index 0000000000000..66c670b7ca3ec
--- /dev/null
+++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * 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';
+
+let React;
+let ReactFeatureFlags;
+let ReactDOM;
+
+function dispatchClickEvent(element) {
+ const event = document.createEvent('Event');
+ event.initEvent('click', true, true);
+ element.dispatchEvent(event);
+}
+
+describe('DOMModernPluginEventSystem', () => {
+ let container;
+
+ beforeEach(() => {
+ jest.resetModules();
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
+ ReactFeatureFlags.enableModernEventSystem = true;
+
+ React = require('react');
+ ReactDOM = require('react-dom');
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ container = null;
+ });
+
+ it('handle propagation of click events', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const log = [];
+ const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ function Test() {
+ return (
+
+ );
+ }
+
+ ReactDOM.render(, container);
+
+ let buttonElement = buttonRef.current;
+ dispatchClickEvent(buttonElement);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onClickCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ dispatchClickEvent(divElement);
+ expect(onClick).toHaveBeenCalledTimes(3);
+ expect(onClickCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', buttonElement]);
+ expect(log[3]).toEqual(['capture', divElement]);
+ expect(log[4]).toEqual(['bubble', divElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+ });
+
+ it('handle propagation of focus events', () => {
+ const buttonRef = React.createRef();
+ const divRef = React.createRef();
+ const log = [];
+ const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
+ const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
+
+ function Test() {
+ return (
+
+ );
+ }
+
+ ReactDOM.render(, container);
+
+ let buttonElement = buttonRef.current;
+ buttonElement.focus();
+ expect(onFocus).toHaveBeenCalledTimes(1);
+ expect(onFocusCapture).toHaveBeenCalledTimes(1);
+ expect(log[0]).toEqual(['capture', buttonElement]);
+ expect(log[1]).toEqual(['bubble', buttonElement]);
+
+ let divElement = divRef.current;
+ divElement.focus();
+ expect(onFocus).toHaveBeenCalledTimes(3);
+ expect(onFocusCapture).toHaveBeenCalledTimes(3);
+ expect(log[2]).toEqual(['capture', buttonElement]);
+ expect(log[3]).toEqual(['capture', divElement]);
+ expect(log[4]).toEqual(['bubble', divElement]);
+ expect(log[5]).toEqual(['bubble', buttonElement]);
+ });
+});