Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from Mar 3, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -17,6 +17,7 @@ let ReactDOMComponentTree;
let listenToEvent;
let ReactDOMEventListener;
let ReactTestUtils;
let ReactFeatureFlags;

let idCallOrder;
const recordID = function(id) {
Expand Down Expand Up @@ -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');

Expand Down
95 changes: 64 additions & 31 deletions packages/react-dom/src/__tests__/ReactDOMEventListener-test.js
Expand Up @@ -12,36 +12,41 @@
describe('ReactDOMEventListener', () => {
let React;
let ReactDOM;
let ReactFeatureFlags = require('shared/ReactFeatureFlags');

beforeEach(() => {
jest.resetModules();
React = require('react');
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(<div onMouseEnter={mock} />, container);
const otherNode = document.createElement('h1');
document.body.appendChild(container);
document.body.appendChild(otherNode);
const container = document.createElement('div');
const node = ReactDOM.render(<div onMouseEnter={mock} />, 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', () => {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -362,13 +383,25 @@ describe('ReactDOMEventListener', () => {
bubbles: false,
}),
);
// Historically, we happened to not support onLoadStart
// on <img>, 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
// <img>. 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 <img>. 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dope

} else {
// Historically, we happened to not support onLoadStart
// on <img>, 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
// <img>. So far nobody asked us for it.
expect(handleImgLoadStart).toHaveBeenCalledTimes(0);
}

videoRef.current.dispatchEvent(
new ProgressEvent('loadstart', {
Expand Down
119 changes: 84 additions & 35 deletions packages/react-dom/src/__tests__/ReactTreeTraversal-test.js
Expand Up @@ -11,6 +11,7 @@

let React;
let ReactDOM;
let ReactFeatureFlags = require('shared/ReactFeatureFlags');

const ChildComponent = ({id, eventHandler}) => (
<div
Expand Down Expand Up @@ -203,41 +204,89 @@ describe('ReactTreeTraversal', () => {
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');
Expand Down
59 changes: 58 additions & 1 deletion packages/react-dom/src/events/DOMModernPluginEventSystem.js
Expand Up @@ -11,10 +11,15 @@ 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 {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,
Expand Down Expand Up @@ -87,6 +92,48 @@ 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 = [];

for (let i = 0; i < plugins.length; i++) {
const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
trueadm marked this conversation as resolved.
Show resolved Hide resolved
if (possiblePlugin) {
trueadm marked this conversation as resolved.
Show resolved Hide resolved
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
rootContainer,
);
if (extractedEvents) {
if (isArray(extractedEvents)) {
syntheticEvents.push(...(extractedEvents: any));
trueadm marked this conversation as resolved.
Show resolved Hide resolved
} else {
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,
Expand Down Expand Up @@ -123,5 +170,15 @@ export function dispatchEventForPluginEventSystem(
targetInst: null | Fiber,
rootContainer: Document | Element,
): void {
// TODO
let ancestorInst = targetInst;

batchedEventUpdates(() =>
dispatchEventsForPlugins(
topLevelType,
eventSystemFlags,
nativeEvent,
ancestorInst,
rootContainer,
),
);
}