@samitouri / QOS-React-2 / commits / 8e6a08ea4f

Modern Event System: add plugin handling and forked paths (#18195)

Dominic Gannaway committed Mar 3, 2020 at 14:37 UTC 8e6a08ea4f3beb2127ec3d1595cf5f83df5816b6
6 files changed +368 -84
packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js
+10 -2
@@ -17,6 +17,7 @@ let ReactDOMComponentTree;
17 let listenToEvent;
18 let ReactDOMEventListener;
19 let ReactTestUtils;
20 +let ReactFeatureFlags;
21
22 let idCallOrder;
23 const recordID = function(id) {
@@ -60,13 +61,20 @@ describe('ReactBrowserEventEmitter', () => {
61 jest.resetModules();
62 LISTENER.mockClear();
63
64 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
65 EventPluginGetListener = require('legacy-events/getListener').default;
66 EventPluginRegistry = require('legacy-events/EventPluginRegistry');
67 React = require('react');
68 ReactDOM = require('react-dom');
69 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 +
78 ReactDOMEventListener = require('../events/ReactDOMEventListener');
79 ReactTestUtils = require('react-dom/test-utils');
80
packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
+64 -31
@@ -12,6 +12,7 @@
12 describe('ReactDOMEventListener', () => {
13 let React;
14 let ReactDOM;
15 + let ReactFeatureFlags = require('shared/ReactFeatureFlags');
16
17 beforeEach(() => {
18 jest.resetModules();
@@ -19,29 +20,33 @@ describe('ReactDOMEventListener', () => {
20 ReactDOM = require('react-dom');
21 });
22
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();
28
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);
34
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 + }
50
51 describe('Propagation', () => {
52 it('should propagate events one level down', () => {
@@ -189,9 +194,25 @@ describe('ReactDOMEventListener', () => {
194 // The first call schedules a render of '1' into the 'Child'.
195 // However, we're batching so it isn't flushed yet.
196 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 + }
216 // By the time we leave the handler, the second update is flushed.
217 expect(childNode.textContent).toBe('2');
218 } finally {
@@ -362,13 +383,25 @@ describe('ReactDOMEventListener', () => {
383 bubbles: false,
384 }),
385 );
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 + }
405
406 videoRef.current.dispatchEvent(
407 new ProgressEvent('loadstart', {
packages/react-dom/src/__tests__/ReactTreeTraversal-test.js
+84 -35
@@ -11,6 +11,7 @@
11
12 let React;
13 let ReactDOM;
14 +let ReactFeatureFlags = require('shared/ReactFeatureFlags');
15
16 const ChildComponent = ({id, eventHandler}) => (
17 <div
@@ -203,41 +204,89 @@ describe('ReactTreeTraversal', () => {
204 expect(mockFn.mock.calls).toEqual(expectedCalls);
205 });
206
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 + }
290
291 it('should leave to the window', () => {
292 const leaveNode = document.getElementById('P_P1_C1__DIV');
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+60 -1
@@ -11,10 +11,16 @@ import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
11 import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
12 import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
13 import type {Fiber} from 'react-reconciler/src/ReactFiber';
14 +import type {PluginModule} from 'legacy-events/PluginModuleType';
15 +import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
16
17 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';
21
22 import {trapEventForPluginEventSystem} from './ReactDOMEventListener';
23 +import getEventTarget from './getEventTarget';
24 import {getListenerMapForElement} from './DOMEventListenerMap';
25 import {
26 TOP_FOCUS,
@@ -87,6 +93,49 @@ const capturePhaseEvents = new Set([
93 TOP_WAITING,
94 ]);
95
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 +
139 export function listenToTopLevelEvent(
140 topLevelType: DOMTopLevelEventType,
141 rootContainerElement: Element,
@@ -123,5 +172,15 @@ export function dispatchEventForPluginEventSystem(
172 targetInst: null | Fiber,
173 rootContainer: Document | Element,
174 ): void {
126 - // TODO
175 + let ancestorInst = targetInst;
176 +
177 + batchedEventUpdates(() =>
178 + dispatchEventsForPlugins(
179 + topLevelType,
180 + eventSystemFlags,
181 + nativeEvent,
182 + ancestorInst,
183 + rootContainer,
184 + ),
185 + );
186 }
packages/react-dom/src/events/EnterLeaveEventPlugin.js
+28 -15
@@ -22,6 +22,7 @@ import {
22 } from '../client/ReactDOMComponentTree';
23 import {HostComponent, HostText} from 'shared/ReactWorkTags';
24 import {getNearestMountedFiber} from 'react-reconciler/reflection';
25 +import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
26
27 const eventTypes = {
28 mouseEnter: {
@@ -64,16 +65,26 @@ const EnterLeaveEventPlugin = {
65 const isOutEvent =
66 topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT;
67
67 - if (
68 - isOverEvent &&
69 - (eventSystemFlags & IS_REPLAYED) === 0 &&
70 - (nativeEvent.relatedTarget || nativeEvent.fromElement)
71 - ) {
72 - // If this is an over event with a target, then we've already dispatched
73 - // the event in the out event of the other target. If this is replayed,
74 - // then it's because we couldn't dispatch against this target previously
75 - // so we have to do it now instead.
76 - return null;
68 + if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0) {
69 + const related = nativeEvent.relatedTarget || nativeEvent.fromElement;
70 + if (related) {
71 + if (enableModernEventSystem) {
72 + // Due to the fact we don't add listeners to the document with the
73 + // modern event system and instead attach listeners to roots, we
74 + // need to handle the over event case. To ensure this, we just need to
75 + // make sure the node that we're coming from is managed by React.
76 + const inst = getClosestInstanceFromNode(related);
77 + if (inst !== null) {
78 + return null;
79 + }
80 + } else {
81 + // If this is an over event with a target, then we've already dispatched
82 + // the event in the out event of the other target. If this is replayed,
83 + // then it's because we couldn't dispatch against this target previously
84 + // so we have to do it now instead.
85 + return null;
86 + }
87 + }
88 }
89
90 if (!isOutEvent && !isOverEvent) {
@@ -163,11 +174,13 @@ const EnterLeaveEventPlugin = {
174
175 accumulateEnterLeaveDispatches(leave, enter, from, to);
176
166 - // If we are not processing the first ancestor, then we
167 - // should not process the same nativeEvent again, as we
168 - // will have already processed it in the first ancestor.
169 - if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) {
170 - return [leave];
177 + if (!enableModernEventSystem) {
178 + // If we are not processing the first ancestor, then we
179 + // should not process the same nativeEvent again, as we
180 + // will have already processed it in the first ancestor.
181 + if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) {
182 + return [leave];
183 + }
184 }
185
186 return [leave, enter];
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js new
+122
@@ -0,0 +1,122 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactFeatureFlags;
14 +let ReactDOM;
15 +
16 +function dispatchClickEvent(element) {
17 + const event = document.createEvent('Event');
18 + event.initEvent('click', true, true);
19 + element.dispatchEvent(event);
20 +}
21 +
22 +describe('DOMModernPluginEventSystem', () => {
23 + let container;
24 +
25 + beforeEach(() => {
26 + jest.resetModules();
27 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
28 + ReactFeatureFlags.enableModernEventSystem = true;
29 +
30 + React = require('react');
31 + ReactDOM = require('react-dom');
32 + container = document.createElement('div');
33 + document.body.appendChild(container);
34 + });
35 +
36 + afterEach(() => {
37 + document.body.removeChild(container);
38 + container = null;
39 + });
40 +
41 + it('handle propagation of click events', () => {
42 + const buttonRef = React.createRef();
43 + const divRef = React.createRef();
44 + const log = [];
45 + const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
46 + const onClickCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
47 +
48 + function Test() {
49 + return (
50 + <button
51 + ref={buttonRef}
52 + onClick={onClick}
53 + onClickCapture={onClickCapture}>
54 + <div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
55 + Click me!
56 + </div>
57 + </button>
58 + );
59 + }
60 +
61 + ReactDOM.render(<Test />, container);
62 +
63 + let buttonElement = buttonRef.current;
64 + dispatchClickEvent(buttonElement);
65 + expect(onClick).toHaveBeenCalledTimes(1);
66 + expect(onClickCapture).toHaveBeenCalledTimes(1);
67 + expect(log[0]).toEqual(['capture', buttonElement]);
68 + expect(log[1]).toEqual(['bubble', buttonElement]);
69 +
70 + let divElement = divRef.current;
71 + dispatchClickEvent(divElement);
72 + expect(onClick).toHaveBeenCalledTimes(3);
73 + expect(onClickCapture).toHaveBeenCalledTimes(3);
74 + expect(log[2]).toEqual(['capture', buttonElement]);
75 + expect(log[3]).toEqual(['capture', divElement]);
76 + expect(log[4]).toEqual(['bubble', divElement]);
77 + expect(log[5]).toEqual(['bubble', buttonElement]);
78 + });
79 +
80 + it('handle propagation of focus events', () => {
81 + const buttonRef = React.createRef();
82 + const divRef = React.createRef();
83 + const log = [];
84 + const onFocus = jest.fn(e => log.push(['bubble', e.currentTarget]));
85 + const onFocusCapture = jest.fn(e => log.push(['capture', e.currentTarget]));
86 +
87 + function Test() {
88 + return (
89 + <button
90 + ref={buttonRef}
91 + onFocus={onFocus}
92 + onFocusCapture={onFocusCapture}>
93 + <div
94 + ref={divRef}
95 + onFocus={onFocus}
96 + onFocusCapture={onFocusCapture}
97 + tabIndex={0}>
98 + Click me!
99 + </div>
100 + </button>
101 + );
102 + }
103 +
104 + ReactDOM.render(<Test />, container);
105 +
106 + let buttonElement = buttonRef.current;
107 + buttonElement.focus();
108 + expect(onFocus).toHaveBeenCalledTimes(1);
109 + expect(onFocusCapture).toHaveBeenCalledTimes(1);
110 + expect(log[0]).toEqual(['capture', buttonElement]);
111 + expect(log[1]).toEqual(['bubble', buttonElement]);
112 +
113 + let divElement = divRef.current;
114 + divElement.focus();
115 + expect(onFocus).toHaveBeenCalledTimes(3);
116 + expect(onFocusCapture).toHaveBeenCalledTimes(3);
117 + expect(log[2]).toEqual(['capture', buttonElement]);
118 + expect(log[3]).toEqual(['capture', divElement]);
119 + expect(log[4]).toEqual(['bubble', divElement]);
120 + expect(log[5]).toEqual(['bubble', buttonElement]);
121 + });
122 +});