mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Modern Event System: add plugin handling and forked paths (#18195)
This commit is contained in:
@@ -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');
|
||||
|
||||
|
||||
+64
-31
@@ -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(<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', () => {
|
||||
@@ -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 <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);
|
||||
} 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', {
|
||||
|
||||
+77
-28
@@ -11,6 +11,7 @@
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
|
||||
const ChildComponent = ({id, eventHandler}) => (
|
||||
<div
|
||||
@@ -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');
|
||||
// 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'],
|
||||
];
|
||||
const expectedCalls = [
|
||||
['P', 'mouseenter'],
|
||||
['P_P1', 'mouseenter'],
|
||||
['P_P1_C1__DIV', 'mouseenter'],
|
||||
];
|
||||
|
||||
outerNode1.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: enterNode,
|
||||
}),
|
||||
);
|
||||
enterNode.dispatchEvent(
|
||||
new MouseEvent('mouseover', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: outerNode1,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockFn.mock.calls).toEqual(expectedCalls);
|
||||
});
|
||||
expect(mockFn.mock.calls).toEqual(expectedCalls);
|
||||
});
|
||||
} else {
|
||||
it('should enter from the window', () => {
|
||||
const enterNode = document.getElementById('P_P1_C1__DIV');
|
||||
|
||||
it('should enter from the window to the shallowest', () => {
|
||||
const enterNode = document.getElementById('P');
|
||||
const expectedCalls = [
|
||||
['P', 'mouseenter'],
|
||||
['P_P1', 'mouseenter'],
|
||||
['P_P1_C1__DIV', 'mouseenter'],
|
||||
];
|
||||
|
||||
const expectedCalls = [['P', 'mouseenter']];
|
||||
outerNode1.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: enterNode,
|
||||
}),
|
||||
);
|
||||
|
||||
outerNode1.dispatchEvent(
|
||||
new MouseEvent('mouseout', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
relatedTarget: enterNode,
|
||||
}),
|
||||
);
|
||||
expect(mockFn.mock.calls).toEqual(expectedCalls);
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
+60
-1
@@ -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<ReactSyntheticEvent> = [];
|
||||
|
||||
for (let i = 0; i < plugins.length; i++) {
|
||||
const possiblePlugin: PluginModule<AnyNativeEvent> = 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<ReactSyntheticEvent>);
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+28
-15
@@ -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];
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={onClick}
|
||||
onClickCapture={onClickCapture}>
|
||||
<div ref={divRef} onClick={onClick} onClickCapture={onClickCapture}>
|
||||
Click me!
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, 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 (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onFocus={onFocus}
|
||||
onFocusCapture={onFocusCapture}>
|
||||
<div
|
||||
ref={divRef}
|
||||
onFocus={onFocus}
|
||||
onFocusCapture={onFocusCapture}
|
||||
tabIndex={0}>
|
||||
Click me!
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, 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]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user