Skip to content

Commit 8e6a08e

Browse files
authored
Modern Event System: add plugin handling and forked paths (#18195)
1 parent 7e83af1 commit 8e6a08e

6 files changed

+368
-84
lines changed

packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ let ReactDOMComponentTree;
1717
let listenToEvent;
1818
let ReactDOMEventListener;
1919
let ReactTestUtils;
20+
let ReactFeatureFlags;
2021

2122
let idCallOrder;
2223
const recordID = function(id) {
@@ -60,13 +61,20 @@ describe('ReactBrowserEventEmitter', () => {
6061
jest.resetModules();
6162
LISTENER.mockClear();
6263

64+
ReactFeatureFlags = require('shared/ReactFeatureFlags');
6365
EventPluginGetListener = require('legacy-events/getListener').default;
6466
EventPluginRegistry = require('legacy-events/EventPluginRegistry');
6567
React = require('react');
6668
ReactDOM = require('react-dom');
6769
ReactDOMComponentTree = require('../client/ReactDOMComponentTree');
68-
listenToEvent = require('../events/DOMLegacyEventPluginSystem')
69-
.legacyListenToEvent;
70+
if (ReactFeatureFlags.enableModernEventSystem) {
71+
listenToEvent = require('../events/DOMModernPluginEventSystem')
72+
.listenToEvent;
73+
} else {
74+
listenToEvent = require('../events/DOMLegacyEventPluginSystem')
75+
.legacyListenToEvent;
76+
}
77+
7078
ReactDOMEventListener = require('../events/ReactDOMEventListener');
7179
ReactTestUtils = require('react-dom/test-utils');
7280

packages/react-dom/src/__tests__/ReactDOMEventListener-test.js

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,41 @@
1212
describe('ReactDOMEventListener', () => {
1313
let React;
1414
let ReactDOM;
15+
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
1516

1617
beforeEach(() => {
1718
jest.resetModules();
1819
React = require('react');
1920
ReactDOM = require('react-dom');
2021
});
2122

22-
it('should dispatch events from outside React tree', () => {
23-
const mock = jest.fn();
23+
// We attached events to roots with the modern system,
24+
// so this test is no longer valid.
25+
if (!ReactFeatureFlags.enableModernEventSystem) {
26+
it('should dispatch events from outside React tree', () => {
27+
const mock = jest.fn();
2428

25-
const container = document.createElement('div');
26-
const node = ReactDOM.render(<div onMouseEnter={mock} />, container);
27-
const otherNode = document.createElement('h1');
28-
document.body.appendChild(container);
29-
document.body.appendChild(otherNode);
29+
const container = document.createElement('div');
30+
const node = ReactDOM.render(<div onMouseEnter={mock} />, container);
31+
const otherNode = document.createElement('h1');
32+
document.body.appendChild(container);
33+
document.body.appendChild(otherNode);
3034

31-
try {
32-
otherNode.dispatchEvent(
33-
new MouseEvent('mouseout', {
34-
bubbles: true,
35-
cancelable: true,
36-
relatedTarget: node,
37-
}),
38-
);
39-
expect(mock).toBeCalled();
40-
} finally {
41-
document.body.removeChild(container);
42-
document.body.removeChild(otherNode);
43-
}
44-
});
35+
try {
36+
otherNode.dispatchEvent(
37+
new MouseEvent('mouseout', {
38+
bubbles: true,
39+
cancelable: true,
40+
relatedTarget: node,
41+
}),
42+
);
43+
expect(mock).toBeCalled();
44+
} finally {
45+
document.body.removeChild(container);
46+
document.body.removeChild(otherNode);
47+
}
48+
});
49+
}
4550

4651
describe('Propagation', () => {
4752
it('should propagate events one level down', () => {
@@ -189,9 +194,25 @@ describe('ReactDOMEventListener', () => {
189194
// The first call schedules a render of '1' into the 'Child'.
190195
// However, we're batching so it isn't flushed yet.
191196
expect(mock.mock.calls[0][0]).toBe('Child');
192-
// The first call schedules a render of '2' into the 'Child'.
193-
// We're still batching so it isn't flushed yet either.
194-
expect(mock.mock.calls[1][0]).toBe('Child');
197+
if (ReactFeatureFlags.enableModernEventSystem) {
198+
// As we have two roots, it means we have two event listeners.
199+
// This also means we enter the event batching phase twice,
200+
// flushing the child to be 1.
201+
202+
// We don't have any good way of knowing if another event will
203+
// occur because another event handler might invoke
204+
// stopPropagation() along the way. After discussions internally
205+
// with Sebastian, it seems that for now over-flushing should
206+
// be fine, especially as the new event system is a breaking
207+
// change anyway. We can maybe revisit this later as part of
208+
// the work to refine this in the scheduler (maybe by leveraging
209+
// isInputPending?).
210+
expect(mock.mock.calls[1][0]).toBe('1');
211+
} else {
212+
// The first call schedules a render of '2' into the 'Child'.
213+
// We're still batching so it isn't flushed yet either.
214+
expect(mock.mock.calls[1][0]).toBe('Child');
215+
}
195216
// By the time we leave the handler, the second update is flushed.
196217
expect(childNode.textContent).toBe('2');
197218
} finally {
@@ -362,13 +383,25 @@ describe('ReactDOMEventListener', () => {
362383
bubbles: false,
363384
}),
364385
);
365-
// Historically, we happened to not support onLoadStart
366-
// on <img>, and this test documents that lack of support.
367-
// If we decide to support it in the future, we should change
368-
// this line to expect 1 call. Note that fixing this would
369-
// be simple but would require attaching a handler to each
370-
// <img>. So far nobody asked us for it.
371-
expect(handleImgLoadStart).toHaveBeenCalledTimes(0);
386+
if (ReactFeatureFlags.enableModernEventSystem) {
387+
// As of the modern event system refactor, we now support
388+
// this on <img>. The reason for this, is because we now
389+
// attach all media events to the "root" or "portal" in the
390+
// capture phase, rather than the bubble phase. This allows
391+
// us to assign less event listeners to individual elements,
392+
// which also nicely allows us to support more without needing
393+
// to add more individual code paths to support various
394+
// events that do not bubble.
395+
expect(handleImgLoadStart).toHaveBeenCalledTimes(1);
396+
} else {
397+
// Historically, we happened to not support onLoadStart
398+
// on <img>, and this test documents that lack of support.
399+
// If we decide to support it in the future, we should change
400+
// this line to expect 1 call. Note that fixing this would
401+
// be simple but would require attaching a handler to each
402+
// <img>. So far nobody asked us for it.
403+
expect(handleImgLoadStart).toHaveBeenCalledTimes(0);
404+
}
372405

373406
videoRef.current.dispatchEvent(
374407
new ProgressEvent('loadstart', {

packages/react-dom/src/__tests__/ReactTreeTraversal-test.js

Lines changed: 84 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
let React;
1313
let ReactDOM;
14+
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
1415

1516
const ChildComponent = ({id, eventHandler}) => (
1617
<div
@@ -203,41 +204,89 @@ describe('ReactTreeTraversal', () => {
203204
expect(mockFn.mock.calls).toEqual(expectedCalls);
204205
});
205206

206-
it('should enter from the window', () => {
207-
const enterNode = document.getElementById('P_P1_C1__DIV');
208-
209-
const expectedCalls = [
210-
['P', 'mouseenter'],
211-
['P_P1', 'mouseenter'],
212-
['P_P1_C1__DIV', 'mouseenter'],
213-
];
214-
215-
outerNode1.dispatchEvent(
216-
new MouseEvent('mouseout', {
217-
bubbles: true,
218-
cancelable: true,
219-
relatedTarget: enterNode,
220-
}),
221-
);
222-
223-
expect(mockFn.mock.calls).toEqual(expectedCalls);
224-
});
225-
226-
it('should enter from the window to the shallowest', () => {
227-
const enterNode = document.getElementById('P');
228-
229-
const expectedCalls = [['P', 'mouseenter']];
230-
231-
outerNode1.dispatchEvent(
232-
new MouseEvent('mouseout', {
233-
bubbles: true,
234-
cancelable: true,
235-
relatedTarget: enterNode,
236-
}),
237-
);
238-
239-
expect(mockFn.mock.calls).toEqual(expectedCalls);
240-
});
207+
// This will not work with the modern event system that
208+
// attaches event listeners to roots as the event below
209+
// is being triggered on a node that React does not listen
210+
// to any more. Instead we should fire mouseover.
211+
if (ReactFeatureFlags.enableModernEventSystem) {
212+
it('should enter from the window', () => {
213+
const enterNode = document.getElementById('P_P1_C1__DIV');
214+
215+
const expectedCalls = [
216+
['P', 'mouseenter'],
217+
['P_P1', 'mouseenter'],
218+
['P_P1_C1__DIV', 'mouseenter'],
219+
];
220+
221+
enterNode.dispatchEvent(
222+
new MouseEvent('mouseover', {
223+
bubbles: true,
224+
cancelable: true,
225+
relatedTarget: outerNode1,
226+
}),
227+
);
228+
229+
expect(mockFn.mock.calls).toEqual(expectedCalls);
230+
});
231+
} else {
232+
it('should enter from the window', () => {
233+
const enterNode = document.getElementById('P_P1_C1__DIV');
234+
235+
const expectedCalls = [
236+
['P', 'mouseenter'],
237+
['P_P1', 'mouseenter'],
238+
['P_P1_C1__DIV', 'mouseenter'],
239+
];
240+
241+
outerNode1.dispatchEvent(
242+
new MouseEvent('mouseout', {
243+
bubbles: true,
244+
cancelable: true,
245+
relatedTarget: enterNode,
246+
}),
247+
);
248+
249+
expect(mockFn.mock.calls).toEqual(expectedCalls);
250+
});
251+
}
252+
253+
// This will not work with the modern event system that
254+
// attaches event listeners to roots as the event below
255+
// is being triggered on a node that React does not listen
256+
// to any more. Instead we should fire mouseover.
257+
if (ReactFeatureFlags.enableModernEventSystem) {
258+
it('should enter from the window to the shallowest', () => {
259+
const enterNode = document.getElementById('P');
260+
261+
const expectedCalls = [['P', 'mouseenter']];
262+
263+
enterNode.dispatchEvent(
264+
new MouseEvent('mouseover', {
265+
bubbles: true,
266+
cancelable: true,
267+
relatedTarget: outerNode1,
268+
}),
269+
);
270+
271+
expect(mockFn.mock.calls).toEqual(expectedCalls);
272+
});
273+
} else {
274+
it('should enter from the window to the shallowest', () => {
275+
const enterNode = document.getElementById('P');
276+
277+
const expectedCalls = [['P', 'mouseenter']];
278+
279+
outerNode1.dispatchEvent(
280+
new MouseEvent('mouseout', {
281+
bubbles: true,
282+
cancelable: true,
283+
relatedTarget: enterNode,
284+
}),
285+
);
286+
287+
expect(mockFn.mock.calls).toEqual(expectedCalls);
288+
});
289+
}
241290

242291
it('should leave to the window', () => {
243292
const leaveNode = document.getElementById('P_P1_C1__DIV');

packages/react-dom/src/events/DOMModernPluginEventSystem.js

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
1111
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
1212
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
1313
import type {Fiber} from 'react-reconciler/src/ReactFiber';
14+
import type {PluginModule} from 'legacy-events/PluginModuleType';
15+
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
1416

1517
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
18+
import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
19+
import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils';
20+
import {plugins} from 'legacy-events/EventPluginRegistry';
1621

1722
import {trapEventForPluginEventSystem} from './ReactDOMEventListener';
23+
import getEventTarget from './getEventTarget';
1824
import {getListenerMapForElement} from './DOMEventListenerMap';
1925
import {
2026
TOP_FOCUS,
@@ -87,6 +93,49 @@ const capturePhaseEvents = new Set([
8793
TOP_WAITING,
8894
]);
8995

96+
const isArray = Array.isArray;
97+
98+
function dispatchEventsForPlugins(
99+
topLevelType: DOMTopLevelEventType,
100+
eventSystemFlags: EventSystemFlags,
101+
nativeEvent: AnyNativeEvent,
102+
targetInst: null | Fiber,
103+
rootContainer: Element | Document,
104+
): void {
105+
const nativeEventTarget = getEventTarget(nativeEvent);
106+
const syntheticEvents: Array<ReactSyntheticEvent> = [];
107+
108+
for (let i = 0; i < plugins.length; i++) {
109+
const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
110+
if (possiblePlugin !== undefined) {
111+
const extractedEvents = possiblePlugin.extractEvents(
112+
topLevelType,
113+
targetInst,
114+
nativeEvent,
115+
nativeEventTarget,
116+
eventSystemFlags,
117+
rootContainer,
118+
);
119+
if (isArray(extractedEvents)) {
120+
// Flow complains about @@iterator being missing in ReactSyntheticEvent,
121+
// so we cast to avoid the Flow error.
122+
const arrOfExtractedEvents = ((extractedEvents: any): Array<ReactSyntheticEvent>);
123+
syntheticEvents.push(...arrOfExtractedEvents);
124+
} else if (extractedEvents != null) {
125+
syntheticEvents.push(extractedEvents);
126+
}
127+
}
128+
}
129+
for (let i = 0; i < syntheticEvents.length; i++) {
130+
const syntheticEvent = syntheticEvents[i];
131+
executeDispatchesInOrder(syntheticEvent);
132+
// Release the event from the pool if needed
133+
if (!syntheticEvent.isPersistent()) {
134+
syntheticEvent.constructor.release(syntheticEvent);
135+
}
136+
}
137+
}
138+
90139
export function listenToTopLevelEvent(
91140
topLevelType: DOMTopLevelEventType,
92141
rootContainerElement: Element,
@@ -123,5 +172,15 @@ export function dispatchEventForPluginEventSystem(
123172
targetInst: null | Fiber,
124173
rootContainer: Document | Element,
125174
): void {
126-
// TODO
175+
let ancestorInst = targetInst;
176+
177+
batchedEventUpdates(() =>
178+
dispatchEventsForPlugins(
179+
topLevelType,
180+
eventSystemFlags,
181+
nativeEvent,
182+
ancestorInst,
183+
rootContainer,
184+
),
185+
);
127186
}

0 commit comments

Comments
 (0)