Enable modern event system and delete dead code (#19230)

This commit is contained in:
Dan Abramov
2020-07-01 17:43:34 +01:00
committed by GitHub
parent e3f4eb7272
commit 9fba65efa5
52 changed files with 198 additions and 7148 deletions
+1 -4
View File
@@ -357,10 +357,7 @@ describe('ReactDOM', () => {
ReactDOM.render(<Wrapper />, container);
let expected;
if (
ReactFeatureFlags.enableModernEventSystem &
ReactFeatureFlags.enableLegacyFBSupport
) {
if (ReactFeatureFlags.enableLegacyFBSupport) {
// We expect to duplicate the 2nd handler because this test is
// not really designed around how the legacy FB support system works.
// This is because the above test sync fires a click() event
+1 -4
View File
@@ -2719,10 +2719,7 @@ describe('ReactDOMComponent', () => {
// might depend on this.
//
// @see https://github.com/facebook/react/pull/12919#issuecomment-395224674
if (
ReactFeatureFlags.enableModernEventSystem &
ReactFeatureFlags.enableLegacyFBSupport
) {
if (ReactFeatureFlags.enableLegacyFBSupport) {
// The order will change here, as the legacy FB support adds
// the event listener onto the document after the one above has.
expect(eventOrder).toEqual([
@@ -12,7 +12,6 @@
describe('ReactDOMEventListener', () => {
let React;
let ReactDOM;
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
beforeEach(() => {
jest.resetModules();
@@ -20,34 +19,6 @@ describe('ReactDOMEventListener', () => {
ReactDOM = require('react-dom');
});
// 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);
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', () => {
const mouseOut = jest.fn();
@@ -194,25 +165,19 @@ 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');
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.
// 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');
}
// 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');
// By the time we leave the handler, the second update is flushed.
expect(childNode.textContent).toBe('2');
} finally {
@@ -383,25 +348,15 @@ describe('ReactDOMEventListener', () => {
bubbles: false,
}),
);
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);
}
// 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);
videoRef.current.dispatchEvent(
new ProgressEvent('loadstart', {
-94
View File
@@ -15,8 +15,6 @@ import * as ReactDOM from 'react-dom';
import * as ReactDOMServer from 'react-dom/server';
import * as ReactTestUtils from 'react-dom/test-utils';
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
function getTestDocument(markup) {
const doc = document.implementation.createHTMLDocument('');
doc.open();
@@ -33,98 +31,6 @@ describe('ReactTestUtils', () => {
expect(Object.keys(ReactTestUtils.Simulate).sort()).toMatchSnapshot();
});
if (!ReactFeatureFlags.enableModernEventSystem) {
// SimulateNative API has been removed in the modern event system
it('SimulateNative should have locally attached media events', () => {
expect(Object.keys(ReactTestUtils.SimulateNative).sort()).toEqual([
'abort',
'animationEnd',
'animationIteration',
'animationStart',
'blur',
'canPlay',
'canPlayThrough',
'cancel',
'change',
'click',
'close',
'compositionEnd',
'compositionStart',
'compositionUpdate',
'contextMenu',
'copy',
'cut',
'doubleClick',
'drag',
'dragEnd',
'dragEnter',
'dragExit',
'dragLeave',
'dragOver',
'dragStart',
'drop',
'durationChange',
'emptied',
'encrypted',
'ended',
'error',
'focus',
'input',
'keyDown',
'keyPress',
'keyUp',
'load',
'loadStart',
'loadedData',
'loadedMetadata',
'mouseDown',
'mouseMove',
'mouseOut',
'mouseOver',
'mouseUp',
'paste',
'pause',
'play',
'playing',
'progress',
'rateChange',
'scroll',
'seeked',
'seeking',
'selectionChange',
'stalled',
'suspend',
'textInput',
'timeUpdate',
'toggle',
'touchCancel',
'touchEnd',
'touchMove',
'touchStart',
'transitionEnd',
'volumeChange',
'waiting',
'wheel',
]);
});
it('SimulateNative should warn about deprecation', () => {
const container = document.createElement('div');
const node = ReactDOM.render(<div />, container);
expect(() =>
ReactTestUtils.SimulateNative.click(node),
).toWarnDev(
'ReactTestUtils.SimulateNative is an undocumented API that does not match ' +
'how the browser dispatches events, and will be removed in a future major ' +
'version of React. If you rely on it for testing, consider attaching the root ' +
'DOM container to the document during the test, and then dispatching native browser ' +
'events by calling `node.dispatchEvent()` on the DOM nodes. Make sure to set ' +
'the `bubbles` flag to `true` when creating the native browser event.',
{withoutStack: true},
);
});
}
it('gives Jest mocks a passthrough implementation with mockComponent()', () => {
class MockedComponent extends React.Component {
render() {
+30 -76
View File
@@ -11,7 +11,6 @@
let React;
let ReactDOM;
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
const ChildComponent = ({id, eventHandler}) => (
<div
@@ -204,89 +203,44 @@ describe('ReactTreeTraversal', () => {
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
// The modern event system attaches event listeners to roots so 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');
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'],
];
enterNode.dispatchEvent(
new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
relatedTarget: outerNode1,
}),
);
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');
expect(mockFn.mock.calls).toEqual(expectedCalls);
});
const expectedCalls = [
['P', 'mouseenter'],
['P_P1', 'mouseenter'],
['P_P1_C1__DIV', 'mouseenter'],
];
it('should enter from the window to the shallowest', () => {
const enterNode = document.getElementById('P');
outerNode1.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: enterNode,
}),
);
const expectedCalls = [['P', 'mouseenter']];
expect(mockFn.mock.calls).toEqual(expectedCalls);
});
}
enterNode.dispatchEvent(
new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
relatedTarget: outerNode1,
}),
);
// 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);
});
}
expect(mockFn.mock.calls).toEqual(expectedCalls);
});
it('should leave to the window', () => {
const leaveNode = document.getElementById('P_P1_C1__DIV');
+9 -67
View File
@@ -5,19 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {setComponentTree} from '../legacy-events/EventPluginUtils';
import {
getFiberCurrentPropsFromNode,
getInstanceFromNode,
getNodeFromInstance,
} from './ReactDOMComponentTree';
import LegacyBeforeInputEventPlugin from '../events/plugins/LegacyBeforeInputEventPlugin';
import LegacyChangeEventPlugin from '../events/plugins/LegacyChangeEventPlugin';
import LegacyEnterLeaveEventPlugin from '../events/plugins/LegacyEnterLeaveEventPlugin';
import LegacySelectEventPlugin from '../events/plugins/LegacySelectEventPlugin';
import LegacySimpleEventPlugin from '../events/plugins/LegacySimpleEventPlugin';
// TODO: remove this injection altogether.
import ModernBeforeInputEventPlugin from '../events/plugins/ModernBeforeInputEventPlugin';
import ModernChangeEventPlugin from '../events/plugins/ModernChangeEventPlugin';
@@ -25,58 +13,12 @@ import ModernEnterLeaveEventPlugin from '../events/plugins/ModernEnterLeaveEvent
import ModernSelectEventPlugin from '../events/plugins/ModernSelectEventPlugin';
import ModernSimpleEventPlugin from '../events/plugins/ModernSimpleEventPlugin';
import {
injectEventPluginOrder,
injectEventPluginsByName,
injectEventPlugins,
} from '../legacy-events/EventPluginRegistry';
import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
import {injectEventPlugins} from '../legacy-events/EventPluginRegistry';
if (enableModernEventSystem) {
injectEventPlugins([
ModernSimpleEventPlugin,
ModernEnterLeaveEventPlugin,
ModernChangeEventPlugin,
ModernSelectEventPlugin,
ModernBeforeInputEventPlugin,
]);
} else {
/**
* Specifies a deterministic ordering of `EventPlugin`s. A convenient way to
* reason about plugins, without having to package every one of them. This
* is better than having plugins be ordered in the same order that they
* are injected because that ordering would be influenced by the packaging order.
* `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that
* preventing default on events is convenient in `SimpleEventPlugin` handlers.
*/
const DOMEventPluginOrder = [
'ResponderEventPlugin',
'SimpleEventPlugin',
'EnterLeaveEventPlugin',
'ChangeEventPlugin',
'SelectEventPlugin',
'BeforeInputEventPlugin',
];
/**
* Inject modules for resolving DOM hierarchy and plugin ordering.
*/
injectEventPluginOrder(DOMEventPluginOrder);
setComponentTree(
getFiberCurrentPropsFromNode,
getInstanceFromNode,
getNodeFromInstance,
);
/**
* Some important event plugins included by default (without having to require
* them).
*/
injectEventPluginsByName({
SimpleEventPlugin: LegacySimpleEventPlugin,
EnterLeaveEventPlugin: LegacyEnterLeaveEventPlugin,
ChangeEventPlugin: LegacyChangeEventPlugin,
SelectEventPlugin: LegacySelectEventPlugin,
BeforeInputEventPlugin: LegacyBeforeInputEventPlugin,
});
}
injectEventPlugins([
ModernSimpleEventPlugin,
ModernEnterLeaveEventPlugin,
ModernChangeEventPlugin,
ModernSelectEventPlugin,
ModernBeforeInputEventPlugin,
]);
+16 -123
View File
@@ -51,15 +51,6 @@ import {
import {track} from './inputValueTracking';
import setInnerHTML from './setInnerHTML';
import setTextContent from './setTextContent';
import {
TOP_ERROR,
TOP_INVALID,
TOP_LOAD,
TOP_RESET,
TOP_SUBMIT,
TOP_TOGGLE,
} from '../events/DOMTopLevelEventTypes';
import {mediaEventTypes} from '../events/DOMTopLevelEventTypes';
import {
createDangerousStringForStyles,
setValueForStyles,
@@ -74,7 +65,6 @@ import {
import assertValidProps from '../shared/assertValidProps';
import {
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
ELEMENT_NODE,
COMMENT_NODE,
} from '../shared/HTMLNodeType';
@@ -88,12 +78,7 @@ import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols';
import {
enableDeprecatedFlareAPI,
enableTrustedTypesIntegration,
enableModernEventSystem,
} from 'shared/ReactFeatureFlags';
import {
legacyListenToEvent,
legacyTrapBubbledEvent,
} from '../events/DOMLegacyEventPluginSystem';
import {listenToEvent} from '../events/DOMModernPluginEventSystem';
import {getEventListenerMap} from './ReactDOMComponentTree';
@@ -273,33 +258,22 @@ export function ensureListeningTo(
rootContainerInstance: Element | Node,
registrationName: string,
): void {
if (enableModernEventSystem) {
// If we have a comment node, then use the parent node,
// which should be an element.
const rootContainerElement =
rootContainerInstance.nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
: rootContainerInstance;
// Containers should only ever be element nodes. We do not
// want to register events to document fragments or documents
// with the modern plugin event system.
invariant(
rootContainerElement != null &&
rootContainerElement.nodeType === ELEMENT_NODE,
'ensureListeningTo(): received a container that was not an element node. ' +
'This is likely a bug in React.',
);
listenToEvent(registrationName, ((rootContainerElement: any): Element));
} else {
// Legacy plugin event system path
const isDocumentOrFragment =
rootContainerInstance.nodeType === DOCUMENT_NODE ||
rootContainerInstance.nodeType === DOCUMENT_FRAGMENT_NODE;
const doc = isDocumentOrFragment
? rootContainerInstance
: rootContainerInstance.ownerDocument;
legacyListenToEvent(registrationName, ((doc: any): Document));
}
// If we have a comment node, then use the parent node,
// which should be an element.
const rootContainerElement =
rootContainerInstance.nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
: rootContainerInstance;
// Containers should only ever be element nodes. We do not
// want to register events to document fragments or documents
// with the modern plugin event system.
invariant(
rootContainerElement != null &&
rootContainerElement.nodeType === ELEMENT_NODE,
'ensureListeningTo(): received a container that was not an element node. ' +
'This is likely a bug in React.',
);
listenToEvent(registrationName, ((rootContainerElement: any): Element));
}
function getOwnerDocumentFromRootContainer(
@@ -544,55 +518,29 @@ export function setInitialProperties(
case 'iframe':
case 'object':
case 'embed':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
props = rawProps;
break;
case 'video':
case 'audio':
if (!enableModernEventSystem) {
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
legacyTrapBubbledEvent(mediaEventTypes[i], domElement);
}
}
props = rawProps;
break;
case 'source':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
}
props = rawProps;
break;
case 'img':
case 'image':
case 'link':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
props = rawProps;
break;
case 'form':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_RESET, domElement);
legacyTrapBubbledEvent(TOP_SUBMIT, domElement);
}
props = rawProps;
break;
case 'details':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_TOGGLE, domElement);
}
props = rawProps;
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
props = ReactDOMInputGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
@@ -604,9 +552,6 @@ export function setInitialProperties(
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
props = ReactDOMSelectGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
@@ -614,9 +559,6 @@ export function setInitialProperties(
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
props = ReactDOMTextareaGetHostProps(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
@@ -949,51 +891,8 @@ export function diffHydratedProperties(
// TODO: Make sure that we check isMounted before firing any of these events.
switch (tag) {
case 'iframe':
case 'object':
case 'embed':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
break;
case 'video':
case 'audio':
if (!enableModernEventSystem) {
// Create listener for each media event
for (let i = 0; i < mediaEventTypes.length; i++) {
legacyTrapBubbledEvent(mediaEventTypes[i], domElement);
}
}
break;
case 'source':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
}
break;
case 'img':
case 'image':
case 'link':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_ERROR, domElement);
legacyTrapBubbledEvent(TOP_LOAD, domElement);
}
break;
case 'form':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_RESET, domElement);
legacyTrapBubbledEvent(TOP_SUBMIT, domElement);
}
break;
case 'details':
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_TOGGLE, domElement);
}
break;
case 'input':
ReactDOMInputInitWrapperState(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
@@ -1003,18 +902,12 @@ export function diffHydratedProperties(
break;
case 'select':
ReactDOMSelectInitWrapperState(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
break;
case 'textarea':
ReactDOMTextareaInitWrapperState(domElement, rawProps);
if (!enableModernEventSystem) {
legacyTrapBubbledEvent(TOP_INVALID, domElement);
}
// For controlled components we always need to ensure we're listening
// to onChange. Even if there is no listener.
ensureListeningTo(rootContainerElement, 'onChange');
+1 -4
View File
@@ -75,7 +75,6 @@ import {
enableSuspenseServerRenderer,
enableDeprecatedFlareAPI,
enableFundamentalAPI,
enableModernEventSystem,
enableCreateEventHandleAPI,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
@@ -1123,9 +1122,7 @@ export function makeOpaqueHydratingObject(
}
export function preparePortalMount(portalInstance: Instance): void {
if (enableModernEventSystem) {
listenToEvent('onMouseEnter', portalInstance);
}
listenToEvent('onMouseEnter', portalInstance);
}
export function prepareScopeUpdate(
-3
View File
@@ -57,8 +57,6 @@ import {
LegacyRoot,
} from 'react-reconciler/src/ReactRootTags';
import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
function ReactDOMRoot(container: Container, options: void | RootOptions) {
this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
}
@@ -144,7 +142,6 @@ function createRootImpl(
// hoist it to reduce code-size.
eagerlyTrapReplayableEvents(container, ((doc: any): Document));
} else if (
enableModernEventSystem &&
containerNodeType !== DOCUMENT_FRAGMENT_NODE &&
containerNodeType !== DOCUMENT_NODE
) {
@@ -1,622 +0,0 @@
/**
* 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.
*
* @flow
*/
import type {AnyNativeEvent} from '../legacy-events/PluginModuleType';
import type {DOMTopLevelEventType} from '../legacy-events/TopLevelEventTypes';
import type {ElementListenerMap} from '../client/ReactDOMComponentTree';
import type {EventSystemFlags} from './EventSystemFlags';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {LegacyPluginModule} from '../legacy-events/PluginModuleType';
import type {ReactSyntheticEvent} from '../legacy-events/ReactSyntheticEventType';
import type {TopLevelType} from '../legacy-events/TopLevelEventTypes';
import forEachAccumulated from '../legacy-events/forEachAccumulated';
import {
HostRoot,
HostComponent,
HostText,
} from 'react-reconciler/src/ReactWorkTags';
import {IS_FIRST_ANCESTOR, PLUGIN_EVENT_SYSTEM} from './EventSystemFlags';
import {runEventsInBatch} from '../legacy-events/EventBatching';
import {plugins} from '../legacy-events/EventPluginRegistry';
import accumulateInto from '../legacy-events/accumulateInto';
import {registrationNameDependencies} from '../legacy-events/EventPluginRegistry';
import getEventTarget from './getEventTarget';
import {
getClosestInstanceFromNode,
getEventListenerMap,
} from '../client/ReactDOMComponentTree';
import isEventSupported from './isEventSupported';
import {
TOP_BLUR,
TOP_CANCEL,
TOP_CLOSE,
TOP_FOCUS,
TOP_INVALID,
TOP_RESET,
TOP_SCROLL,
TOP_SUBMIT,
getRawEventName,
mediaEventTypes,
} from './DOMTopLevelEventTypes';
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
import {batchedEventUpdates} from './ReactDOMUpdateBatching';
import getListener from './getListener';
import {addEventCaptureListener, addEventBubbleListener} from './EventListener';
/**
* Summary of `DOMEventPluginSystem` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactDOMEventListener, which is injected and can therefore support
* pluggable event sources. This is the only work that occurs in the main
* thread.
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginRegistry`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - The `EventPluginRegistry` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - The `EventPluginRegistry` then dispatches the events.
*
* Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|PluginRegistry| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
const CALLBACK_BOOKKEEPING_POOL_SIZE = 10;
const callbackBookkeepingPool = [];
type BookKeepingInstance = {|
topLevelType: DOMTopLevelEventType | null,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent | null,
targetInst: Fiber | null,
ancestors: Array<Fiber | null>,
|};
function releaseTopLevelCallbackBookKeeping(
instance: BookKeepingInstance,
): void {
instance.topLevelType = null;
instance.nativeEvent = null;
instance.targetInst = null;
instance.ancestors.length = 0;
if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) {
callbackBookkeepingPool.push(instance);
}
}
// Used to store ancestor hierarchy in top level callback
function getTopLevelCallbackBookKeeping(
topLevelType: DOMTopLevelEventType,
nativeEvent: AnyNativeEvent,
targetInst: Fiber | null,
eventSystemFlags: EventSystemFlags,
): BookKeepingInstance {
if (callbackBookkeepingPool.length) {
const instance = callbackBookkeepingPool.pop();
instance.topLevelType = topLevelType;
instance.eventSystemFlags = eventSystemFlags;
instance.nativeEvent = nativeEvent;
instance.targetInst = targetInst;
return instance;
}
return {
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst,
ancestors: [],
};
}
/**
* Find the deepest React component completely containing the root of the
* passed-in instance (for use when entire React trees are nested within each
* other). If React trees are not nested, returns null.
*/
function findRootContainerNode(inst) {
if (inst.tag === HostRoot) {
return inst.stateNode.containerInfo;
}
// TODO: It may be a good idea to cache this to prevent unnecessary DOM
// traversal, but caching is difficult to do correctly without using a
// mutation observer to listen for all DOM changes.
while (inst.return) {
inst = inst.return;
}
if (inst.tag !== HostRoot) {
// This can happen if we're in a detached tree.
return null;
}
return inst.stateNode.containerInfo;
}
/**
* Allows registered plugins an opportunity to extract events from top-level
* native browser events.
*
* @return {*} An accumulation of synthetic events.
* @internal
*/
function extractPluginEvents(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
let events = null;
const legacyPlugins = ((plugins: any): Array<LegacyPluginModule<Event>>);
for (let i = 0; i < legacyPlugins.length; i++) {
// Not every plugin in the ordering may be loaded at runtime.
const possiblePlugin = legacyPlugins[i];
if (possiblePlugin) {
const extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
);
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
function runExtractedPluginEventsInBatch(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: null | EventTarget,
eventSystemFlags: EventSystemFlags,
) {
const events = extractPluginEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
);
runEventsInBatch(events);
}
function handleTopLevel(bookKeeping: BookKeepingInstance) {
let targetInst = bookKeeping.targetInst;
// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
let ancestor = targetInst;
do {
if (!ancestor) {
const ancestors = bookKeeping.ancestors;
((ancestors: any): Array<Fiber | null>).push(ancestor);
break;
}
const root = findRootContainerNode(ancestor);
if (!root) {
break;
}
const tag = ancestor.tag;
if (tag === HostComponent || tag === HostText) {
bookKeeping.ancestors.push(ancestor);
}
ancestor = getClosestInstanceFromNode(root);
} while (ancestor);
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
const eventTarget = getEventTarget(bookKeeping.nativeEvent);
const topLevelType = ((bookKeeping.topLevelType: any): DOMTopLevelEventType);
const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent);
let eventSystemFlags = bookKeeping.eventSystemFlags;
// If this is the first ancestor, we mark it on the system flags
if (i === 0) {
eventSystemFlags |= IS_FIRST_ANCESTOR;
}
runExtractedPluginEventsInBatch(
topLevelType,
targetInst,
nativeEvent,
eventTarget,
eventSystemFlags,
);
}
}
export function dispatchEventForLegacyPluginEventSystem(
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
nativeEvent: AnyNativeEvent,
targetInst: null | Fiber,
): void {
const bookKeeping = getTopLevelCallbackBookKeeping(
topLevelType,
nativeEvent,
targetInst,
eventSystemFlags,
);
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
batchedEventUpdates(handleTopLevel, bookKeeping);
} finally {
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
/**
* We listen for bubbled touch events on the document object.
*
* Firefox v8.01 (and possibly others) exhibited strange behavior when
* mounting `onmousemove` events at some node that was not the document
* element. The symptoms were that if your mouse is not moving over something
* contained within that mount point (for example on the background) the
* top-level listeners for `onmousemove` won't be called. However, if you
* register the `mousemove` on the document object, then it will of course
* catch all `mousemove`s. This along with iOS quirks, justifies restricting
* top-level listeners to the document object only, at least for these
* movement types of events and possibly all events.
*
* @see https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
*
* Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
* they bubble to document.
*
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @param {object} mountAt Container where to mount the listener
*/
export function legacyListenToEvent(
registrationName: string,
mountAt: Document | Element,
): void {
const listenerMap = getEventListenerMap(mountAt);
const dependencies = registrationNameDependencies[registrationName];
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
legacyListenToTopLevelEvent(dependency, mountAt, listenerMap);
}
}
export function legacyListenToTopLevelEvent(
topLevelType: DOMTopLevelEventType,
mountAt: Document | Element,
listenerMap: ElementListenerMap,
): void {
if (!listenerMap.has(topLevelType)) {
switch (topLevelType) {
case TOP_SCROLL: {
legacyTrapCapturedEvent(TOP_SCROLL, mountAt, listenerMap);
break;
}
case TOP_FOCUS:
case TOP_BLUR:
legacyTrapCapturedEvent(TOP_FOCUS, mountAt, listenerMap);
legacyTrapCapturedEvent(TOP_BLUR, mountAt, listenerMap);
break;
case TOP_CANCEL:
case TOP_CLOSE: {
if (isEventSupported(getRawEventName(topLevelType))) {
legacyTrapCapturedEvent(topLevelType, mountAt, listenerMap);
}
break;
}
case TOP_INVALID:
case TOP_SUBMIT:
case TOP_RESET:
// We listen to them on the target DOM elements.
// Some of them bubble so we don't want them to fire twice.
break;
default: {
// By default, listen on the top level to all non-media events.
// Media events don't bubble so adding the listener wouldn't do anything.
const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1;
if (!isMediaEvent) {
legacyTrapBubbledEvent(topLevelType, mountAt, listenerMap);
}
break;
}
}
}
}
export function legacyTrapBubbledEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
listenerMap?: ElementListenerMap,
): void {
const listener = addTrappedEventListener(
element,
topLevelType,
PLUGIN_EVENT_SYSTEM,
false,
);
if (listenerMap) {
listenerMap.set(topLevelType, {passive: undefined, listener});
}
}
export function legacyTrapCapturedEvent(
topLevelType: DOMTopLevelEventType,
element: Document | Element,
listenerMap: ElementListenerMap,
): void {
const listener = addTrappedEventListener(
element,
topLevelType,
PLUGIN_EVENT_SYSTEM,
true,
);
listenerMap.set(topLevelType, {passive: undefined, listener});
}
function addTrappedEventListener(
targetContainer: EventTarget,
topLevelType: DOMTopLevelEventType,
eventSystemFlags: EventSystemFlags,
capture: boolean,
): any => void {
const rawEventName = getRawEventName(topLevelType);
const listener = createEventListenerWrapperWithPriority(
targetContainer,
topLevelType,
eventSystemFlags,
);
const unsubscribeListener = capture
? addEventCaptureListener(targetContainer, rawEventName, listener)
: addEventBubbleListener(targetContainer, rawEventName, listener);
return unsubscribeListener;
}
function getParent(inst: Object | null): Object | null {
if (!inst) {
return null;
}
do {
inst = inst.return;
// TODO: If this is a HostRoot we might want to bail out.
// That is depending on if we want nested subtrees (layers) to bubble
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
} while (inst && inst.tag !== HostComponent);
if (inst) {
return inst;
}
return null;
}
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
function traverseTwoPhase(
inst: Object,
fn: Function,
arg: ReactSyntheticEvent,
) {
const path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
let i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
function listenerAtPhase(inst, event, propagationPhase) {
const registrationName =
event.dispatchConfig.phasedRegistrationNames[propagationPhase];
return getListener(inst, registrationName);
}
/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
export function getLowestCommonAncestor(
instA: Object,
instB: Object,
): Object | null {
let depthA = 0;
for (let tempA = instA; tempA; tempA = getParent(tempA)) {
depthA++;
}
let depthB = 0;
for (let tempB = instB; tempB; tempB = getParent(tempB)) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
instA = getParent(instA);
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
instB = getParent(instB);
depthB--;
}
// Walk in lockstep until we find a match.
let depth = depthA;
while (depth--) {
if (instA === instB || instA === instB.alternate) {
return instA;
}
instA = getParent(instA);
instB = getParent(instB);
}
return null;
}
/**
* Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that
* should would receive a `mouseEnter` or `mouseLeave` event.
*
* Does not invoke the callback on the nearest common ancestor because nothing
* "entered" or "left" that element.
*/
export function traverseEnterLeave(
from: Object,
to: Object,
fn: Function,
argFrom: ReactSyntheticEvent,
argTo: ReactSyntheticEvent,
) {
const common = from && to ? getLowestCommonAncestor(from, to) : null;
const pathFrom = [];
while (true) {
if (!from) {
break;
}
if (from === common) {
break;
}
const alternate = from.alternate;
if (alternate !== null && alternate === common) {
break;
}
pathFrom.push(from);
from = getParent(from);
}
const pathTo = [];
while (true) {
if (!to) {
break;
}
if (to === common) {
break;
}
const alternate = to.alternate;
if (alternate !== null && alternate === common) {
break;
}
pathTo.push(to);
to = getParent(to);
}
for (let i = 0; i < pathFrom.length; i++) {
fn(pathFrom[i], 'bubbled', argFrom);
}
for (let i = pathTo.length; i-- > 0; ) {
fn(pathTo[i], 'captured', argTo);
}
}
function accumulateDirectionalDispatches(inst, phase, event) {
if (__DEV__) {
if (!inst) {
console.error('Dispatching inst must not be null');
}
}
const listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
}
/**
* Accumulates without regard to direction, does not look for phased
* registration names. Same as `accumulateDirectDispatchesSingle` but without
* requiring that the `dispatchMarker` be the same as the dispatched ID.
*/
function accumulateDispatches(
inst: Object,
ignoredDirection: ?boolean,
event: Object,
): void {
if (inst && event && event.dispatchConfig.registrationName) {
const registrationName = event.dispatchConfig.registrationName;
const listener = getListener(inst, registrationName);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
}
export function accumulateTwoPhaseDispatches(
events: ReactSyntheticEvent | Array<ReactSyntheticEvent>,
): void {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
export function accumulateEnterLeaveDispatches(
leave: ReactSyntheticEvent,
enter: ReactSyntheticEvent,
from: Fiber,
to: Fiber,
) {
traverseEnterLeave(from, to, accumulateDispatches, leave, enter);
}
+28 -66
View File
@@ -43,7 +43,6 @@ import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import {
enableDeprecatedFlareAPI,
enableModernEventSystem,
enableLegacyFBSupport,
} from 'shared/ReactFeatureFlags';
import {
@@ -52,7 +51,6 @@ import {
DiscreteEvent,
} from 'shared/ReactTypes';
import {getEventPriorityForPluginSystem} from './DOMEventProperties';
import {dispatchEventForLegacyPluginEventSystem} from './DOMLegacyEventPluginSystem';
import {dispatchEventForPluginEventSystem} from './DOMModernPluginEventSystem';
import {
flushDiscreteUpdatesIfNeeded,
@@ -230,22 +228,13 @@ export function dispatchEvent(
// in case the event system needs to trace it.
if (enableDeprecatedFlareAPI) {
if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
if (enableModernEventSystem) {
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
} else {
dispatchEventForLegacyPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
);
}
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) {
// React Flare event system
@@ -258,22 +247,13 @@ export function dispatchEvent(
);
}
} else {
if (enableModernEventSystem) {
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
} else {
dispatchEventForLegacyPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
);
}
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
null,
targetContainer,
);
}
}
@@ -329,22 +309,13 @@ export function attemptToDispatchEvent(
if (enableDeprecatedFlareAPI) {
if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
if (enableModernEventSystem) {
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
} else {
dispatchEventForLegacyPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst,
);
}
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
}
if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) {
// React Flare event system
@@ -357,22 +328,13 @@ export function attemptToDispatchEvent(
);
}
} else {
if (enableModernEventSystem) {
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
} else {
dispatchEventForLegacyPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst,
);
}
dispatchEventForPluginEventSystem(
topLevelType,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
}
// We're not blocked on anything.
return null;
+11 -23
View File
@@ -17,7 +17,6 @@ import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import {
enableDeprecatedFlareAPI,
enableSelectiveHydration,
enableModernEventSystem,
} from 'shared/ReactFeatureFlags';
import {
unstable_runWithPriority as runWithPriority,
@@ -116,7 +115,6 @@ import {
TOP_BLUR,
} from './DOMTopLevelEventTypes';
import {IS_REPLAYED, PLUGIN_EVENT_SYSTEM} from './EventSystemFlags';
import {legacyListenToTopLevelEvent} from './DOMLegacyEventPluginSystem';
import {listenToTopLevelEvent} from './DOMModernPluginEventSystem';
import {addResponderEventSystemEvent} from './DeprecatedDOMEventResponderSystem';
@@ -230,9 +228,6 @@ function trapReplayableEventForDocument(
document: Document,
listenerMap: ElementListenerMap,
) {
if (!enableModernEventSystem) {
legacyListenToTopLevelEvent(topLevelType, document, listenerMap);
}
if (enableDeprecatedFlareAPI) {
// Trap events for the responder system.
const topLevelTypeString = unsafeCastDOMTopLevelTypeToString(topLevelType);
@@ -257,30 +252,23 @@ export function eagerlyTrapReplayableEvents(
document: Document,
) {
const listenerMapForDoc = getEventListenerMap(document);
let listenerMapForContainer;
if (enableModernEventSystem) {
listenerMapForContainer = getEventListenerMap(container);
}
const listenerMapForContainer = getEventListenerMap(container);
// Discrete
discreteReplayableEvents.forEach(topLevelType => {
if (enableModernEventSystem) {
trapReplayableEventForContainer(
topLevelType,
container,
listenerMapForContainer,
);
}
trapReplayableEventForContainer(
topLevelType,
container,
listenerMapForContainer,
);
trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc);
});
// Continuous
continuousReplayableEvents.forEach(topLevelType => {
if (enableModernEventSystem) {
trapReplayableEventForContainer(
topLevelType,
container,
listenerMapForContainer,
);
}
trapReplayableEventForContainer(
topLevelType,
container,
listenerMapForContainer,
);
trapReplayableEventForDocument(topLevelType, document, listenerMapForDoc);
});
}
@@ -60,7 +60,6 @@ describe('DOMModernPluginEventSystem', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableModernEventSystem = true;
ReactFeatureFlags.enableLegacyFBSupport = enableLegacyFBSupport;
React = require('react');
@@ -1197,7 +1196,6 @@ describe('DOMModernPluginEventSystem', () => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableLegacyFBSupport = enableLegacyFBSupport;
ReactFeatureFlags.enableModernEventSystem = true;
ReactFeatureFlags.enableCreateEventHandleAPI = true;
React = require('react');
@@ -2555,7 +2553,6 @@ describe('DOMModernPluginEventSystem', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableModernEventSystem = true;
ReactFeatureFlags.enableCreateEventHandleAPI = true;
ReactFeatureFlags.enableScopeAPI = true;
@@ -11,7 +11,6 @@
let React;
let ReactDOM;
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
describe('SyntheticClipboardEvent', () => {
let container;
@@ -117,44 +116,5 @@ describe('SyntheticClipboardEvent', () => {
expect(expectedCount).toBe(3);
});
if (!ReactFeatureFlags.enableModernEventSystem) {
it('is able to `persist`', () => {
const persistentEvents = [];
const eventHandler = event => {
expect(event.isPersistent()).toBe(false);
event.persist();
expect(event.isPersistent()).toBe(true);
persistentEvents.push(event);
};
const div = ReactDOM.render(
<div
onCopy={eventHandler}
onCut={eventHandler}
onPaste={eventHandler}
/>,
container,
);
let event;
event = document.createEvent('Event');
event.initEvent('copy', true, true);
div.dispatchEvent(event);
event = document.createEvent('Event');
event.initEvent('cut', true, true);
div.dispatchEvent(event);
event = document.createEvent('Event');
event.initEvent('paste', true, true);
div.dispatchEvent(event);
expect(persistentEvents.length).toBe(3);
expect(persistentEvents[0].type).toBe('copy');
expect(persistentEvents[1].type).toBe('cut');
expect(persistentEvents[2].type).toBe('paste');
});
}
});
});
@@ -11,7 +11,6 @@
let React;
let ReactDOM;
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
describe('SyntheticEvent', () => {
let container;
@@ -99,256 +98,4 @@ describe('SyntheticEvent', () => {
expect(expectedCount).toBe(1);
});
if (!ReactFeatureFlags.enableModernEventSystem) {
it('should be able to `persist`', () => {
let expectedCount = 0;
let syntheticEvent;
const eventHandler = e => {
expect(e.isPersistent()).toBe(false);
e.persist();
syntheticEvent = e;
expect(e.isPersistent()).toBe(true);
expectedCount++;
};
const node = ReactDOM.render(<div onClick={eventHandler} />, container);
const event = document.createEvent('Event');
event.initEvent('click', true, true);
node.dispatchEvent(event);
expect(syntheticEvent.type).toBe('click');
expect(syntheticEvent.bubbles).toBe(true);
expect(syntheticEvent.cancelable).toBe(true);
expect(expectedCount).toBe(1);
});
it('should be nullified and log warnings if the synthetic event has not been persisted', () => {
let expectedCount = 0;
let syntheticEvent;
const eventHandler = e => {
syntheticEvent = e;
expectedCount++;
};
const node = ReactDOM.render(<div onClick={eventHandler} />, container);
const event = document.createEvent('Event');
event.initEvent('click', true, true);
node.dispatchEvent(event);
const getExpectedWarning = property =>
'Warning: This synthetic event is reused for performance reasons. If ' +
`you're seeing this, you're accessing the property \`${property}\` on a ` +
'released/nullified synthetic event. This is set to null. If you must ' +
'keep the original synthetic event around, use event.persist(). ' +
'See https://fb.me/react-event-pooling for more information.';
// once for each property accessed
expect(() =>
expect(syntheticEvent.type).toBe(null),
).toErrorDev(getExpectedWarning('type'), {withoutStack: true});
expect(() =>
expect(syntheticEvent.nativeEvent).toBe(null),
).toErrorDev(getExpectedWarning('nativeEvent'), {withoutStack: true});
expect(() =>
expect(syntheticEvent.target).toBe(null),
).toErrorDev(getExpectedWarning('target'), {withoutStack: true});
expect(expectedCount).toBe(1);
});
it('should warn when setting properties of a synthetic event that has not been persisted', () => {
let expectedCount = 0;
let syntheticEvent;
const eventHandler = e => {
syntheticEvent = e;
expectedCount++;
};
const node = ReactDOM.render(<div onClick={eventHandler} />, container);
const event = document.createEvent('Event');
event.initEvent('click', true, true);
node.dispatchEvent(event);
expect(() => {
syntheticEvent.type = 'MouseEvent';
}).toErrorDev(
'Warning: This synthetic event is reused for performance reasons. If ' +
"you're seeing this, you're setting the property `type` on a " +
'released/nullified synthetic event. This is effectively a no-op. If you must ' +
'keep the original synthetic event around, use event.persist(). ' +
'See https://fb.me/react-event-pooling for more information.',
{withoutStack: true},
);
expect(expectedCount).toBe(1);
});
it('should warn when calling `preventDefault` if the synthetic event has not been persisted', () => {
let expectedCount = 0;
let syntheticEvent;
const eventHandler = e => {
syntheticEvent = e;
expectedCount++;
};
const node = ReactDOM.render(<div onClick={eventHandler} />, container);
const event = document.createEvent('Event');
event.initEvent('click', true, true);
node.dispatchEvent(event);
expect(() =>
syntheticEvent.preventDefault(),
).toErrorDev(
'Warning: This synthetic event is reused for performance reasons. If ' +
"you're seeing this, you're accessing the method `preventDefault` on a " +
'released/nullified synthetic event. This is a no-op function. If you must ' +
'keep the original synthetic event around, use event.persist(). ' +
'See https://fb.me/react-event-pooling for more information.',
{withoutStack: true},
);
expect(expectedCount).toBe(1);
});
it('should warn when calling `stopPropagation` if the synthetic event has not been persisted', () => {
let expectedCount = 0;
let syntheticEvent;
const eventHandler = e => {
syntheticEvent = e;
expectedCount++;
};
const node = ReactDOM.render(<div onClick={eventHandler} />, container);
const event = document.createEvent('Event');
event.initEvent('click', true, true);
node.dispatchEvent(event);
expect(() =>
syntheticEvent.stopPropagation(),
).toErrorDev(
'Warning: This synthetic event is reused for performance reasons. If ' +
"you're seeing this, you're accessing the method `stopPropagation` on a " +
'released/nullified synthetic event. This is a no-op function. If you must ' +
'keep the original synthetic event around, use event.persist(). ' +
'See https://fb.me/react-event-pooling for more information.',
{withoutStack: true},
);
expect(expectedCount).toBe(1);
});
it('should warn when calling `isPropagationStopped` if the synthetic event has not been persisted', () => {
let expectedCount = 0;
let syntheticEvent;
const eventHandler = e => {
syntheticEvent = e;
expectedCount++;
};
const node = ReactDOM.render(<div onClick={eventHandler} />, container);
const event = document.createEvent('Event');
event.initEvent('click', true, true);
node.dispatchEvent(event);
expect(() =>
expect(syntheticEvent.isPropagationStopped()).toBe(false),
).toErrorDev(
'Warning: This synthetic event is reused for performance reasons. If ' +
"you're seeing this, you're accessing the method `isPropagationStopped` on a " +
'released/nullified synthetic event. This is a no-op function. If you must ' +
'keep the original synthetic event around, use event.persist(). ' +
'See https://fb.me/react-event-pooling for more information.',
{withoutStack: true},
);
expect(expectedCount).toBe(1);
});
it('should warn when calling `isDefaultPrevented` if the synthetic event has not been persisted', () => {
let expectedCount = 0;
let syntheticEvent;
const eventHandler = e => {
syntheticEvent = e;
expectedCount++;
};
const node = ReactDOM.render(<div onClick={eventHandler} />, container);
const event = document.createEvent('Event');
event.initEvent('click', true, true);
node.dispatchEvent(event);
expect(() =>
expect(syntheticEvent.isDefaultPrevented()).toBe(false),
).toErrorDev(
'Warning: This synthetic event is reused for performance reasons. If ' +
"you're seeing this, you're accessing the method `isDefaultPrevented` on a " +
'released/nullified synthetic event. This is a no-op function. If you must ' +
'keep the original synthetic event around, use event.persist(). ' +
'See https://fb.me/react-event-pooling for more information.',
{withoutStack: true},
);
expect(expectedCount).toBe(1);
});
it('should properly log warnings when events simulated with rendered components', () => {
let event;
function assignEvent(e) {
event = e;
}
const node = ReactDOM.render(<div onClick={assignEvent} />, container);
node.click();
// access a property to cause the warning
expect(() => {
event.nativeEvent; // eslint-disable-line no-unused-expressions
}).toErrorDev(
'Warning: This synthetic event is reused for performance reasons. If ' +
"you're seeing this, you're accessing the property `nativeEvent` on a " +
'released/nullified synthetic event. This is set to null. If you must ' +
'keep the original synthetic event around, use event.persist(). ' +
'See https://fb.me/react-event-pooling for more information.',
{withoutStack: true},
);
});
}
// TODO: we might want to re-add a warning like this in the future,
// but it shouldn't use Proxies because they make debugging difficult.
// Or we might disallow this pattern altogether:
// https://github.com/facebook/react/issues/13224
xit('should warn if a property is added to the synthetic event', () => {
let expectedCount = 0;
let syntheticEvent;
const eventHandler = e => {
expect(() => {
e.foo = 'bar';
}).toErrorDev(
'Warning: This synthetic event is reused for performance reasons. If ' +
"you're seeing this, you're adding a new property in the synthetic " +
'event object. The property is never released. ' +
'See https://fb.me/react-event-pooling for more information.',
{withoutStack: true},
);
syntheticEvent = e;
expectedCount++;
};
const node = ReactDOM.render(<div onClick={eventHandler} />, container);
const event = document.createEvent('Event');
event.initEvent('click', true, true);
node.dispatchEvent(event);
expect(syntheticEvent.foo).toBe('bar');
expect(expectedCount).toBe(1);
});
});
@@ -11,7 +11,6 @@
let React;
let ReactDOM;
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
describe('SyntheticKeyboardEvent', () => {
let container;
@@ -547,52 +546,5 @@ describe('SyntheticKeyboardEvent', () => {
);
expect(expectedCount).toBe(3);
});
if (!ReactFeatureFlags.enableModernEventSystem) {
it('is able to `persist`', () => {
const persistentEvents = [];
const eventHandler = event => {
expect(event.isPersistent()).toBe(false);
event.persist();
expect(event.isPersistent()).toBe(true);
persistentEvents.push(event);
};
const div = ReactDOM.render(
<div
onKeyDown={eventHandler}
onKeyUp={eventHandler}
onKeyPress={eventHandler}
/>,
container,
);
div.dispatchEvent(
new KeyboardEvent('keydown', {
keyCode: 40,
bubbles: true,
cancelable: true,
}),
);
div.dispatchEvent(
new KeyboardEvent('keyup', {
keyCode: 40,
bubbles: true,
cancelable: true,
}),
);
div.dispatchEvent(
new KeyboardEvent('keypress', {
charCode: 40,
keyCode: 40,
bubbles: true,
cancelable: true,
}),
);
expect(persistentEvents.length).toBe(3);
expect(persistentEvents[0].type).toBe('keydown');
expect(persistentEvents[1].type).toBe('keyup');
expect(persistentEvents[2].type).toBe('keypress');
});
}
});
});
@@ -11,7 +11,6 @@
let React;
let ReactDOM;
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
describe('SyntheticWheelEvent', () => {
let container;
@@ -113,26 +112,4 @@ describe('SyntheticWheelEvent', () => {
expect(events.length).toBe(2);
});
if (!ReactFeatureFlags.enableModernEventSystem) {
it('should be able to `persist`', () => {
const events = [];
const onWheel = event => {
expect(event.isPersistent()).toBe(false);
event.persist();
expect(event.isPersistent()).toBe(true);
events.push(event);
};
ReactDOM.render(<div onWheel={onWheel} />, container);
container.firstChild.dispatchEvent(
new MouseEvent('wheel', {
bubbles: true,
}),
);
expect(events.length).toBe(1);
expect(events[0].type).toBe('wheel');
});
}
});
@@ -1,498 +0,0 @@
/**
* 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.
*/
import type {TopLevelType} from '../../legacy-events/TopLevelEventTypes';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import {
TOP_BLUR,
TOP_COMPOSITION_START,
TOP_COMPOSITION_END,
TOP_COMPOSITION_UPDATE,
TOP_KEY_DOWN,
TOP_KEY_PRESS,
TOP_KEY_UP,
TOP_MOUSE_DOWN,
TOP_TEXT_INPUT,
TOP_PASTE,
} from '../DOMTopLevelEventTypes';
import {
getData as FallbackCompositionStateGetData,
initialize as FallbackCompositionStateInitialize,
reset as FallbackCompositionStateReset,
} from '../FallbackCompositionState';
import SyntheticCompositionEvent from '../SyntheticCompositionEvent';
import SyntheticInputEvent from '../SyntheticInputEvent';
import {accumulateTwoPhaseDispatches} from '../DOMLegacyEventPluginSystem';
const END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space
const START_KEYCODE = 229;
const canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window;
let documentMode = null;
if (canUseDOM && 'documentMode' in document) {
documentMode = document.documentMode;
}
// Webkit offers a very useful `textInput` event that can be used to
// directly represent `beforeInput`. The IE `textinput` event is not as
// useful, so we don't use it.
const canUseTextInputEvent =
canUseDOM && 'TextEvent' in window && !documentMode;
// In IE9+, we have access to composition events, but the data supplied
// by the native compositionend event may be incorrect. Japanese ideographic
// spaces, for instance (\u3000) are not recorded correctly.
const useFallbackCompositionData =
canUseDOM &&
(!canUseCompositionEvent ||
(documentMode && documentMode > 8 && documentMode <= 11));
const SPACEBAR_CODE = 32;
const SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE);
// Events and their corresponding property names.
const eventTypes = {
beforeInput: {
phasedRegistrationNames: {
bubbled: 'onBeforeInput',
captured: 'onBeforeInputCapture',
},
dependencies: [
TOP_COMPOSITION_END,
TOP_KEY_PRESS,
TOP_TEXT_INPUT,
TOP_PASTE,
],
},
compositionEnd: {
phasedRegistrationNames: {
bubbled: 'onCompositionEnd',
captured: 'onCompositionEndCapture',
},
dependencies: [
TOP_BLUR,
TOP_COMPOSITION_END,
TOP_KEY_DOWN,
TOP_KEY_PRESS,
TOP_KEY_UP,
TOP_MOUSE_DOWN,
],
},
compositionStart: {
phasedRegistrationNames: {
bubbled: 'onCompositionStart',
captured: 'onCompositionStartCapture',
},
dependencies: [
TOP_BLUR,
TOP_COMPOSITION_START,
TOP_KEY_DOWN,
TOP_KEY_PRESS,
TOP_KEY_UP,
TOP_MOUSE_DOWN,
],
},
compositionUpdate: {
phasedRegistrationNames: {
bubbled: 'onCompositionUpdate',
captured: 'onCompositionUpdateCapture',
},
dependencies: [
TOP_BLUR,
TOP_COMPOSITION_UPDATE,
TOP_KEY_DOWN,
TOP_KEY_PRESS,
TOP_KEY_UP,
TOP_MOUSE_DOWN,
],
},
};
// Track whether we've ever handled a keypress on the space key.
let hasSpaceKeypress = false;
/**
* Return whether a native keypress event is assumed to be a command.
* This is required because Firefox fires `keypress` events for key commands
* (cut, copy, select-all, etc.) even though no character is inserted.
*/
function isKeypressCommand(nativeEvent) {
return (
(nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) &&
// ctrlKey && altKey is equivalent to AltGr, and is not a command.
!(nativeEvent.ctrlKey && nativeEvent.altKey)
);
}
/**
* Translate native top level events into event types.
*
* @param {string} topLevelType
* @return {object}
*/
function getCompositionEventType(topLevelType) {
switch (topLevelType) {
case TOP_COMPOSITION_START:
return eventTypes.compositionStart;
case TOP_COMPOSITION_END:
return eventTypes.compositionEnd;
case TOP_COMPOSITION_UPDATE:
return eventTypes.compositionUpdate;
}
}
/**
* Does our fallback best-guess model think this event signifies that
* composition has begun?
*
* @param {string} topLevelType
* @param {object} nativeEvent
* @return {boolean}
*/
function isFallbackCompositionStart(topLevelType, nativeEvent) {
return topLevelType === TOP_KEY_DOWN && nativeEvent.keyCode === START_KEYCODE;
}
/**
* Does our fallback mode think that this event is the end of composition?
*
* @param {string} topLevelType
* @param {object} nativeEvent
* @return {boolean}
*/
function isFallbackCompositionEnd(topLevelType, nativeEvent) {
switch (topLevelType) {
case TOP_KEY_UP:
// Command keys insert or clear IME input.
return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
case TOP_KEY_DOWN:
// Expect IME keyCode on each keydown. If we get any other
// code we must have exited earlier.
return nativeEvent.keyCode !== START_KEYCODE;
case TOP_KEY_PRESS:
case TOP_MOUSE_DOWN:
case TOP_BLUR:
// Events are not possible without cancelling IME.
return true;
default:
return false;
}
}
/**
* Google Input Tools provides composition data via a CustomEvent,
* with the `data` property populated in the `detail` object. If this
* is available on the event object, use it. If not, this is a plain
* composition event and we have nothing special to extract.
*
* @param {object} nativeEvent
* @return {?string}
*/
function getDataFromCustomEvent(nativeEvent) {
const detail = nativeEvent.detail;
if (typeof detail === 'object' && 'data' in detail) {
return detail.data;
}
return null;
}
/**
* Check if a composition event was triggered by Korean IME.
* Our fallback mode does not work well with IE's Korean IME,
* so just use native composition events when Korean IME is used.
* Although CompositionEvent.locale property is deprecated,
* it is available in IE, where our fallback mode is enabled.
*
* @param {object} nativeEvent
* @return {boolean}
*/
function isUsingKoreanIME(nativeEvent) {
return nativeEvent.locale === 'ko';
}
// Track the current IME composition status, if any.
let isComposing = false;
/**
* @return {?object} A SyntheticCompositionEvent.
*/
function extractCompositionEvent(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
let eventType;
let fallbackData;
if (canUseCompositionEvent) {
eventType = getCompositionEventType(topLevelType);
} else if (!isComposing) {
if (isFallbackCompositionStart(topLevelType, nativeEvent)) {
eventType = eventTypes.compositionStart;
}
} else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) {
eventType = eventTypes.compositionEnd;
}
if (!eventType) {
return null;
}
if (useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)) {
// The current composition is stored statically and must not be
// overwritten while composition continues.
if (!isComposing && eventType === eventTypes.compositionStart) {
isComposing = FallbackCompositionStateInitialize(nativeEventTarget);
} else if (eventType === eventTypes.compositionEnd) {
if (isComposing) {
fallbackData = FallbackCompositionStateGetData();
}
}
}
const event = SyntheticCompositionEvent.getPooled(
eventType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (fallbackData) {
// Inject data generated from fallback path into the synthetic event.
// This matches the property of native CompositionEventInterface.
event.data = fallbackData;
} else {
const customData = getDataFromCustomEvent(nativeEvent);
if (customData !== null) {
event.data = customData;
}
}
accumulateTwoPhaseDispatches(event);
return event;
}
/**
* @param {TopLevelType} topLevelType Number from `TopLevelType`.
* @param {object} nativeEvent Native browser event.
* @return {?string} The string corresponding to this `beforeInput` event.
*/
function getNativeBeforeInputChars(topLevelType: TopLevelType, nativeEvent) {
switch (topLevelType) {
case TOP_COMPOSITION_END:
return getDataFromCustomEvent(nativeEvent);
case TOP_KEY_PRESS:
/**
* If native `textInput` events are available, our goal is to make
* use of them. However, there is a special case: the spacebar key.
* In Webkit, preventing default on a spacebar `textInput` event
* cancels character insertion, but it *also* causes the browser
* to fall back to its default spacebar behavior of scrolling the
* page.
*
* Tracking at:
* https://code.google.com/p/chromium/issues/detail?id=355103
*
* To avoid this issue, use the keypress event as if no `textInput`
* event is available.
*/
const which = nativeEvent.which;
if (which !== SPACEBAR_CODE) {
return null;
}
hasSpaceKeypress = true;
return SPACEBAR_CHAR;
case TOP_TEXT_INPUT:
// Record the characters to be added to the DOM.
const chars = nativeEvent.data;
// If it's a spacebar character, assume that we have already handled
// it at the keypress level and bail immediately. Android Chrome
// doesn't give us keycodes, so we need to ignore it.
if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
return null;
}
return chars;
default:
// For other native event types, do nothing.
return null;
}
}
/**
* For browsers that do not provide the `textInput` event, extract the
* appropriate string to use for SyntheticInputEvent.
*
* @param {number} topLevelType Number from `TopLevelEventTypes`.
* @param {object} nativeEvent Native browser event.
* @return {?string} The fallback string for this `beforeInput` event.
*/
function getFallbackBeforeInputChars(topLevelType: TopLevelType, nativeEvent) {
// If we are currently composing (IME) and using a fallback to do so,
// try to extract the composed characters from the fallback object.
// If composition event is available, we extract a string only at
// compositionevent, otherwise extract it at fallback events.
if (isComposing) {
if (
topLevelType === TOP_COMPOSITION_END ||
(!canUseCompositionEvent &&
isFallbackCompositionEnd(topLevelType, nativeEvent))
) {
const chars = FallbackCompositionStateGetData();
FallbackCompositionStateReset();
isComposing = false;
return chars;
}
return null;
}
switch (topLevelType) {
case TOP_PASTE:
// If a paste event occurs after a keypress, throw out the input
// chars. Paste events should not lead to BeforeInput events.
return null;
case TOP_KEY_PRESS:
/**
* As of v27, Firefox may fire keypress events even when no character
* will be inserted. A few possibilities:
*
* - `which` is `0`. Arrow keys, Esc key, etc.
*
* - `which` is the pressed key code, but no char is available.
* Ex: 'AltGr + d` in Polish. There is no modified character for
* this key combination and no character is inserted into the
* document, but FF fires the keypress for char code `100` anyway.
* No `input` event will occur.
*
* - `which` is the pressed key code, but a command combination is
* being used. Ex: `Cmd+C`. No character is inserted, and no
* `input` event will occur.
*/
if (!isKeypressCommand(nativeEvent)) {
// IE fires the `keypress` event when a user types an emoji via
// Touch keyboard of Windows. In such a case, the `char` property
// holds an emoji character like `\uD83D\uDE0A`. Because its length
// is 2, the property `which` does not represent an emoji correctly.
// In such a case, we directly return the `char` property instead of
// using `which`.
if (nativeEvent.char && nativeEvent.char.length > 1) {
return nativeEvent.char;
} else if (nativeEvent.which) {
return String.fromCharCode(nativeEvent.which);
}
}
return null;
case TOP_COMPOSITION_END:
return useFallbackCompositionData && !isUsingKoreanIME(nativeEvent)
? null
: nativeEvent.data;
default:
return null;
}
}
/**
* Extract a SyntheticInputEvent for `beforeInput`, based on either native
* `textInput` or fallback behavior.
*
* @return {?object} A SyntheticInputEvent.
*/
function extractBeforeInputEvent(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
let chars;
if (canUseTextInputEvent) {
chars = getNativeBeforeInputChars(topLevelType, nativeEvent);
} else {
chars = getFallbackBeforeInputChars(topLevelType, nativeEvent);
}
// If no characters are being inserted, no BeforeInput event should
// be fired.
if (!chars) {
return null;
}
const event = SyntheticInputEvent.getPooled(
eventTypes.beforeInput,
targetInst,
nativeEvent,
nativeEventTarget,
);
event.data = chars;
accumulateTwoPhaseDispatches(event);
return event;
}
/**
* Create an `onBeforeInput` event to match
* http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents.
*
* This event plugin is based on the native `textInput` event
* available in Chrome, Safari, Opera, and IE. This event fires after
* `onKeyPress` and `onCompositionEnd`, but before `onInput`.
*
* `beforeInput` is spec'd but not implemented in any browsers, and
* the `input` event does not provide any useful information about what has
* actually been added, contrary to the spec. Thus, `textInput` is the best
* available event to identify the characters that have actually been inserted
* into the target node.
*
* This plugin is also responsible for emitting `composition` events, thus
* allowing us to share composition fallback code for both `beforeInput` and
* `composition` event types.
*/
const BeforeInputEventPlugin = {
eventTypes: eventTypes,
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
) {
const composition = extractCompositionEvent(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
const beforeInput = extractBeforeInputEvent(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (composition === null) {
return beforeInput;
}
if (beforeInput === null) {
return composition;
}
return [composition, beforeInput];
},
};
export default BeforeInputEventPlugin;
@@ -1,310 +0,0 @@
/**
* 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.
*/
import {runEventsInBatch} from '../../legacy-events/EventBatching';
import SyntheticEvent from '../../legacy-events/SyntheticEvent';
import isTextInputElement from '../isTextInputElement';
import {canUseDOM} from 'shared/ExecutionEnvironment';
import {
TOP_BLUR,
TOP_CHANGE,
TOP_CLICK,
TOP_FOCUS,
TOP_INPUT,
TOP_KEY_DOWN,
TOP_KEY_UP,
TOP_SELECTION_CHANGE,
} from '../DOMTopLevelEventTypes';
import getEventTarget from '../getEventTarget';
import isEventSupported from '../isEventSupported';
import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
import {updateValueIfChanged} from '../../client/inputValueTracking';
import {setDefaultValue} from '../../client/ReactDOMInput';
import {enqueueStateRestore} from '../ReactDOMControlledComponent';
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
import {batchedUpdates} from '../ReactDOMUpdateBatching';
import {accumulateTwoPhaseDispatches} from '../DOMLegacyEventPluginSystem';
const eventTypes = {
change: {
phasedRegistrationNames: {
bubbled: 'onChange',
captured: 'onChangeCapture',
},
dependencies: [
TOP_BLUR,
TOP_CHANGE,
TOP_CLICK,
TOP_FOCUS,
TOP_INPUT,
TOP_KEY_DOWN,
TOP_KEY_UP,
TOP_SELECTION_CHANGE,
],
},
};
function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
const event = SyntheticEvent.getPooled(
eventTypes.change,
inst,
nativeEvent,
target,
);
event.type = 'change';
// Flag this event loop as needing state restore.
enqueueStateRestore(target);
accumulateTwoPhaseDispatches(event);
return event;
}
/**
* For IE shims
*/
let activeElement = null;
let activeElementInst = null;
/**
* SECTION: handle `change` event
*/
function shouldUseChangeEvent(elem) {
const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
return (
nodeName === 'select' || (nodeName === 'input' && elem.type === 'file')
);
}
function manualDispatchChangeEvent(nativeEvent) {
const event = createAndAccumulateChangeEvent(
activeElementInst,
nativeEvent,
getEventTarget(nativeEvent),
);
// If change and propertychange bubbled, we'd just bind to it like all the
// other events and have it go through ReactBrowserEventEmitter. Since it
// doesn't, we manually listen for the events and so we have to enqueue and
// process the abstract event manually.
//
// Batching is necessary here in order to ensure that all event handlers run
// before the next rerender (including event handlers attached to ancestor
// elements instead of directly on the input). Without this, controlled
// components don't work properly in conjunction with event bubbling because
// the component is rerendered and the value reverted before all the event
// handlers can run. See https://github.com/facebook/react/issues/708.
batchedUpdates(runEventInBatch, event);
}
function runEventInBatch(event) {
runEventsInBatch(event);
}
function getInstIfValueChanged(targetInst) {
const targetNode = getNodeFromInstance(targetInst);
if (updateValueIfChanged(targetNode)) {
return targetInst;
}
}
function getTargetInstForChangeEvent(topLevelType, targetInst) {
if (topLevelType === TOP_CHANGE) {
return targetInst;
}
}
/**
* SECTION: handle `input` event
*/
let isInputEventSupported = false;
if (canUseDOM) {
// IE9 claims to support the input event but fails to trigger it when
// deleting text, so we ignore its input events.
isInputEventSupported =
isEventSupported('input') &&
(!document.documentMode || document.documentMode > 9);
}
/**
* (For IE <=9) Starts tracking propertychange events on the passed-in element
* and override the value property so that we can distinguish user events from
* value changes in JS.
*/
function startWatchingForValueChange(target, targetInst) {
activeElement = target;
activeElementInst = targetInst;
activeElement.attachEvent('onpropertychange', handlePropertyChange);
}
/**
* (For IE <=9) Removes the event listeners from the currently-tracked element,
* if any exists.
*/
function stopWatchingForValueChange() {
if (!activeElement) {
return;
}
activeElement.detachEvent('onpropertychange', handlePropertyChange);
activeElement = null;
activeElementInst = null;
}
/**
* (For IE <=9) Handles a propertychange event, sending a `change` event if
* the value of the active element has changed.
*/
function handlePropertyChange(nativeEvent) {
if (nativeEvent.propertyName !== 'value') {
return;
}
if (getInstIfValueChanged(activeElementInst)) {
manualDispatchChangeEvent(nativeEvent);
}
}
function handleEventsForInputEventPolyfill(topLevelType, target, targetInst) {
if (topLevelType === TOP_FOCUS) {
// In IE9, propertychange fires for most input events but is buggy and
// doesn't fire when text is deleted, but conveniently, selectionchange
// appears to fire in all of the remaining cases so we catch those and
// forward the event if the value has changed
// In either case, we don't want to call the event handler if the value
// is changed from JS so we redefine a setter for `.value` that updates
// our activeElementValue variable, allowing us to ignore those changes
//
// stopWatching() should be a noop here but we call it just in case we
// missed a blur event somehow.
stopWatchingForValueChange();
startWatchingForValueChange(target, targetInst);
} else if (topLevelType === TOP_BLUR) {
stopWatchingForValueChange();
}
}
// For IE8 and IE9.
function getTargetInstForInputEventPolyfill(topLevelType, targetInst) {
if (
topLevelType === TOP_SELECTION_CHANGE ||
topLevelType === TOP_KEY_UP ||
topLevelType === TOP_KEY_DOWN
) {
// On the selectionchange event, the target is just document which isn't
// helpful for us so just check activeElement instead.
//
// 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
// propertychange on the first input event after setting `value` from a
// script and fires only keydown, keypress, keyup. Catching keyup usually
// gets it and catching keydown lets us fire an event for the first
// keystroke if user does a key repeat (it'll be a little delayed: right
// before the second keystroke). Other input methods (e.g., paste) seem to
// fire selectionchange normally.
return getInstIfValueChanged(activeElementInst);
}
}
/**
* SECTION: handle `click` event
*/
function shouldUseClickEvent(elem) {
// Use the `click` event to detect changes to checkbox and radio inputs.
// This approach works across all browsers, whereas `change` does not fire
// until `blur` in IE8.
const nodeName = elem.nodeName;
return (
nodeName &&
nodeName.toLowerCase() === 'input' &&
(elem.type === 'checkbox' || elem.type === 'radio')
);
}
function getTargetInstForClickEvent(topLevelType, targetInst) {
if (topLevelType === TOP_CLICK) {
return getInstIfValueChanged(targetInst);
}
}
function getTargetInstForInputOrChangeEvent(topLevelType, targetInst) {
if (topLevelType === TOP_INPUT || topLevelType === TOP_CHANGE) {
return getInstIfValueChanged(targetInst);
}
}
function handleControlledInputBlur(node) {
const state = node._wrapperState;
if (!state || !state.controlled || node.type !== 'number') {
return;
}
if (!disableInputAttributeSyncing) {
// If controlled, assign the value attribute to the current value on blur
setDefaultValue(node, 'number', node.value);
}
}
/**
* This plugin creates an `onChange` event that normalizes change events
* across form elements. This event fires at a time when it's possible to
* change the element's value without seeing a flicker.
*
* Supported elements are:
* - input (see `isTextInputElement`)
* - textarea
* - select
*/
const ChangeEventPlugin = {
eventTypes: eventTypes,
_isInputEventSupported: isInputEventSupported,
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
) {
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
let getTargetInstFunc, handleEventFunc;
if (shouldUseChangeEvent(targetNode)) {
getTargetInstFunc = getTargetInstForChangeEvent;
} else if (isTextInputElement(targetNode)) {
if (isInputEventSupported) {
getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else {
getTargetInstFunc = getTargetInstForInputEventPolyfill;
handleEventFunc = handleEventsForInputEventPolyfill;
}
} else if (shouldUseClickEvent(targetNode)) {
getTargetInstFunc = getTargetInstForClickEvent;
}
if (getTargetInstFunc) {
const inst = getTargetInstFunc(topLevelType, targetInst);
if (inst) {
const event = createAndAccumulateChangeEvent(
inst,
nativeEvent,
nativeEventTarget,
);
return event;
}
}
if (handleEventFunc) {
handleEventFunc(topLevelType, targetNode, targetInst);
}
// When blurring, set the value attribute for number inputs
if (topLevelType === TOP_BLUR) {
handleControlledInputBlur(targetNode);
}
},
};
export default ChangeEventPlugin;
@@ -1,179 +0,0 @@
/**
* 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.
*/
import {
TOP_MOUSE_OUT,
TOP_MOUSE_OVER,
TOP_POINTER_OUT,
TOP_POINTER_OVER,
} from '../DOMTopLevelEventTypes';
import {
IS_REPLAYED,
IS_FIRST_ANCESTOR,
} from 'react-dom/src/events/EventSystemFlags';
import SyntheticMouseEvent from '../SyntheticMouseEvent';
import SyntheticPointerEvent from '../SyntheticPointerEvent';
import {
getClosestInstanceFromNode,
getNodeFromInstance,
} from '../../client/ReactDOMComponentTree';
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
import {getNearestMountedFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
import {accumulateEnterLeaveDispatches} from '../DOMLegacyEventPluginSystem';
const eventTypes = {
mouseEnter: {
registrationName: 'onMouseEnter',
dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER],
},
mouseLeave: {
registrationName: 'onMouseLeave',
dependencies: [TOP_MOUSE_OUT, TOP_MOUSE_OVER],
},
pointerEnter: {
registrationName: 'onPointerEnter',
dependencies: [TOP_POINTER_OUT, TOP_POINTER_OVER],
},
pointerLeave: {
registrationName: 'onPointerLeave',
dependencies: [TOP_POINTER_OUT, TOP_POINTER_OVER],
},
};
const EnterLeaveEventPlugin = {
eventTypes: eventTypes,
/**
* For almost every interaction we care about, there will be both a top-level
* `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that
* we do not extract duplicate events. However, moving the mouse into the
* browser from outside will not fire a `mouseout` event. In this case, we use
* the `mouseover` top-level event.
*/
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
) {
const isOverEvent =
topLevelType === TOP_MOUSE_OVER || topLevelType === TOP_POINTER_OVER;
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 (!isOutEvent && !isOverEvent) {
// Must not be a mouse or pointer in or out - ignoring.
return null;
}
let win;
if (nativeEventTarget.window === nativeEventTarget) {
// `nativeEventTarget` is probably a window object.
win = nativeEventTarget;
} else {
// TODO: Figure out why `ownerDocument` is sometimes undefined in IE8.
const doc = nativeEventTarget.ownerDocument;
if (doc) {
win = doc.defaultView || doc.parentWindow;
} else {
win = window;
}
}
let from;
let to;
if (isOutEvent) {
from = targetInst;
const related = nativeEvent.relatedTarget || nativeEvent.toElement;
to = related ? getClosestInstanceFromNode(related) : null;
if (to !== null) {
const nearestMounted = getNearestMountedFiber(to);
if (
to !== nearestMounted ||
(to.tag !== HostComponent && to.tag !== HostText)
) {
to = null;
}
}
} else {
// Moving to a node from outside the window.
from = null;
to = targetInst;
}
if (from === to) {
// Nothing pertains to our managed components.
return null;
}
let eventInterface, leaveEventType, enterEventType, eventTypePrefix;
if (topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_MOUSE_OVER) {
eventInterface = SyntheticMouseEvent;
leaveEventType = eventTypes.mouseLeave;
enterEventType = eventTypes.mouseEnter;
eventTypePrefix = 'mouse';
} else if (
topLevelType === TOP_POINTER_OUT ||
topLevelType === TOP_POINTER_OVER
) {
eventInterface = SyntheticPointerEvent;
leaveEventType = eventTypes.pointerLeave;
enterEventType = eventTypes.pointerEnter;
eventTypePrefix = 'pointer';
}
const fromNode = from == null ? win : getNodeFromInstance(from);
const toNode = to == null ? win : getNodeFromInstance(to);
const leave = eventInterface.getPooled(
leaveEventType,
from,
nativeEvent,
nativeEventTarget,
);
leave.type = eventTypePrefix + 'leave';
leave.target = fromNode;
leave.relatedTarget = toNode;
const enter = eventInterface.getPooled(
enterEventType,
to,
nativeEvent,
nativeEventTarget,
);
enter.type = eventTypePrefix + 'enter';
enter.target = toNode;
enter.relatedTarget = fromNode;
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];
}
return [leave, enter];
},
};
export default EnterLeaveEventPlugin;
@@ -1,248 +0,0 @@
/**
* 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.
*/
import {canUseDOM} from 'shared/ExecutionEnvironment';
import SyntheticEvent from '../../legacy-events/SyntheticEvent';
import isTextInputElement from '../isTextInputElement';
import shallowEqual from 'shared/shallowEqual';
import {
TOP_BLUR,
TOP_CONTEXT_MENU,
TOP_DRAG_END,
TOP_FOCUS,
TOP_KEY_DOWN,
TOP_KEY_UP,
TOP_MOUSE_DOWN,
TOP_MOUSE_UP,
TOP_SELECTION_CHANGE,
} from '../DOMTopLevelEventTypes';
import getActiveElement from '../../client/getActiveElement';
import {
getNodeFromInstance,
getEventListenerMap,
} from '../../client/ReactDOMComponentTree';
import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
import {DOCUMENT_NODE} from '../../shared/HTMLNodeType';
import {accumulateTwoPhaseDispatches} from '../DOMLegacyEventPluginSystem';
import {registrationNameDependencies} from '../../legacy-events/EventPluginRegistry';
const skipSelectionChangeEvent =
canUseDOM && 'documentMode' in document && document.documentMode <= 11;
const eventTypes = {
select: {
phasedRegistrationNames: {
bubbled: 'onSelect',
captured: 'onSelectCapture',
},
dependencies: [
TOP_BLUR,
TOP_CONTEXT_MENU,
TOP_DRAG_END,
TOP_FOCUS,
TOP_KEY_DOWN,
TOP_KEY_UP,
TOP_MOUSE_DOWN,
TOP_MOUSE_UP,
TOP_SELECTION_CHANGE,
],
},
};
let activeElement = null;
let activeElementInst = null;
let lastSelection = null;
let mouseDown = false;
/**
* Get an object which is a unique representation of the current selection.
*
* The return value will not be consistent across nodes or browsers, but
* two identical selections on the same node will return identical objects.
*
* @param {DOMElement} node
* @return {object}
*/
function getSelection(node) {
if ('selectionStart' in node && hasSelectionCapabilities(node)) {
return {
start: node.selectionStart,
end: node.selectionEnd,
};
} else {
const win =
(node.ownerDocument && node.ownerDocument.defaultView) || window;
const selection = win.getSelection();
return {
anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode,
focusOffset: selection.focusOffset,
};
}
}
/**
* Get document associated with the event target.
*
* @param {object} nativeEventTarget
* @return {Document}
*/
function getEventTargetDocument(eventTarget) {
return eventTarget.window === eventTarget
? eventTarget.document
: eventTarget.nodeType === DOCUMENT_NODE
? eventTarget
: eventTarget.ownerDocument;
}
/**
* Poll selection to see whether it's changed.
*
* @param {object} nativeEvent
* @param {object} nativeEventTarget
* @return {?SyntheticEvent}
*/
function constructSelectEvent(nativeEvent, nativeEventTarget) {
// Ensure we have the right element, and that the user is not dragging a
// selection (this matches native `select` event behavior). In HTML5, select
// fires only on input and textarea thus if there's no focused element we
// won't dispatch.
const doc = getEventTargetDocument(nativeEventTarget);
if (
mouseDown ||
activeElement == null ||
activeElement !== getActiveElement(doc)
) {
return null;
}
// Only fire when selection has actually changed.
const currentSelection = getSelection(activeElement);
if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
lastSelection = currentSelection;
const syntheticEvent = SyntheticEvent.getPooled(
eventTypes.select,
activeElementInst,
nativeEvent,
nativeEventTarget,
);
syntheticEvent.type = 'select';
syntheticEvent.target = activeElement;
accumulateTwoPhaseDispatches(syntheticEvent);
return syntheticEvent;
}
return null;
}
function isListeningToAllDependencies(
registrationName: string,
mountAt: Document | Element,
): boolean {
const dependencies = registrationNameDependencies[registrationName];
const listenerMap = getEventListenerMap(mountAt);
for (let i = 0; i < dependencies.length; i++) {
const event = dependencies[i];
if (!listenerMap.has(event)) {
return false;
}
}
return true;
}
/**
* This plugin creates an `onSelect` event that normalizes select events
* across form elements.
*
* Supported elements are:
* - input (see `isTextInputElement`)
* - textarea
* - contentEditable
*
* This differs from native browser implementations in the following ways:
* - Fires on contentEditable fields as well as inputs.
* - Fires for collapsed selection.
* - Fires after user input.
*/
const SelectEventPlugin = {
eventTypes: eventTypes,
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
) {
const doc = getEventTargetDocument(nativeEventTarget);
// Track whether all listeners exists for this plugin. If none exist, we do
// not extract events. See #3639.
if (!doc || !isListeningToAllDependencies('onSelect', doc)) {
return null;
}
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
switch (topLevelType) {
// Track the input node that has focus.
case TOP_FOCUS:
if (
isTextInputElement(targetNode) ||
targetNode.contentEditable === 'true'
) {
activeElement = targetNode;
activeElementInst = targetInst;
lastSelection = null;
}
break;
case TOP_BLUR:
activeElement = null;
activeElementInst = null;
lastSelection = null;
break;
// Don't fire the event while the user is dragging. This matches the
// semantics of the native select event.
case TOP_MOUSE_DOWN:
mouseDown = true;
break;
case TOP_CONTEXT_MENU:
case TOP_MOUSE_UP:
case TOP_DRAG_END:
mouseDown = false;
return constructSelectEvent(nativeEvent, nativeEventTarget);
// Chrome and IE fire non-standard event when selection is changed (and
// sometimes when it hasn't). IE's event fires out of order with respect
// to key and input events on deletion, so we discard it.
//
// Firefox doesn't support selectionchange, so check selection status
// after each key entry. The selection changes after keydown and before
// keyup, but we check on keydown as well in the case of holding down a
// key, when multiple keydown events are fired but only one keyup is.
// This is also our approach for IE handling, for the reason above.
case TOP_SELECTION_CHANGE:
if (skipSelectionChangeEvent) {
break;
}
// falls through
case TOP_KEY_DOWN:
case TOP_KEY_UP:
return constructSelectEvent(nativeEvent, nativeEventTarget);
}
return null;
},
};
export default SelectEventPlugin;
@@ -1,197 +0,0 @@
/**
* 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.
*
* @flow
*/
import type {
TopLevelType,
DOMTopLevelEventType,
} from '../../legacy-events/TopLevelEventTypes';
import type {ReactSyntheticEvent} from '../../legacy-events/ReactSyntheticEventType';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {LegacyPluginModule} from '../../legacy-events/PluginModuleType';
import SyntheticEvent from '../../legacy-events/SyntheticEvent';
import * as DOMTopLevelEventTypes from '../DOMTopLevelEventTypes';
import {
topLevelEventsToDispatchConfig,
simpleEventPluginEventTypes,
} from '../DOMEventProperties';
import SyntheticAnimationEvent from '../SyntheticAnimationEvent';
import SyntheticClipboardEvent from '../SyntheticClipboardEvent';
import SyntheticFocusEvent from '../SyntheticFocusEvent';
import SyntheticKeyboardEvent from '../SyntheticKeyboardEvent';
import SyntheticMouseEvent from '../SyntheticMouseEvent';
import SyntheticPointerEvent from '../SyntheticPointerEvent';
import SyntheticDragEvent from '../SyntheticDragEvent';
import SyntheticTouchEvent from '../SyntheticTouchEvent';
import SyntheticTransitionEvent from '../SyntheticTransitionEvent';
import SyntheticUIEvent from '../SyntheticUIEvent';
import SyntheticWheelEvent from '../SyntheticWheelEvent';
import getEventCharCode from '../getEventCharCode';
import {accumulateTwoPhaseDispatches} from '../DOMLegacyEventPluginSystem';
// Only used in DEV for exhaustiveness validation.
const knownHTMLTopLevelTypes: Array<DOMTopLevelEventType> = [
DOMTopLevelEventTypes.TOP_ABORT,
DOMTopLevelEventTypes.TOP_CANCEL,
DOMTopLevelEventTypes.TOP_CAN_PLAY,
DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH,
DOMTopLevelEventTypes.TOP_CLOSE,
DOMTopLevelEventTypes.TOP_DURATION_CHANGE,
DOMTopLevelEventTypes.TOP_EMPTIED,
DOMTopLevelEventTypes.TOP_ENCRYPTED,
DOMTopLevelEventTypes.TOP_ENDED,
DOMTopLevelEventTypes.TOP_ERROR,
DOMTopLevelEventTypes.TOP_INPUT,
DOMTopLevelEventTypes.TOP_INVALID,
DOMTopLevelEventTypes.TOP_LOAD,
DOMTopLevelEventTypes.TOP_LOADED_DATA,
DOMTopLevelEventTypes.TOP_LOADED_METADATA,
DOMTopLevelEventTypes.TOP_LOAD_START,
DOMTopLevelEventTypes.TOP_PAUSE,
DOMTopLevelEventTypes.TOP_PLAY,
DOMTopLevelEventTypes.TOP_PLAYING,
DOMTopLevelEventTypes.TOP_PROGRESS,
DOMTopLevelEventTypes.TOP_RATE_CHANGE,
DOMTopLevelEventTypes.TOP_RESET,
DOMTopLevelEventTypes.TOP_SEEKED,
DOMTopLevelEventTypes.TOP_SEEKING,
DOMTopLevelEventTypes.TOP_STALLED,
DOMTopLevelEventTypes.TOP_SUBMIT,
DOMTopLevelEventTypes.TOP_SUSPEND,
DOMTopLevelEventTypes.TOP_TIME_UPDATE,
DOMTopLevelEventTypes.TOP_TOGGLE,
DOMTopLevelEventTypes.TOP_VOLUME_CHANGE,
DOMTopLevelEventTypes.TOP_WAITING,
];
const SimpleEventPlugin: LegacyPluginModule<MouseEvent> = {
// simpleEventPluginEventTypes gets populated from
// the DOMEventProperties module.
eventTypes: simpleEventPluginEventTypes,
extractEvents: function(
topLevelType: TopLevelType,
targetInst: null | Fiber,
nativeEvent: MouseEvent,
nativeEventTarget: null | EventTarget,
): null | ReactSyntheticEvent {
const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);
if (!dispatchConfig) {
return null;
}
let EventConstructor;
switch (topLevelType) {
case DOMTopLevelEventTypes.TOP_KEY_PRESS:
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
if (getEventCharCode(nativeEvent) === 0) {
return null;
}
/* falls through */
case DOMTopLevelEventTypes.TOP_KEY_DOWN:
case DOMTopLevelEventTypes.TOP_KEY_UP:
EventConstructor = SyntheticKeyboardEvent;
break;
case DOMTopLevelEventTypes.TOP_BLUR:
case DOMTopLevelEventTypes.TOP_FOCUS:
EventConstructor = SyntheticFocusEvent;
break;
case DOMTopLevelEventTypes.TOP_CLICK:
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return null;
}
/* falls through */
case DOMTopLevelEventTypes.TOP_AUX_CLICK:
case DOMTopLevelEventTypes.TOP_DOUBLE_CLICK:
case DOMTopLevelEventTypes.TOP_MOUSE_DOWN:
case DOMTopLevelEventTypes.TOP_MOUSE_MOVE:
case DOMTopLevelEventTypes.TOP_MOUSE_UP:
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case DOMTopLevelEventTypes.TOP_MOUSE_OUT:
case DOMTopLevelEventTypes.TOP_MOUSE_OVER:
case DOMTopLevelEventTypes.TOP_CONTEXT_MENU:
EventConstructor = SyntheticMouseEvent;
break;
case DOMTopLevelEventTypes.TOP_DRAG:
case DOMTopLevelEventTypes.TOP_DRAG_END:
case DOMTopLevelEventTypes.TOP_DRAG_ENTER:
case DOMTopLevelEventTypes.TOP_DRAG_EXIT:
case DOMTopLevelEventTypes.TOP_DRAG_LEAVE:
case DOMTopLevelEventTypes.TOP_DRAG_OVER:
case DOMTopLevelEventTypes.TOP_DRAG_START:
case DOMTopLevelEventTypes.TOP_DROP:
EventConstructor = SyntheticDragEvent;
break;
case DOMTopLevelEventTypes.TOP_TOUCH_CANCEL:
case DOMTopLevelEventTypes.TOP_TOUCH_END:
case DOMTopLevelEventTypes.TOP_TOUCH_MOVE:
case DOMTopLevelEventTypes.TOP_TOUCH_START:
EventConstructor = SyntheticTouchEvent;
break;
case DOMTopLevelEventTypes.TOP_ANIMATION_END:
case DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION:
case DOMTopLevelEventTypes.TOP_ANIMATION_START:
EventConstructor = SyntheticAnimationEvent;
break;
case DOMTopLevelEventTypes.TOP_TRANSITION_END:
EventConstructor = SyntheticTransitionEvent;
break;
case DOMTopLevelEventTypes.TOP_SCROLL:
EventConstructor = SyntheticUIEvent;
break;
case DOMTopLevelEventTypes.TOP_WHEEL:
EventConstructor = SyntheticWheelEvent;
break;
case DOMTopLevelEventTypes.TOP_COPY:
case DOMTopLevelEventTypes.TOP_CUT:
case DOMTopLevelEventTypes.TOP_PASTE:
EventConstructor = SyntheticClipboardEvent;
break;
case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE:
case DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE:
case DOMTopLevelEventTypes.TOP_POINTER_CANCEL:
case DOMTopLevelEventTypes.TOP_POINTER_DOWN:
case DOMTopLevelEventTypes.TOP_POINTER_MOVE:
case DOMTopLevelEventTypes.TOP_POINTER_OUT:
case DOMTopLevelEventTypes.TOP_POINTER_OVER:
case DOMTopLevelEventTypes.TOP_POINTER_UP:
EventConstructor = SyntheticPointerEvent;
break;
default:
if (__DEV__) {
if (knownHTMLTopLevelTypes.indexOf(topLevelType) === -1) {
console.error(
'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' +
'is likely caused by a bug in React. Please file an issue.',
topLevelType,
);
}
}
// HTML Events
// @see http://www.w3.org/TR/html5/index.html#events-0
EventConstructor = SyntheticEvent;
break;
}
const event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
accumulateTwoPhaseDispatches(event);
return event;
},
};
export default SimpleEventPlugin;
@@ -1,763 +0,0 @@
/**
* 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 = require('react');
let ReactDOM = require('react-dom');
let TestUtils = require('react-dom/test-utils');
let ReactFeatureFlags;
let Scheduler;
const setUntrackedChecked = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'checked',
).set;
const setUntrackedValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).set;
const setUntrackedTextareaValue = Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
'value',
).set;
describe('ChangeEventPlugin', () => {
let container;
beforeEach(() => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
// TODO pull this into helper method, reduce repetition.
// mock the browser APIs which are used in schedule:
// - calling 'window.postMessage' should actually fire postmessage handlers
const originalAddEventListener = global.addEventListener;
let postMessageCallback;
global.addEventListener = function(eventName, callback, useCapture) {
if (eventName === 'message') {
postMessageCallback = callback;
} else {
originalAddEventListener(eventName, callback, useCapture);
}
};
global.postMessage = function(messageKey, targetOrigin) {
const postMessageEvent = {source: window, data: messageKey};
if (postMessageCallback) {
postMessageCallback(postMessageEvent);
}
};
jest.resetModules();
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
// We try to avoid firing "duplicate" React change events.
// However, to tell which events are "duplicates" and should be ignored,
// we are tracking the "current" input value, and only respect events
// that occur after it changes. In most of these tests, we verify that we
// keep track of the "current" value and only fire events when it changes.
// See https://github.com/facebook/react/pull/5746.
it('should consider initial text value to be current', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const node = ReactDOM.render(
<input type="text" onChange={cb} defaultValue="foo" />,
container,
);
node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
if (ReactFeatureFlags.disableInputAttributeSyncing) {
// TODO: figure out why. This might be a bug.
expect(called).toBe(1);
} else {
// There should be no React change events because the value stayed the same.
expect(called).toBe(0);
}
});
it('should consider initial checkbox checked=true to be current', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const node = ReactDOM.render(
<input type="checkbox" onChange={cb} defaultChecked={true} />,
container,
);
// Secretly, set `checked` to false, so that dispatching the `click` will
// make it `true` again. Thus, at the time of the event, React should not
// consider it a change from the initial `true` value.
setUntrackedChecked.call(node, false);
node.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// There should be no React change events because the value stayed the same.
expect(called).toBe(0);
});
it('should consider initial checkbox checked=false to be current', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const node = ReactDOM.render(
<input type="checkbox" onChange={cb} defaultChecked={false} />,
container,
);
// Secretly, set `checked` to true, so that dispatching the `click` will
// make it `false` again. Thus, at the time of the event, React should not
// consider it a change from the initial `false` value.
setUntrackedChecked.call(node, true);
node.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// There should be no React change events because the value stayed the same.
expect(called).toBe(0);
});
it('should fire change for checkbox input', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const node = ReactDOM.render(
<input type="checkbox" onChange={cb} />,
container,
);
expect(node.checked).toBe(false);
node.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Note: unlike with text input events, dispatching `click` actually
// toggles the checkbox and updates its `checked` value.
expect(node.checked).toBe(true);
expect(called).toBe(1);
expect(node.checked).toBe(true);
node.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
expect(node.checked).toBe(false);
expect(called).toBe(2);
});
it('should not fire change setting the value programmatically', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const input = ReactDOM.render(
<input type="text" defaultValue="foo" onChange={cb} />,
container,
);
// Set it programmatically.
input.value = 'bar';
// Even if a DOM input event fires, React sees that the real input value now
// ('bar') is the same as the "current" one we already recorded.
input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
expect(input.value).toBe('bar');
// In this case we don't expect to get a React event.
expect(called).toBe(0);
// However, we can simulate user typing by calling the underlying setter.
setUntrackedValue.call(input, 'foo');
// Now, when the event fires, the real input value ('foo') differs from the
// "current" one we previously recorded ('bar').
input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
expect(input.value).toBe('foo');
// In this case React should fire an event for it.
expect(called).toBe(1);
// Verify again that extra events without real changes are ignored.
input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
expect(called).toBe(1);
});
it('should not distinguish equal string and number values', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const input = ReactDOM.render(
<input type="text" defaultValue="42" onChange={cb} />,
container,
);
// When we set `value` as a property, React updates the "current" value
// that it tracks internally. The "current" value is later used to determine
// whether a change event is a duplicate or not.
// Even though we set value to a number, we still shouldn't get a change
// event because as a string, it's equal to the initial value ('42').
input.value = 42;
input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
expect(input.value).toBe('42');
expect(called).toBe(0);
});
// See a similar input test above for a detailed description of why.
it('should not fire change when setting checked programmatically', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const input = ReactDOM.render(
<input type="checkbox" onChange={cb} defaultChecked={false} />,
container,
);
// Set the value, updating the "current" value that React tracks to true.
input.checked = true;
// Under the hood, uncheck the box so that the click will "check" it again.
setUntrackedChecked.call(input, false);
input.click();
expect(input.checked).toBe(true);
// We don't expect a React event because at the time of the click, the real
// checked value (true) was the same as the last recorded "current" value
// (also true).
expect(called).toBe(0);
// However, simulating a normal click should fire a React event because the
// real value (false) would have changed from the last tracked value (true).
input.click();
expect(called).toBe(1);
});
it('should unmount', () => {
const input = ReactDOM.render(<input />, container);
ReactDOM.unmountComponentAtNode(container);
});
it('should only fire change for checked radio button once', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const input = ReactDOM.render(
<input type="radio" onChange={cb} />,
container,
);
setUntrackedChecked.call(input, true);
input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
expect(called).toBe(1);
});
it('should track radio button cousins in a group', () => {
let called1 = 0;
let called2 = 0;
function cb1(e) {
called1++;
expect(e.type).toBe('change');
}
function cb2(e) {
called2++;
expect(e.type).toBe('change');
}
const div = ReactDOM.render(
<div>
<input type="radio" name="group" onChange={cb1} />
<input type="radio" name="group" onChange={cb2} />
</div>,
container,
);
const option1 = div.childNodes[0];
const option2 = div.childNodes[1];
// Select first option.
option1.click();
expect(called1).toBe(1);
expect(called2).toBe(0);
// Select second option.
option2.click();
expect(called1).toBe(1);
expect(called2).toBe(1);
// Select the first option.
// It should receive the React change event again.
option1.click();
expect(called1).toBe(2);
expect(called2).toBe(1);
});
it('should deduplicate input value change events', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
let input;
['text', 'number', 'range'].forEach(type => {
called = 0;
input = ReactDOM.render(<input type={type} onChange={cb} />, container);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
expect(called).toBe(1);
ReactDOM.unmountComponentAtNode(container);
called = 0;
input = ReactDOM.render(<input type={type} onChange={cb} />, container);
// Should be ignored (no change):
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
expect(called).toBe(1);
ReactDOM.unmountComponentAtNode(container);
called = 0;
input = ReactDOM.render(<input type={type} onChange={cb} />, container);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
expect(called).toBe(1);
ReactDOM.unmountComponentAtNode(container);
});
});
it('should listen for both change and input events when supported', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const input = ReactDOM.render(
<input type="range" onChange={cb} />,
container,
);
setUntrackedValue.call(input, 10);
input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
setUntrackedValue.call(input, 20);
input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
expect(called).toBe(2);
});
it('should only fire events when the value changes for range inputs', () => {
let called = 0;
function cb(e) {
called++;
expect(e.type).toBe('change');
}
const input = ReactDOM.render(
<input type="range" onChange={cb} />,
container,
);
setUntrackedValue.call(input, '40');
input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
setUntrackedValue.call(input, 'foo');
input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
expect(called).toBe(2);
});
it('does not crash for nodes with custom value property', () => {
let originalCreateElement;
// https://github.com/facebook/react/issues/10196
try {
originalCreateElement = document.createElement;
document.createElement = function() {
const node = originalCreateElement.apply(this, arguments);
Object.defineProperty(node, 'value', {
get() {},
set() {},
});
return node;
};
const div = document.createElement('div');
// Mount
const node = ReactDOM.render(<input type="text" />, div);
// Update
ReactDOM.render(<input type="text" />, div);
// Change
node.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
// Unmount
ReactDOM.unmountComponentAtNode(div);
} finally {
document.createElement = originalCreateElement;
}
});
describe('concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
React = require('react');
ReactDOM = require('react-dom');
TestUtils = require('react-dom/test-utils');
Scheduler = require('scheduler');
});
// @gate experimental
it('text input', () => {
const root = ReactDOM.unstable_createRoot(container);
let input;
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<input
ref={el => (input = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(input.value).toBe('initial');
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(Scheduler).toHaveYielded(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
// @gate experimental
it('checkbox input', () => {
const root = ReactDOM.unstable_createRoot(container);
let input;
class ControlledInput extends React.Component {
state = {checked: false};
onChange = event => {
this.setState({checked: event.target.checked});
};
render() {
Scheduler.unstable_yieldValue(`render: ${this.state.checked}`);
const controlledValue = this.props.reverse
? !this.state.checked
: this.state.checked;
return (
<input
ref={el => (input = el)}
type="checkbox"
checked={controlledValue}
onChange={this.onChange}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput reverse={false} />);
// Should not have flushed yet.
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
expect(Scheduler).toFlushAndYield(['render: false']);
expect(input.checked).toBe(false);
// Trigger a change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(Scheduler).toHaveYielded(['render: true']);
expect(input.checked).toBe(true);
// Now let's make sure we're using the controlled value.
root.render(<ControlledInput reverse={true} />);
expect(Scheduler).toFlushAndYield(['render: true']);
// Trigger another change event.
input.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(Scheduler).toHaveYielded(['render: true']);
expect(input.checked).toBe(false);
});
// @gate experimental
it('textarea', () => {
const root = ReactDOM.unstable_createRoot(container);
let textarea;
class ControlledTextarea extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<textarea
ref={el => (textarea = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledTextarea />);
// Should not have flushed yet.
expect(Scheduler).toHaveYielded([]);
expect(textarea).toBe(undefined);
// Flush callbacks.
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(textarea.value).toBe('initial');
// Trigger a change event.
setUntrackedTextareaValue.call(textarea, 'changed');
textarea.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(Scheduler).toHaveYielded(['render: changed']);
// Value should be the controlled value, not the original one
expect(textarea.value).toBe('changed [!]');
});
// @gate experimental
it('parent of input', () => {
const root = ReactDOM.unstable_createRoot(container);
let input;
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
render() {
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<div onChange={this.onChange}>
<input
ref={el => (input = el)}
type="text"
value={controlledValue}
onChange={() => {
// Does nothing. Parent handler is responsible for updating.
}}
/>
</div>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(input.value).toBe('initial');
// Trigger a change event.
setUntrackedValue.call(input, 'changed');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Change should synchronously flush
expect(Scheduler).toHaveYielded(['render: changed']);
// Value should be the controlled value, not the original one
expect(input.value).toBe('changed [!]');
});
// @gate experimental
it('is async for non-input events', () => {
const root = ReactDOM.unstable_createRoot(container);
let input;
class ControlledInput extends React.Component {
state = {value: 'initial'};
onChange = event => this.setState({value: event.target.value});
reset = () => {
this.setState({value: ''});
};
render() {
Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
const controlledValue =
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<input
ref={el => (input = el)}
type="text"
value={controlledValue}
onChange={this.onChange}
onClick={this.reset}
/>
);
}
}
// Initial mount. Test that this is async.
root.render(<ControlledInput />);
// Should not have flushed yet.
expect(Scheduler).toHaveYielded([]);
expect(input).toBe(undefined);
// Flush callbacks.
expect(Scheduler).toFlushAndYield(['render: initial']);
expect(input.value).toBe('initial');
// Trigger a click event
input.dispatchEvent(
new Event('click', {bubbles: true, cancelable: true}),
);
// Nothing should have changed
expect(Scheduler).toHaveYielded([]);
expect(input.value).toBe('initial');
// Flush callbacks.
// Now the click update has flushed.
expect(Scheduler).toFlushAndYield(['render: ']);
expect(input.value).toBe('');
});
// @gate experimental
it('mouse enter/leave should be user-blocking but not discrete', async () => {
// This is currently behind a feature flag
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
TestUtils = require('react-dom/test-utils');
Scheduler = require('scheduler');
const {act} = TestUtils;
const {useState} = React;
const root = ReactDOM.unstable_createRoot(container);
const target = React.createRef(null);
function Foo() {
const [isHover, setHover] = useState(false);
return (
<div
ref={target}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}>
{isHover ? 'hovered' : 'not hovered'}
</div>
);
}
await act(async () => {
root.render(<Foo />);
});
expect(container.textContent).toEqual('not hovered');
await act(async () => {
const mouseOverEvent = document.createEvent('MouseEvents');
mouseOverEvent.initEvent('mouseover', true, true);
target.current.dispatchEvent(mouseOverEvent);
// 3s should be enough to expire the updates
Scheduler.unstable_advanceTime(3000);
expect(Scheduler).toFlushExpired([]);
expect(container.textContent).toEqual('hovered');
});
});
});
});
@@ -1,239 +0,0 @@
/**
* 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 ReactDOM;
describe('EnterLeaveEventPlugin', () => {
let container;
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
// The container has to be attached for events to fire.
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('should set onMouseLeave relatedTarget properly in iframe', () => {
const iframe = document.createElement('iframe');
container.appendChild(iframe);
const iframeDocument = iframe.contentDocument;
iframeDocument.write(
'<!DOCTYPE html><html><head></head><body><div></div></body></html>',
);
iframeDocument.close();
const leaveEvents = [];
const node = ReactDOM.render(
<div
onMouseLeave={e => {
e.persist();
leaveEvents.push(e);
}}
/>,
iframeDocument.body.getElementsByTagName('div')[0],
);
node.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: iframe.contentWindow,
}),
);
expect(leaveEvents.length).toBe(1);
expect(leaveEvents[0].target).toBe(node);
expect(leaveEvents[0].relatedTarget).toBe(iframe.contentWindow);
});
it('should set onMouseEnter relatedTarget properly in iframe', () => {
const iframe = document.createElement('iframe');
container.appendChild(iframe);
const iframeDocument = iframe.contentDocument;
iframeDocument.write(
'<!DOCTYPE html><html><head></head><body><div></div></body></html>',
);
iframeDocument.close();
const enterEvents = [];
const node = ReactDOM.render(
<div
onMouseEnter={e => {
e.persist();
enterEvents.push(e);
}}
/>,
iframeDocument.body.getElementsByTagName('div')[0],
);
node.dispatchEvent(
new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
relatedTarget: null,
}),
);
expect(enterEvents.length).toBe(1);
expect(enterEvents[0].target).toBe(node);
expect(enterEvents[0].relatedTarget).toBe(iframe.contentWindow);
});
// Regression test for https://github.com/facebook/react/issues/10906.
it('should find the common parent after updates', () => {
let parentEnterCalls = 0;
let childEnterCalls = 0;
let parent = null;
class Parent extends React.Component {
render() {
return (
<div
onMouseEnter={() => parentEnterCalls++}
ref={node => (parent = node)}>
{this.props.showChild && (
<div onMouseEnter={() => childEnterCalls++} />
)}
</div>
);
}
}
ReactDOM.render(<Parent />, container);
// The issue only reproduced on insertion during the first update.
ReactDOM.render(<Parent showChild={true} />, container);
// Enter from parent into the child.
parent.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: parent.firstChild,
}),
);
// Entering a child should fire on the child, not on the parent.
expect(childEnterCalls).toBe(1);
expect(parentEnterCalls).toBe(0);
});
// Test for https://github.com/facebook/react/issues/16763.
it('should call mouseEnter once from sibling rendered inside a rendered component', done => {
const mockFn = jest.fn();
class Parent extends React.Component {
constructor(props) {
super(props);
this.parentEl = React.createRef();
}
componentDidMount() {
ReactDOM.render(<MouseEnterDetect />, this.parentEl.current);
}
render() {
return <div ref={this.parentEl} />;
}
}
class MouseEnterDetect extends React.Component {
constructor(props) {
super(props);
this.firstEl = React.createRef();
this.siblingEl = React.createRef();
}
componentDidMount() {
this.siblingEl.current.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: this.firstEl.current,
}),
);
expect(mockFn.mock.calls.length).toBe(1);
done();
}
render() {
return (
<React.Fragment>
<div ref={this.firstEl} onMouseEnter={mockFn} />
<div ref={this.siblingEl} />
</React.Fragment>
);
}
}
ReactDOM.render(<Parent />, container);
});
it('should call mouseEnter when pressing a non tracked React node', done => {
const mockFn = jest.fn();
class Parent extends React.Component {
constructor(props) {
super(props);
this.parentEl = React.createRef();
}
componentDidMount() {
ReactDOM.render(<MouseEnterDetect />, this.parentEl.current);
}
render() {
return <div ref={this.parentEl} />;
}
}
class MouseEnterDetect extends React.Component {
constructor(props) {
super(props);
this.divRef = React.createRef();
this.siblingEl = React.createRef();
}
componentDidMount() {
const attachedNode = document.createElement('div');
this.divRef.current.appendChild(attachedNode);
attachedNode.dispatchEvent(
new MouseEvent('mouseout', {
bubbles: true,
cancelable: true,
relatedTarget: this.siblingEl.current,
}),
);
expect(mockFn.mock.calls.length).toBe(1);
done();
}
render() {
return (
<div ref={this.divRef}>
<div ref={this.siblingEl} onMouseEnter={mockFn} />
</div>
);
}
}
ReactDOM.render(<Parent />, container);
});
});
@@ -1,831 +0,0 @@
/**
* 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 ReactDOM;
let ReactFeatureFlags;
describe('BeforeInputEventPlugin', () => {
let container;
function loadReactDOM(envSimulator) {
jest.resetModules();
if (envSimulator) {
envSimulator();
}
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableModernEventSystem = true;
return require('react-dom');
}
function simulateIE11() {
document.documentMode = 11;
window.CompositionEvent = {};
}
function simulateWebkit() {
window.CompositionEvent = {};
window.TextEvent = {};
}
function simulateComposition() {
window.CompositionEvent = {};
}
function simulateNoComposition() {
// no composition event in Window - will use fallback
}
function simulateEvent(elem, type, data) {
const event = new Event(type, {bubbles: true});
Object.assign(event, data);
elem.dispatchEvent(event);
}
function simulateKeyboardEvent(elem, type, data) {
const {char, value, ...rest} = data;
const event = new KeyboardEvent(type, {
bubbles: true,
...rest,
});
if (char) {
event.char = char;
}
if (value) {
elem.value = value;
}
elem.dispatchEvent(event);
}
function simulatePaste(elem) {
const pasteEvent = new Event('paste', {
bubbles: true,
});
pasteEvent.clipboardData = {
dropEffect: null,
effectAllowed: null,
files: null,
items: null,
types: null,
};
elem.dispatchEvent(pasteEvent);
}
beforeEach(() => {
React = require('react');
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
delete document.documentMode;
delete window.CompositionEvent;
delete window.TextEvent;
delete window.opera;
document.body.removeChild(container);
container = null;
});
function keyCode(char) {
return char.charCodeAt(0);
}
const scenarios = [
{
eventSimulator: simulateEvent,
eventSimulatorArgs: [
'compositionstart',
{detail: {data: 'test'}, data: 'test'},
],
},
{
eventSimulator: simulateEvent,
eventSimulatorArgs: [
'compositionupdate',
{detail: {data: 'test string'}, data: 'test string'},
],
},
{
eventSimulator: simulateEvent,
eventSimulatorArgs: [
'compositionend',
{detail: {data: 'test string 3'}, data: 'test string 3'},
],
},
{
eventSimulator: simulateEvent,
eventSimulatorArgs: ['textInput', {data: 'abcß'}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keypress', {which: keyCode('a')}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keypress', {which: keyCode(' ')}, ' '],
},
{
eventSimulator: simulateEvent,
eventSimulatorArgs: ['textInput', {data: ' '}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keypress', {which: keyCode('a'), ctrlKey: true}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keypress', {which: keyCode('b'), altKey: true}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: [
'keypress',
{which: keyCode('c'), altKey: true, ctrlKey: true},
],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: [
'keypress',
{which: keyCode('X'), char: '\uD83D\uDE0A'},
],
},
{
eventSimulator: simulateEvent,
eventSimulatorArgs: ['textInput', {data: '\uD83D\uDE0A'}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keydown', {keyCode: 229, value: 'foo'}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keydown', {keyCode: 9, value: 'foobar'}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keydown', {keyCode: 229, value: 'foofoo'}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keyup', {keyCode: 9, value: 'fooBARfoo'}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keydown', {keyCode: 229, value: 'foofoo'}],
},
{
eventSimulator: simulateKeyboardEvent,
eventSimulatorArgs: ['keypress', {keyCode: 60, value: 'Barfoofoo'}],
},
{
eventSimulator: simulatePaste,
eventSimulatorArgs: [],
},
];
const environments = [
{
emulator: simulateWebkit,
assertions: [
{
run: ({
beforeInputEvent,
compositionStartEvent,
spyOnBeforeInput,
spyOnCompositionStart,
}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
expect(spyOnCompositionStart).toHaveBeenCalledTimes(1);
expect(compositionStartEvent.type).toBe('compositionstart');
expect(compositionStartEvent.data).toBe('test');
},
},
{
run: ({
beforeInputEvent,
compositionUpdateEvent,
spyOnBeforeInput,
spyOnCompositionUpdate,
}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
expect(spyOnCompositionUpdate).toHaveBeenCalledTimes(1);
expect(compositionUpdateEvent.type).toBe('compositionupdate');
expect(compositionUpdateEvent.data).toBe('test string');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('compositionend');
expect(beforeInputEvent.data).toBe('test string 3');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('textInput');
expect(beforeInputEvent.data).toBe('abcß');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe(' ');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('textInput');
expect(beforeInputEvent.data).toBe('\uD83D\uDE0A');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
],
},
{
emulator: simulateIE11,
assertions: [
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('a');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe(' ');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('c');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('\uD83D\uDE0A');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
],
},
{
emulator: simulateNoComposition,
assertions: [
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('a');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe(' ');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('c');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('\uD83D\uDE0A');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keydown');
expect(beforeInputEvent.data).toBe('bar');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keyup');
expect(beforeInputEvent.data).toBe('BAR');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('Bar');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
],
},
{
emulator: simulateComposition,
assertions: [
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('compositionend');
expect(beforeInputEvent.data).toBe('test string 3');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('a');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe(' ');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('c');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(1);
expect(beforeInputEvent.type).toBe('keypress');
expect(beforeInputEvent.data).toBe('\uD83D\uDE0A');
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
{
run: ({beforeInputEvent, spyOnBeforeInput}) => {
expect(spyOnBeforeInput).toHaveBeenCalledTimes(0);
expect(beforeInputEvent).toBeNull();
},
},
],
},
];
const testInputComponent = (env, scenes) => {
let beforeInputEvent;
let compositionStartEvent;
let compositionUpdateEvent;
let spyOnBeforeInput;
let spyOnCompositionStart;
let spyOnCompositionUpdate;
ReactDOM = loadReactDOM(env.emulator);
const node = ReactDOM.render(
<input
type="text"
onBeforeInput={({type, data}) => {
spyOnBeforeInput();
beforeInputEvent = {type, data};
}}
onCompositionStart={({type, data}) => {
spyOnCompositionStart();
compositionStartEvent = {type, data};
}}
onCompositionUpdate={({type, data}) => {
spyOnCompositionUpdate();
compositionUpdateEvent = {type, data};
}}
/>,
container,
);
scenes.forEach((s, id) => {
beforeInputEvent = null;
compositionStartEvent = null;
compositionUpdateEvent = null;
spyOnBeforeInput = jest.fn();
spyOnCompositionStart = jest.fn();
spyOnCompositionUpdate = jest.fn();
s.eventSimulator.apply(null, [node, ...s.eventSimulatorArgs]);
env.assertions[id].run({
beforeInputEvent,
compositionStartEvent,
compositionUpdateEvent,
spyOnBeforeInput,
spyOnCompositionStart,
spyOnCompositionUpdate,
});
});
};
const testContentEditableComponent = (env, scenes) => {
let beforeInputEvent;
let compositionStartEvent;
let compositionUpdateEvent;
let spyOnBeforeInput;
let spyOnCompositionStart;
let spyOnCompositionUpdate;
ReactDOM = loadReactDOM(env.emulator);
const node = ReactDOM.render(
<div
contentEditable={true}
onBeforeInput={({type, data}) => {
spyOnBeforeInput();
beforeInputEvent = {type, data};
}}
onCompositionStart={({type, data}) => {
spyOnCompositionStart();
compositionStartEvent = {type, data};
}}
onCompositionUpdate={({type, data}) => {
spyOnCompositionUpdate();
compositionUpdateEvent = {type, data};
}}
/>,
container,
);
scenes.forEach((s, id) => {
beforeInputEvent = null;
compositionStartEvent = null;
compositionUpdateEvent = null;
spyOnBeforeInput = jest.fn();
spyOnCompositionStart = jest.fn();
spyOnCompositionUpdate = jest.fn();
s.eventSimulator.apply(null, [node, ...s.eventSimulatorArgs]);
env.assertions[id].run({
beforeInputEvent,
compositionStartEvent,
compositionUpdateEvent,
spyOnBeforeInput,
spyOnCompositionStart,
spyOnCompositionUpdate,
});
});
};
it('should extract onBeforeInput when simulating in Webkit on input[type=text]', () => {
testInputComponent(environments[0], scenarios);
});
it('should extract onBeforeInput when simulating in Webkit on contenteditable', () => {
testContentEditableComponent(environments[0], scenarios);
});
it('should extract onBeforeInput when simulating in IE11 on input[type=text]', () => {
testInputComponent(environments[1], scenarios);
});
it('should extract onBeforeInput when simulating in IE11 on contenteditable', () => {
testContentEditableComponent(environments[1], scenarios);
});
it('should extract onBeforeInput when simulating in env with no CompositionEvent on input[type=text]', () => {
testInputComponent(environments[2], scenarios);
});
// in an environment using composition fallback onBeforeInput will not work
// as expected on a contenteditable as keydown and keyup events are translated
// to keypress events
it('should extract onBeforeInput when simulating in env with only CompositionEvent on input[type=text]', () => {
testInputComponent(environments[3], scenarios);
});
it('should extract onBeforeInput when simulating in env with only CompositionEvent on contenteditable', () => {
testContentEditableComponent(environments[3], scenarios);
});
});
@@ -54,7 +54,6 @@ describe('ChangeEventPlugin', () => {
postMessageCallback(postMessageEvent);
}
};
ReactFeatureFlags.enableModernEventSystem = true;
React = require('react');
ReactDOM = require('react-dom');
TestUtils = require('react-dom/test-utils');
@@ -11,7 +11,6 @@
let React;
let ReactDOM;
let ReactFeatureFlags;
describe('EnterLeaveEventPlugin', () => {
let container;
@@ -19,8 +18,6 @@ describe('EnterLeaveEventPlugin', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableModernEventSystem = true;
React = require('react');
ReactDOM = require('react-dom');
@@ -1,167 +0,0 @@
/**
* 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 ReactDOM;
let ReactFeatureFlags;
describe('SelectEventPlugin', () => {
let container;
beforeEach(() => {
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;
});
// See https://github.com/facebook/react/pull/3639 for details.
it('does not get confused when dependent events are registered independently', () => {
const select = jest.fn();
const onSelect = event => {
expect(typeof event).toBe('object');
expect(event.type).toBe('select');
expect(event.target).toBe(node);
select(event.currentTarget);
};
// Pass `onMouseDown` so React registers a top-level listener.
const node = ReactDOM.render(
<input type="text" onMouseDown={function() {}} />,
container,
);
// Trigger `mousedown` and `mouseup`. Note that
// React is not currently listening to `mouseup`.
node.dispatchEvent(
new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
}),
);
node.dispatchEvent(
new MouseEvent('mouseup', {
bubbles: true,
cancelable: true,
}),
);
// Now subscribe to `onSelect`.
ReactDOM.render(<input type="text" onSelect={onSelect} />, container);
node.focus();
// This triggers a `select` event in our polyfill.
node.dispatchEvent(
new KeyboardEvent('keydown', {bubbles: true, cancelable: true}),
);
// Verify that it doesn't get "stuck" waiting for
// a `mouseup` event that it has "missed" because
// a top-level listener didn't exist yet.
expect(select).toHaveBeenCalledTimes(1);
});
it('should fire `onSelect` when a listener is present', () => {
const select = jest.fn();
const onSelect = event => {
expect(typeof event).toBe('object');
expect(event.type).toBe('select');
expect(event.target).toBe(node);
select(event.currentTarget);
};
const node = ReactDOM.render(
<input type="text" onSelect={onSelect} />,
container,
);
node.focus();
let nativeEvent = new MouseEvent('focus', {
bubbles: true,
cancelable: true,
});
node.dispatchEvent(nativeEvent);
expect(select).toHaveBeenCalledTimes(0);
nativeEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
});
node.dispatchEvent(nativeEvent);
expect(select).toHaveBeenCalledTimes(0);
nativeEvent = new MouseEvent('mouseup', {bubbles: true, cancelable: true});
node.dispatchEvent(nativeEvent);
expect(select).toHaveBeenCalledTimes(1);
});
// Regression test for https://github.com/facebook/react/issues/11379
it('should not wait for `mouseup` after receiving `dragend`', () => {
const select = jest.fn();
const onSelect = event => {
expect(typeof event).toBe('object');
expect(event.type).toBe('select');
expect(event.target).toBe(node);
select(event.currentTarget);
};
const node = ReactDOM.render(
<input type="text" onSelect={onSelect} />,
container,
);
node.focus();
let nativeEvent = new MouseEvent('focus', {
bubbles: true,
cancelable: true,
});
node.dispatchEvent(nativeEvent);
expect(select).toHaveBeenCalledTimes(0);
nativeEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
});
node.dispatchEvent(nativeEvent);
expect(select).toHaveBeenCalledTimes(0);
nativeEvent = new MouseEvent('dragend', {bubbles: true, cancelable: true});
node.dispatchEvent(nativeEvent);
expect(select).toHaveBeenCalledTimes(1);
});
it('should handle selectionchange events', function() {
const onSelect = jest.fn();
const node = ReactDOM.render(
<input type="text" onSelect={onSelect} />,
container,
);
node.focus();
// Make sure the event was not called before we emit the selection change event
expect(onSelect).toHaveBeenCalledTimes(0);
// This is dispatched e.g. when using CMD+a on macOS
document.dispatchEvent(
new Event('selectionchange', {bubbles: false, cancelable: false}),
);
expect(onSelect).toHaveBeenCalledTimes(1);
});
});
@@ -1,493 +0,0 @@
/**
* 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';
describe('SimpleEventPlugin', function() {
let React;
let ReactDOM;
let Scheduler;
let ReactFeatureFlags;
let onClick;
let container;
function expectClickThru(element) {
element.click();
expect(onClick).toHaveBeenCalledTimes(1);
}
function expectNoClickThru(element) {
element.click();
expect(onClick).toHaveBeenCalledTimes(0);
}
function mounted(element) {
container = document.createElement('div');
document.body.appendChild(container);
element = ReactDOM.render(element, container);
return element;
}
beforeEach(function() {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableModernEventSystem = true;
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
onClick = jest.fn();
});
afterEach(() => {
if (container && document.body.contains(container)) {
document.body.removeChild(container);
container = null;
}
});
it('A non-interactive tags click when disabled', function() {
const element = <div onClick={onClick} />;
expectClickThru(mounted(element));
});
it('A non-interactive tags clicks bubble when disabled', function() {
const element = mounted(
<div onClick={onClick}>
<div />
</div>,
);
const child = element.firstChild;
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('does not register a click when clicking a child of a disabled element', function() {
const element = mounted(
<button onClick={onClick} disabled={true}>
<span />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(0);
});
it('triggers click events for children of disabled elements', function() {
const element = mounted(
<button disabled={true}>
<span onClick={onClick} />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('triggers parent captured click events when target is a child of a disabled elements', function() {
const element = mounted(
<div onClickCapture={onClick}>
<button disabled={true}>
<span />
</button>
</div>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
it('triggers captured click events for children of disabled elements', function() {
const element = mounted(
<button disabled={true}>
<span onClickCapture={onClick} />
</button>,
);
const child = element.querySelector('span');
child.click();
expect(onClick).toHaveBeenCalledTimes(1);
});
['button', 'input', 'select', 'textarea'].forEach(function(tagName) {
describe(tagName, function() {
it('should forward clicks when it starts out not disabled', () => {
const element = React.createElement(tagName, {
onClick: onClick,
});
expectClickThru(mounted(element));
});
it('should not forward clicks when it starts out disabled', () => {
const element = React.createElement(tagName, {
onClick: onClick,
disabled: true,
});
expectNoClickThru(mounted(element));
});
it('should forward clicks when it becomes not disabled', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick}),
container,
);
expectClickThru(element);
});
it('should not forward clicks when it becomes disabled', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
expectNoClickThru(element);
});
it('should work correctly if the listener is changed', () => {
container = document.createElement('div');
document.body.appendChild(container);
let element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: true}),
container,
);
element = ReactDOM.render(
React.createElement(tagName, {onClick: onClick, disabled: false}),
container,
);
expectClickThru(element);
});
});
});
it('batches updates that occur as a result of a nested event dispatch', () => {
container = document.createElement('div');
document.body.appendChild(container);
let button;
class Button extends React.Component {
state = {count: 0};
increment = () =>
this.setState(state => ({
count: state.count + 1,
}));
componentDidUpdate() {
Scheduler.unstable_yieldValue(`didUpdate - Count: ${this.state.count}`);
}
render() {
return (
<button
ref={el => (button = el)}
onFocus={this.increment}
onClick={() => {
// The focus call synchronously dispatches a nested event. All of
// the updates in this handler should be batched together.
this.increment();
button.focus();
this.increment();
}}>
Count: {this.state.count}
</button>
);
}
}
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
ReactDOM.render(<Button />, container);
expect(button.textContent).toEqual('Count: 0');
expect(Scheduler).toHaveYielded([]);
click();
// There should be exactly one update.
expect(Scheduler).toHaveYielded(['didUpdate - Count: 3']);
expect(button.textContent).toEqual('Count: 3');
});
describe('interactive events, in concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableModernEventSystem = true;
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
// @gate experimental
it('flushes pending interactive work before extracting event handler', () => {
container = document.createElement('div');
const root = ReactDOM.createRoot(container);
document.body.appendChild(container);
let button;
class Button extends React.Component {
state = {disabled: false};
onClick = () => {
// Perform some side-effect
Scheduler.unstable_yieldValue('Side-effect');
// Disable the button
this.setState({disabled: true});
};
render() {
Scheduler.unstable_yieldValue(
`render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
);
return (
<button
ref={el => (button = el)}
// Handler is removed after the first click
onClick={this.state.disabled ? null : this.onClick}
/>
);
}
}
// Initial mount
root.render(<Button />);
// Should not have flushed yet because it's async
expect(Scheduler).toHaveYielded([]);
expect(button).toBe(undefined);
// Flush async work
expect(Scheduler).toFlushAndYield(['render button: enabled']);
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button to trigger the side-effect
click();
expect(Scheduler).toHaveYielded([
// The handler fired
'Side-effect',
// but the component did not re-render yet, because it's async
]);
// Click the button again
click();
expect(Scheduler).toHaveYielded([
// Before handling this second click event, the previous interactive
// update is flushed
'render button: disabled',
// The event handler was removed from the button, so there's no second
// side-effect
]);
// The handler should not fire again no matter how many times we
// click the handler.
click();
click();
click();
click();
click();
expect(Scheduler).toFlushAndYield([]);
});
// @gate experimental
it('end result of many interactive updates is deterministic', () => {
container = document.createElement('div');
const root = ReactDOM.createRoot(container);
document.body.appendChild(container);
let button;
class Button extends React.Component {
state = {count: 0};
render() {
return (
<button
ref={el => (button = el)}
onClick={() =>
// Intentionally not using the updater form here
this.setState({count: this.state.count + 1})
}>
Count: {this.state.count}
</button>
);
}
}
// Initial mount
root.render(<Button />);
// Should not have flushed yet because it's async
expect(button).toBe(undefined);
// Flush async work
Scheduler.unstable_flushAll();
expect(button.textContent).toEqual('Count: 0');
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button a single time
click();
// The counter should not have updated yet because it's async
expect(button.textContent).toEqual('Count: 0');
// Click the button many more times
click();
click();
click();
click();
click();
click();
// Flush the remaining work
Scheduler.unstable_flushAll();
// The counter should equal the total number of clicks
expect(button.textContent).toEqual('Count: 7');
});
// @gate experimental
it('flushes discrete updates in order', () => {
container = document.createElement('div');
document.body.appendChild(container);
let button;
class Button extends React.Component {
state = {lowPriCount: 0};
render() {
const text = `High-pri count: ${this.props.highPriCount}, Low-pri count: ${this.state.lowPriCount}`;
Scheduler.unstable_yieldValue(text);
return (
<button
ref={el => (button = el)}
onClick={() => {
Scheduler.unstable_next(() => {
this.setState(state => ({
lowPriCount: state.lowPriCount + 1,
}));
});
}}>
{text}
</button>
);
}
}
class Wrapper extends React.Component {
state = {highPriCount: 0};
render() {
return (
<div
onClick={
// Intentionally not using the updater form here, to test
// that updates are serially processed.
() => {
this.setState({highPriCount: this.state.highPriCount + 1});
}
}>
<Button highPriCount={this.state.highPriCount} />
</div>
);
}
}
// Initial mount
const root = ReactDOM.createRoot(container);
root.render(<Wrapper />);
expect(Scheduler).toFlushAndYield([
'High-pri count: 0, Low-pri count: 0',
]);
expect(button.textContent).toEqual('High-pri count: 0, Low-pri count: 0');
function click() {
button.dispatchEvent(
new MouseEvent('click', {bubbles: true, cancelable: true}),
);
}
// Click the button a single time
click();
// Nothing should flush on the first click.
expect(Scheduler).toHaveYielded([]);
// Click again. This will force the previous discrete update to flush. But
// only the high-pri count will increase.
click();
expect(Scheduler).toHaveYielded(['High-pri count: 1, Low-pri count: 0']);
expect(button.textContent).toEqual('High-pri count: 1, Low-pri count: 0');
// Click the button many more times
click();
click();
click();
click();
click();
click();
// Flush the remaining work.
expect(Scheduler).toHaveYielded([
'High-pri count: 2, Low-pri count: 0',
'High-pri count: 3, Low-pri count: 0',
'High-pri count: 4, Low-pri count: 0',
'High-pri count: 5, Low-pri count: 0',
'High-pri count: 6, Low-pri count: 0',
'High-pri count: 7, Low-pri count: 0',
]);
// At the end, both counters should equal the total number of clicks
expect(Scheduler).toFlushAndYield([
'High-pri count: 8, Low-pri count: 0',
'High-pri count: 8, Low-pri count: 8',
]);
expect(button.textContent).toEqual('High-pri count: 8, Low-pri count: 8');
});
});
describe('iOS bubbling click fix', function() {
// See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
it('does not add a local click to interactive elements', function() {
container = document.createElement('div');
ReactDOM.render(<button onClick={onClick} />, container);
const node = container.firstChild;
node.dispatchEvent(new MouseEvent('click'));
expect(onClick).toHaveBeenCalledTimes(0);
});
it('adds a local click listener to non-interactive elements', function() {
container = document.createElement('div');
ReactDOM.render(<div onClick={onClick} />, container);
const node = container.firstChild;
node.dispatchEvent(new MouseEvent('click'));
expect(onClick).toHaveBeenCalledTimes(0);
});
});
});
@@ -231,7 +231,6 @@ describe('SimpleEventPlugin', function() {
describe('interactive events, in concurrent mode', () => {
beforeEach(() => {
jest.resetModules();
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
});
@@ -239,7 +238,7 @@ describe('SimpleEventPlugin', function() {
// @gate experimental
it('flushes pending interactive work before extracting event handler', () => {
container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);
const root = ReactDOM.createRoot(container);
document.body.appendChild(container);
let button;
@@ -310,7 +309,7 @@ describe('SimpleEventPlugin', function() {
// @gate experimental
it('end result of many interactive updates is deterministic', () => {
container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);
const root = ReactDOM.createRoot(container);
document.body.appendChild(container);
let button;
@@ -409,7 +408,7 @@ describe('SimpleEventPlugin', function() {
}
// Initial mount
const root = ReactDOM.unstable_createRoot(container);
const root = ReactDOM.createRoot(container);
root.render(<Wrapper />);
expect(Scheduler).toFlushAndYield([
'High-pri count: 0, Low-pri count: 0',
-66
View File
@@ -1,66 +0,0 @@
/**
* 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.
* @flow
*/
import invariant from 'shared/invariant';
import {rethrowCaughtError} from 'shared/ReactErrorUtils';
import type {ReactSyntheticEvent} from './ReactSyntheticEventType';
import accumulateInto from './accumulateInto';
import forEachAccumulated from './forEachAccumulated';
import {executeDispatchesInOrder} from './EventPluginUtils';
/**
* Internal queue of events that have accumulated their dispatches and are
* waiting to have their dispatches executed.
*/
let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
/**
* Dispatches an event and releases it back into the pool, unless persistent.
*
* @param {?object} event Synthetic event to be dispatched.
* @private
*/
const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
if (event) {
executeDispatchesInOrder(event);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
const executeDispatchesAndReleaseTopLevel = function(e) {
return executeDispatchesAndRelease(e);
};
export function runEventsInBatch(
events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
) {
if (events !== null) {
eventQueue = accumulateInto(eventQueue, events);
}
// Set `eventQueue` to null before processing it so that we can tell if more
// events get enqueued while processing.
const processingEventQueue = eventQueue;
eventQueue = null;
if (!processingEventQueue) {
return;
}
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
invariant(
!eventQueue,
'processEventQueue(): Additional events were enqueued while processing ' +
'an event queue. Support for this has not yet been implemented.',
);
// This would be a good time to rethrow if any of the event handlers threw.
rethrowCaughtError();
}
@@ -1,83 +0,0 @@
/**
* 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.
*/
// Used as a way to call batchedUpdates when we don't have a reference to
// the renderer. Such as when we're dispatching events or if third party
// libraries need to call batchedUpdates. Eventually, this API will go away when
// everything is batched by default. We'll then have a similar API to opt-out of
// scheduled work and instead do synchronous work.
// Defaults
let batchedUpdatesImpl = function(fn, bookkeeping) {
return fn(bookkeeping);
};
let discreteUpdatesImpl = function(fn, a, b, c, d) {
return fn(a, b, c, d);
};
let flushDiscreteUpdatesImpl = function() {};
let batchedEventUpdatesImpl = batchedUpdatesImpl;
let isInsideEventHandler = false;
let isBatchingEventUpdates = false;
export function batchedUpdates(fn, bookkeeping) {
if (isInsideEventHandler) {
// If we are currently inside another batch, we need to wait until it
// fully completes before restoring state.
return fn(bookkeeping);
}
isInsideEventHandler = true;
try {
return batchedUpdatesImpl(fn, bookkeeping);
} finally {
isInsideEventHandler = false;
}
}
export function batchedEventUpdates(fn, a, b) {
if (isBatchingEventUpdates) {
// If we are currently inside another batch, we need to wait until it
// fully completes before restoring state.
return fn(a, b);
}
isBatchingEventUpdates = true;
try {
return batchedEventUpdatesImpl(fn, a, b);
} finally {
isBatchingEventUpdates = false;
}
}
export function discreteUpdates(fn, a, b, c, d) {
const prevIsInsideEventHandler = isInsideEventHandler;
isInsideEventHandler = true;
try {
return discreteUpdatesImpl(fn, a, b, c, d);
} finally {
isInsideEventHandler = prevIsInsideEventHandler;
if (!isInsideEventHandler) {
}
}
}
export function flushDiscreteUpdatesIfNeeded(timeStamp: number) {
if (!isInsideEventHandler) {
flushDiscreteUpdatesImpl();
}
}
export function setBatchingImplementation(
_batchedUpdatesImpl,
_discreteUpdatesImpl,
_flushDiscreteUpdatesImpl,
_batchedEventUpdatesImpl,
) {
batchedUpdatesImpl = _batchedUpdatesImpl;
discreteUpdatesImpl = _discreteUpdatesImpl;
flushDiscreteUpdatesImpl = _flushDiscreteUpdatesImpl;
batchedEventUpdatesImpl = _batchedEventUpdatesImpl;
}
@@ -1,802 +0,0 @@
/**
* 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.
*/
import {
executeDirectDispatch,
hasDispatches,
executeDispatchesInOrderStopAtTrue,
getInstanceFromNode,
getFiberCurrentPropsFromNode,
} from './EventPluginUtils';
import ResponderSyntheticEvent from './ResponderSyntheticEvent';
import ResponderTouchHistoryStore from './ResponderTouchHistoryStore';
import accumulate from './accumulate';
import {
TOP_SCROLL,
TOP_SELECTION_CHANGE,
TOP_TOUCH_CANCEL,
isStartish,
isMoveish,
isEndish,
startDependencies,
moveDependencies,
endDependencies,
} from './ResponderTopLevelEventTypes';
import accumulateInto from './accumulateInto';
import forEachAccumulated from './forEachAccumulated';
import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
import invariant from 'shared/invariant';
/**
* Instance of element that should respond to touch/move types of interactions,
* as indicated explicitly by relevant callbacks.
*/
let responderInst = null;
/**
* Count of current touches. A textInput should become responder iff the
* selection changes while there is a touch on the screen.
*/
let trackedTouchCount = 0;
const changeResponder = function(nextResponderInst, blockHostResponder) {
const oldResponderInst = responderInst;
responderInst = nextResponderInst;
if (ResponderEventPlugin.GlobalResponderHandler !== null) {
ResponderEventPlugin.GlobalResponderHandler.onChange(
oldResponderInst,
nextResponderInst,
blockHostResponder,
);
}
};
const eventTypes = {
/**
* On a `touchStart`/`mouseDown`, is it desired that this element become the
* responder?
*/
startShouldSetResponder: {
phasedRegistrationNames: {
bubbled: 'onStartShouldSetResponder',
captured: 'onStartShouldSetResponderCapture',
},
dependencies: startDependencies,
},
/**
* On a `scroll`, is it desired that this element become the responder? This
* is usually not needed, but should be used to retroactively infer that a
* `touchStart` had occurred during momentum scroll. During a momentum scroll,
* a touch start will be immediately followed by a scroll event if the view is
* currently scrolling.
*
* TODO: This shouldn't bubble.
*/
scrollShouldSetResponder: {
phasedRegistrationNames: {
bubbled: 'onScrollShouldSetResponder',
captured: 'onScrollShouldSetResponderCapture',
},
dependencies: [TOP_SCROLL],
},
/**
* On text selection change, should this element become the responder? This
* is needed for text inputs or other views with native selection, so the
* JS view can claim the responder.
*
* TODO: This shouldn't bubble.
*/
selectionChangeShouldSetResponder: {
phasedRegistrationNames: {
bubbled: 'onSelectionChangeShouldSetResponder',
captured: 'onSelectionChangeShouldSetResponderCapture',
},
dependencies: [TOP_SELECTION_CHANGE],
},
/**
* On a `touchMove`/`mouseMove`, is it desired that this element become the
* responder?
*/
moveShouldSetResponder: {
phasedRegistrationNames: {
bubbled: 'onMoveShouldSetResponder',
captured: 'onMoveShouldSetResponderCapture',
},
dependencies: moveDependencies,
},
/**
* Direct responder events dispatched directly to responder. Do not bubble.
*/
responderStart: {
registrationName: 'onResponderStart',
dependencies: startDependencies,
},
responderMove: {
registrationName: 'onResponderMove',
dependencies: moveDependencies,
},
responderEnd: {
registrationName: 'onResponderEnd',
dependencies: endDependencies,
},
responderRelease: {
registrationName: 'onResponderRelease',
dependencies: endDependencies,
},
responderTerminationRequest: {
registrationName: 'onResponderTerminationRequest',
dependencies: [],
},
responderGrant: {
registrationName: 'onResponderGrant',
dependencies: [],
},
responderReject: {
registrationName: 'onResponderReject',
dependencies: [],
},
responderTerminate: {
registrationName: 'onResponderTerminate',
dependencies: [],
},
};
// Start of inline: the below functions were inlined from
// EventPropagator.js, as they deviated from ReactDOM's newer
// implementations.
function getParent(inst) {
do {
inst = inst.return;
// TODO: If this is a HostRoot we might want to bail out.
// That is depending on if we want nested subtrees (layers) to bubble
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
} while (inst && inst.tag !== HostComponent);
if (inst) {
return inst;
}
return null;
}
/**
* Return the lowest common ancestor of A and B, or null if they are in
* different trees.
*/
export function getLowestCommonAncestor(instA, instB) {
let depthA = 0;
for (let tempA = instA; tempA; tempA = getParent(tempA)) {
depthA++;
}
let depthB = 0;
for (let tempB = instB; tempB; tempB = getParent(tempB)) {
depthB++;
}
// If A is deeper, crawl up.
while (depthA - depthB > 0) {
instA = getParent(instA);
depthA--;
}
// If B is deeper, crawl up.
while (depthB - depthA > 0) {
instB = getParent(instB);
depthB--;
}
// Walk in lockstep until we find a match.
let depth = depthA;
while (depth--) {
if (instA === instB || instA === instB.alternate) {
return instA;
}
instA = getParent(instA);
instB = getParent(instB);
}
return null;
}
/**
* Return if A is an ancestor of B.
*/
function isAncestor(instA, instB) {
while (instB) {
if (instA === instB || instA === instB.alternate) {
return true;
}
instB = getParent(instB);
}
return false;
}
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
function traverseTwoPhase(inst, fn, arg) {
const path = [];
while (inst) {
path.push(inst);
inst = getParent(inst);
}
let i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
function getListener(inst, registrationName) {
const stateNode = inst.stateNode;
if (stateNode === null) {
// Work in progress (ex: onload events in incremental mode).
return null;
}
const props = getFiberCurrentPropsFromNode(stateNode);
if (props === null) {
// Work in progress.
return null;
}
const listener = props[registrationName];
invariant(
!listener || typeof listener === 'function',
'Expected `%s` listener to be a function, instead got a value of `%s` type.',
registrationName,
typeof listener,
);
return listener;
}
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
const registrationName =
event.dispatchConfig.phasedRegistrationNames[propagationPhase];
return getListener(inst, registrationName);
}
function accumulateDirectionalDispatches(inst, phase, event) {
if (__DEV__) {
if (!inst) {
console.error('Dispatching inst must not be null');
}
}
const listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
/**
* Accumulates without regard to direction, does not look for phased
* registration names. Same as `accumulateDirectDispatchesSingle` but without
* requiring that the `dispatchMarker` be the same as the dispatched ID.
*/
function accumulateDispatches(
inst: Object,
ignoredDirection: ?boolean,
event: Object,
): void {
if (inst && event && event.dispatchConfig.registrationName) {
const registrationName = event.dispatchConfig.registrationName;
const listener = getListener(inst, registrationName);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
}
/**
* Accumulates dispatches on an `SyntheticEvent`, but only for the
* `dispatchMarker`.
* @param {SyntheticEvent} event
*/
function accumulateDirectDispatchesSingle(event: Object) {
if (event && event.dispatchConfig.registrationName) {
accumulateDispatches(event._targetInst, null, event);
}
}
function accumulateDirectDispatches(events: ?(Array<Object> | Object)) {
forEachAccumulated(events, accumulateDirectDispatchesSingle);
}
function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
const targetInst = event._targetInst;
const parentInst = targetInst ? getParent(targetInst) : null;
traverseTwoPhase(parentInst, accumulateDirectionalDispatches, event);
}
}
function accumulateTwoPhaseDispatchesSkipTarget(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
}
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
}
function accumulateTwoPhaseDispatches(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
// End of inline
/**
*
* Responder System:
* ----------------
*
* - A global, solitary "interaction lock" on a view.
* - If a node becomes the responder, it should convey visual feedback
* immediately to indicate so, either by highlighting or moving accordingly.
* - To be the responder means, that touches are exclusively important to that
* responder view, and no other view.
* - While touches are still occurring, the responder lock can be transferred to
* a new view, but only to increasingly "higher" views (meaning ancestors of
* the current responder).
*
* Responder being granted:
* ------------------------
*
* - Touch starts, moves, and scrolls can cause an ID to become the responder.
* - We capture/bubble `startShouldSetResponder`/`moveShouldSetResponder` to
* the "appropriate place".
* - If nothing is currently the responder, the "appropriate place" is the
* initiating event's `targetID`.
* - If something *is* already the responder, the "appropriate place" is the
* first common ancestor of the event target and the current `responderInst`.
* - Some negotiation happens: See the timing diagram below.
* - Scrolled views automatically become responder. The reasoning is that a
* platform scroll view that isn't built on top of the responder system has
* began scrolling, and the active responder must now be notified that the
* interaction is no longer locked to it - the system has taken over.
*
* - Responder being released:
* As soon as no more touches that *started* inside of descendants of the
* *current* responderInst, an `onResponderRelease` event is dispatched to the
* current responder, and the responder lock is released.
*
* TODO:
* - on "end", a callback hook for `onResponderEndShouldRemainResponder` that
* determines if the responder lock should remain.
* - If a view shouldn't "remain" the responder, any active touches should by
* default be considered "dead" and do not influence future negotiations or
* bubble paths. It should be as if those touches do not exist.
* -- For multitouch: Usually a translate-z will choose to "remain" responder
* after one out of many touches ended. For translate-y, usually the view
* doesn't wish to "remain" responder after one of many touches end.
* - Consider building this on top of a `stopPropagation` model similar to
* `W3C` events.
* - Ensure that `onResponderTerminate` is called on touch cancels, whether or
* not `onResponderTerminationRequest` returns `true` or `false`.
*
*/
/* Negotiation Performed
+-----------------------+
/ \
Process low level events to + Current Responder + wantsResponderID
determine who to perform negot-| (if any exists at all) |
iation/transition | Otherwise just pass through|
-------------------------------+----------------------------+------------------+
Bubble to find first ID | |
to return true:wantsResponderID| |
| |
+-------------+ | |
| onTouchStart| | |
+------+------+ none | |
| return| |
+-----------v-------------+true| +------------------------+ |
|onStartShouldSetResponder|----->|onResponderStart (cur) |<-----------+
+-----------+-------------+ | +------------------------+ | |
| | | +--------+-------+
| returned true for| false:REJECT +-------->|onResponderReject
| wantsResponderID | | | +----------------+
| (now attempt | +------------------+-----+ |
| handoff) | | onResponder | |
+------------------->| TerminationRequest| |
| +------------------+-----+ |
| | | +----------------+
| true:GRANT +-------->|onResponderGrant|
| | +--------+-------+
| +------------------------+ | |
| | onResponderTerminate |<-----------+
| +------------------+-----+ |
| | | +----------------+
| +-------->|onResponderStart|
| | +----------------+
Bubble to find first ID | |
to return true:wantsResponderID| |
| |
+-------------+ | |
| onTouchMove | | |
+------+------+ none | |
| return| |
+-----------v-------------+true| +------------------------+ |
|onMoveShouldSetResponder |----->|onResponderMove (cur) |<-----------+
+-----------+-------------+ | +------------------------+ | |
| | | +--------+-------+
| returned true for| false:REJECT +-------->|onResponderRejec|
| wantsResponderID | | | +----------------+
| (now attempt | +------------------+-----+ |
| handoff) | | onResponder | |
+------------------->| TerminationRequest| |
| +------------------+-----+ |
| | | +----------------+
| true:GRANT +-------->|onResponderGrant|
| | +--------+-------+
| +------------------------+ | |
| | onResponderTerminate |<-----------+
| +------------------+-----+ |
| | | +----------------+
| +-------->|onResponderMove |
| | +----------------+
| |
| |
Some active touch started| |
inside current responder | +------------------------+ |
+------------------------->| onResponderEnd | |
| | +------------------------+ |
+---+---------+ | |
| onTouchEnd | | |
+---+---------+ | |
| | +------------------------+ |
+------------------------->| onResponderEnd | |
No active touches started| +-----------+------------+ |
inside current responder | | |
| v |
| +------------------------+ |
| | onResponderRelease | |
| +------------------------+ |
| |
+ + */
/**
* A note about event ordering in the `EventPluginRegistry`.
*
* Suppose plugins are injected in the following order:
*
* `[R, S, C]`
*
* To help illustrate the example, assume `S` is `SimpleEventPlugin` (for
* `onClick` etc) and `R` is `ResponderEventPlugin`.
*
* "Deferred-Dispatched Events":
*
* - The current event plugin system will traverse the list of injected plugins,
* in order, and extract events by collecting the plugin's return value of
* `extractEvents()`.
* - These events that are returned from `extractEvents` are "deferred
* dispatched events".
* - When returned from `extractEvents`, deferred-dispatched events contain an
* "accumulation" of deferred dispatches.
* - These deferred dispatches are accumulated/collected before they are
* returned, but processed at a later time by the `EventPluginRegistry` (hence the
* name deferred).
*
* In the process of returning their deferred-dispatched events, event plugins
* themselves can dispatch events on-demand without returning them from
* `extractEvents`. Plugins might want to do this, so that they can use event
* dispatching as a tool that helps them decide which events should be extracted
* in the first place.
*
* "On-Demand-Dispatched Events":
*
* - On-demand-dispatched events are not returned from `extractEvents`.
* - On-demand-dispatched events are dispatched during the process of returning
* the deferred-dispatched events.
* - They should not have side effects.
* - They should be avoided, and/or eventually be replaced with another
* abstraction that allows event plugins to perform multiple "rounds" of event
* extraction.
*
* Therefore, the sequence of event dispatches becomes:
*
* - `R`s on-demand events (if any) (dispatched by `R` on-demand)
* - `S`s on-demand events (if any) (dispatched by `S` on-demand)
* - `C`s on-demand events (if any) (dispatched by `C` on-demand)
* - `R`s extracted events (if any) (dispatched by `EventPluginRegistry`)
* - `S`s extracted events (if any) (dispatched by `EventPluginRegistry`)
* - `C`s extracted events (if any) (dispatched by `EventPluginRegistry`)
*
* In the case of `ResponderEventPlugin`: If the `startShouldSetResponder`
* on-demand dispatch returns `true` (and some other details are satisfied) the
* `onResponderGrant` deferred dispatched event is returned from
* `extractEvents`. The sequence of dispatch executions in this case
* will appear as follows:
*
* - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand)
* - `touchStartCapture` (`EventPluginRegistry` dispatches as usual)
* - `touchStart` (`EventPluginRegistry` dispatches as usual)
* - `responderGrant/Reject` (`EventPluginRegistry` dispatches as usual)
*/
function setResponderAndExtractTransfer(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
const shouldSetEventType = isStartish(topLevelType)
? eventTypes.startShouldSetResponder
: isMoveish(topLevelType)
? eventTypes.moveShouldSetResponder
: topLevelType === TOP_SELECTION_CHANGE
? eventTypes.selectionChangeShouldSetResponder
: eventTypes.scrollShouldSetResponder;
// TODO: stop one short of the current responder.
const bubbleShouldSetFrom = !responderInst
? targetInst
: getLowestCommonAncestor(responderInst, targetInst);
// When capturing/bubbling the "shouldSet" event, we want to skip the target
// (deepest ID) if it happens to be the current responder. The reasoning:
// It's strange to get an `onMoveShouldSetResponder` when you're *already*
// the responder.
const skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderInst;
const shouldSetEvent = ResponderSyntheticEvent.getPooled(
shouldSetEventType,
bubbleShouldSetFrom,
nativeEvent,
nativeEventTarget,
);
shouldSetEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
if (skipOverBubbleShouldSetFrom) {
accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent);
} else {
accumulateTwoPhaseDispatches(shouldSetEvent);
}
const wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent);
if (!shouldSetEvent.isPersistent()) {
shouldSetEvent.constructor.release(shouldSetEvent);
}
if (!wantsResponderInst || wantsResponderInst === responderInst) {
return null;
}
let extracted;
const grantEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderGrant,
wantsResponderInst,
nativeEvent,
nativeEventTarget,
);
grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(grantEvent);
const blockHostResponder = executeDirectDispatch(grantEvent) === true;
if (responderInst) {
const terminationRequestEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderTerminationRequest,
responderInst,
nativeEvent,
nativeEventTarget,
);
terminationRequestEvent.touchHistory =
ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(terminationRequestEvent);
const shouldSwitch =
!hasDispatches(terminationRequestEvent) ||
executeDirectDispatch(terminationRequestEvent);
if (!terminationRequestEvent.isPersistent()) {
terminationRequestEvent.constructor.release(terminationRequestEvent);
}
if (shouldSwitch) {
const terminateEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderTerminate,
responderInst,
nativeEvent,
nativeEventTarget,
);
terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(terminateEvent);
extracted = accumulate(extracted, [grantEvent, terminateEvent]);
changeResponder(wantsResponderInst, blockHostResponder);
} else {
const rejectEvent = ResponderSyntheticEvent.getPooled(
eventTypes.responderReject,
wantsResponderInst,
nativeEvent,
nativeEventTarget,
);
rejectEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(rejectEvent);
extracted = accumulate(extracted, rejectEvent);
}
} else {
extracted = accumulate(extracted, grantEvent);
changeResponder(wantsResponderInst, blockHostResponder);
}
return extracted;
}
/**
* A transfer is a negotiation between a currently set responder and the next
* element to claim responder status. Any start event could trigger a transfer
* of responderInst. Any move event could trigger a transfer.
*
* @param {string} topLevelType Record from `BrowserEventConstants`.
* @return {boolean} True if a transfer of responder could possibly occur.
*/
function canTriggerTransfer(topLevelType, topLevelInst, nativeEvent) {
return (
topLevelInst &&
// responderIgnoreScroll: We are trying to migrate away from specifically
// tracking native scroll events here and responderIgnoreScroll indicates we
// will send topTouchCancel to handle canceling touch events instead
((topLevelType === TOP_SCROLL && !nativeEvent.responderIgnoreScroll) ||
(trackedTouchCount > 0 && topLevelType === TOP_SELECTION_CHANGE) ||
isStartish(topLevelType) ||
isMoveish(topLevelType))
);
}
/**
* Returns whether or not this touch end event makes it such that there are no
* longer any touches that started inside of the current `responderInst`.
*
* @param {NativeEvent} nativeEvent Native touch end event.
* @return {boolean} Whether or not this touch end event ends the responder.
*/
function noResponderTouches(nativeEvent) {
const touches = nativeEvent.touches;
if (!touches || touches.length === 0) {
return true;
}
for (let i = 0; i < touches.length; i++) {
const activeTouch = touches[i];
const target = activeTouch.target;
if (target !== null && target !== undefined && target !== 0) {
// Is the original touch location inside of the current responder?
const targetInst = getInstanceFromNode(target);
if (isAncestor(responderInst, targetInst)) {
return false;
}
}
}
return true;
}
const ResponderEventPlugin = {
/* For unit testing only */
_getResponder: function() {
return responderInst;
},
eventTypes: eventTypes,
/**
* We must be resilient to `targetInst` being `null` on `touchMove` or
* `touchEnd`. On certain platforms, this means that a native scroll has
* assumed control and the original touch targets are destroyed.
*/
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
) {
if (isStartish(topLevelType)) {
trackedTouchCount += 1;
} else if (isEndish(topLevelType)) {
if (trackedTouchCount >= 0) {
trackedTouchCount -= 1;
} else {
if (__DEV__) {
console.warn(
'Ended a touch event which was not counted in `trackedTouchCount`.',
);
}
return null;
}
}
ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);
let extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent)
? setResponderAndExtractTransfer(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
)
: null;
// Responder may or may not have transferred on a new touch start/move.
// Regardless, whoever is the responder after any potential transfer, we
// direct all touch start/move/ends to them in the form of
// `onResponderMove/Start/End`. These will be called for *every* additional
// finger that move/start/end, dispatched directly to whoever is the
// current responder at that moment, until the responder is "released".
//
// These multiple individual change touch events are are always bookended
// by `onResponderGrant`, and one of
// (`onResponderRelease/onResponderTerminate`).
const isResponderTouchStart = responderInst && isStartish(topLevelType);
const isResponderTouchMove = responderInst && isMoveish(topLevelType);
const isResponderTouchEnd = responderInst && isEndish(topLevelType);
const incrementalTouch = isResponderTouchStart
? eventTypes.responderStart
: isResponderTouchMove
? eventTypes.responderMove
: isResponderTouchEnd
? eventTypes.responderEnd
: null;
if (incrementalTouch) {
const gesture = ResponderSyntheticEvent.getPooled(
incrementalTouch,
responderInst,
nativeEvent,
nativeEventTarget,
);
gesture.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(gesture);
extracted = accumulate(extracted, gesture);
}
const isResponderTerminate =
responderInst && topLevelType === TOP_TOUCH_CANCEL;
const isResponderRelease =
responderInst &&
!isResponderTerminate &&
isEndish(topLevelType) &&
noResponderTouches(nativeEvent);
const finalTouch = isResponderTerminate
? eventTypes.responderTerminate
: isResponderRelease
? eventTypes.responderRelease
: null;
if (finalTouch) {
const finalEvent = ResponderSyntheticEvent.getPooled(
finalTouch,
responderInst,
nativeEvent,
nativeEventTarget,
);
finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;
accumulateDirectDispatches(finalEvent);
extracted = accumulate(extracted, finalEvent);
changeResponder(null);
}
return extracted;
},
GlobalResponderHandler: null,
injection: {
/**
* @param {{onChange: (ReactID, ReactID) => void} GlobalResponderHandler
* Object that handles any change in responder. Use this to inject
* integration with an existing touch handling system etc.
*/
injectGlobalResponderHandler(GlobalResponderHandler) {
ResponderEventPlugin.GlobalResponderHandler = GlobalResponderHandler;
},
},
};
export default ResponderEventPlugin;
@@ -1,21 +0,0 @@
/**
* 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.
*/
import SyntheticEvent from './SyntheticEvent';
/**
* `touchHistory` isn't actually on the native event, but putting it in the
* interface will ensure that it is cleaned up when pooled/destroyed. The
* `ResponderEventPlugin` will populate it appropriately.
*/
const ResponderSyntheticEvent = SyntheticEvent.extend({
touchHistory: function(nativeEvent) {
return null; // Actually doesn't even look at the native event.
},
});
export default ResponderSyntheticEvent;
@@ -1,31 +0,0 @@
/**
* 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.
*
* @flow
*/
export const TOP_TOUCH_START = 'topTouchStart';
export const TOP_TOUCH_MOVE = 'topTouchMove';
export const TOP_TOUCH_END = 'topTouchEnd';
export const TOP_TOUCH_CANCEL = 'topTouchCancel';
export const TOP_SCROLL = 'topScroll';
export const TOP_SELECTION_CHANGE = 'topSelectionChange';
export function isStartish(topLevelType: mixed): boolean {
return topLevelType === TOP_TOUCH_START;
}
export function isMoveish(topLevelType: mixed): boolean {
return topLevelType === TOP_TOUCH_MOVE;
}
export function isEndish(topLevelType: mixed): boolean {
return topLevelType === TOP_TOUCH_END || topLevelType === TOP_TOUCH_CANCEL;
}
export const startDependencies = [TOP_TOUCH_START];
export const moveDependencies = [TOP_TOUCH_MOVE];
export const endDependencies = [TOP_TOUCH_CANCEL, TOP_TOUCH_END];
@@ -1,222 +0,0 @@
/**
* 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.
*
* @flow
*/
import invariant from 'shared/invariant';
import {isStartish, isMoveish, isEndish} from './ResponderTopLevelEventTypes';
/**
* Tracks the position and time of each active touch by `touch.identifier`. We
* should typically only see IDs in the range of 1-20 because IDs get recycled
* when touches end and start again.
*/
type TouchRecord = {|
touchActive: boolean,
startPageX: number,
startPageY: number,
startTimeStamp: number,
currentPageX: number,
currentPageY: number,
currentTimeStamp: number,
previousPageX: number,
previousPageY: number,
previousTimeStamp: number,
|};
const MAX_TOUCH_BANK = 20;
const touchBank: Array<TouchRecord> = [];
const touchHistory = {
touchBank,
numberActiveTouches: 0,
// If there is only one active touch, we remember its location. This prevents
// us having to loop through all of the touches all the time in the most
// common case.
indexOfSingleActiveTouch: -1,
mostRecentTimeStamp: 0,
};
type Touch = {
identifier: ?number,
pageX: number,
pageY: number,
timestamp: number,
...
};
type TouchEvent = {
changedTouches: Array<Touch>,
touches: Array<Touch>,
...
};
function timestampForTouch(touch: Touch): number {
// The legacy internal implementation provides "timeStamp", which has been
// renamed to "timestamp". Let both work for now while we iron it out
// TODO (evv): rename timeStamp to timestamp in internal code
return (touch: any).timeStamp || touch.timestamp;
}
/**
* TODO: Instead of making gestures recompute filtered velocity, we could
* include a built in velocity computation that can be reused globally.
*/
function createTouchRecord(touch: Touch): TouchRecord {
return {
touchActive: true,
startPageX: touch.pageX,
startPageY: touch.pageY,
startTimeStamp: timestampForTouch(touch),
currentPageX: touch.pageX,
currentPageY: touch.pageY,
currentTimeStamp: timestampForTouch(touch),
previousPageX: touch.pageX,
previousPageY: touch.pageY,
previousTimeStamp: timestampForTouch(touch),
};
}
function resetTouchRecord(touchRecord: TouchRecord, touch: Touch): void {
touchRecord.touchActive = true;
touchRecord.startPageX = touch.pageX;
touchRecord.startPageY = touch.pageY;
touchRecord.startTimeStamp = timestampForTouch(touch);
touchRecord.currentPageX = touch.pageX;
touchRecord.currentPageY = touch.pageY;
touchRecord.currentTimeStamp = timestampForTouch(touch);
touchRecord.previousPageX = touch.pageX;
touchRecord.previousPageY = touch.pageY;
touchRecord.previousTimeStamp = timestampForTouch(touch);
}
function getTouchIdentifier({identifier}: Touch): number {
invariant(identifier != null, 'Touch object is missing identifier.');
if (__DEV__) {
if (identifier > MAX_TOUCH_BANK) {
console.error(
'Touch identifier %s is greater than maximum supported %s which causes ' +
'performance issues backfilling array locations for all of the indices.',
identifier,
MAX_TOUCH_BANK,
);
}
}
return identifier;
}
function recordTouchStart(touch: Touch): void {
const identifier = getTouchIdentifier(touch);
const touchRecord = touchBank[identifier];
if (touchRecord) {
resetTouchRecord(touchRecord, touch);
} else {
touchBank[identifier] = createTouchRecord(touch);
}
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
}
function recordTouchMove(touch: Touch): void {
const touchRecord = touchBank[getTouchIdentifier(touch)];
if (touchRecord) {
touchRecord.touchActive = true;
touchRecord.previousPageX = touchRecord.currentPageX;
touchRecord.previousPageY = touchRecord.currentPageY;
touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
touchRecord.currentPageX = touch.pageX;
touchRecord.currentPageY = touch.pageY;
touchRecord.currentTimeStamp = timestampForTouch(touch);
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
} else {
if (__DEV__) {
console.warn(
'Cannot record touch move without a touch start.\n' +
'Touch Move: %s\n' +
'Touch Bank: %s',
printTouch(touch),
printTouchBank(),
);
}
}
}
function recordTouchEnd(touch: Touch): void {
const touchRecord = touchBank[getTouchIdentifier(touch)];
if (touchRecord) {
touchRecord.touchActive = false;
touchRecord.previousPageX = touchRecord.currentPageX;
touchRecord.previousPageY = touchRecord.currentPageY;
touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
touchRecord.currentPageX = touch.pageX;
touchRecord.currentPageY = touch.pageY;
touchRecord.currentTimeStamp = timestampForTouch(touch);
touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
} else {
if (__DEV__) {
console.warn(
'Cannot record touch end without a touch start.\n' +
'Touch End: %s\n' +
'Touch Bank: %s',
printTouch(touch),
printTouchBank(),
);
}
}
}
function printTouch(touch: Touch): string {
return JSON.stringify({
identifier: touch.identifier,
pageX: touch.pageX,
pageY: touch.pageY,
timestamp: timestampForTouch(touch),
});
}
function printTouchBank(): string {
let printed = JSON.stringify(touchBank.slice(0, MAX_TOUCH_BANK));
if (touchBank.length > MAX_TOUCH_BANK) {
printed += ' (original size: ' + touchBank.length + ')';
}
return printed;
}
const ResponderTouchHistoryStore = {
recordTouchTrack(topLevelType: string, nativeEvent: TouchEvent): void {
if (isMoveish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordTouchMove);
} else if (isStartish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordTouchStart);
touchHistory.numberActiveTouches = nativeEvent.touches.length;
if (touchHistory.numberActiveTouches === 1) {
touchHistory.indexOfSingleActiveTouch =
nativeEvent.touches[0].identifier;
}
} else if (isEndish(topLevelType)) {
nativeEvent.changedTouches.forEach(recordTouchEnd);
touchHistory.numberActiveTouches = nativeEvent.touches.length;
if (touchHistory.numberActiveTouches === 1) {
for (let i = 0; i < touchBank.length; i++) {
const touchTrackToCheck = touchBank[i];
if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
touchHistory.indexOfSingleActiveTouch = i;
break;
}
}
if (__DEV__) {
const activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch];
if (activeRecord == null || !activeRecord.touchActive) {
console.error('Cannot find single active touch.');
}
}
}
}
},
touchHistory,
};
export default ResponderTouchHistoryStore;
+4 -149
View File
@@ -7,11 +7,6 @@
/* eslint valid-typeof: 0 */
import invariant from 'shared/invariant';
import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
const EVENT_POOL_SIZE = 10;
/**
* @interface Event
* @see http://www.w3.org/TR/DOM-Level-3-Events/
@@ -77,10 +72,6 @@ function SyntheticEvent(
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;
if (!enableModernEventSystem) {
this._dispatchListeners = null;
this._dispatchInstances = null;
}
const Interface = this.constructor.Interface;
for (const propName in Interface) {
@@ -158,9 +149,6 @@ Object.assign(SyntheticEvent.prototype, {
*/
persist: function() {
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
this.isPersistent = functionThatReturnsTrue;
}
},
/**
@@ -168,69 +156,14 @@ Object.assign(SyntheticEvent.prototype, {
*
* @return {boolean} True if this should not be released, false otherwise.
*/
isPersistent: enableModernEventSystem
? functionThatReturnsTrue
: functionThatReturnsFalse,
isPersistent: functionThatReturnsTrue,
/**
* `PooledClass` looks for `destructor` on each instance it releases.
*/
destructor: function() {
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
const Interface = this.constructor.Interface;
for (const propName in Interface) {
if (__DEV__) {
Object.defineProperty(
this,
propName,
getPooledWarningPropertyDefinition(propName, Interface[propName]),
);
} else {
this[propName] = null;
}
}
this.dispatchConfig = null;
this._targetInst = null;
this.nativeEvent = null;
this.isDefaultPrevented = functionThatReturnsFalse;
this.isPropagationStopped = functionThatReturnsFalse;
this._dispatchListeners = null;
this._dispatchInstances = null;
if (__DEV__) {
Object.defineProperty(
this,
'nativeEvent',
getPooledWarningPropertyDefinition('nativeEvent', null),
);
Object.defineProperty(
this,
'isDefaultPrevented',
getPooledWarningPropertyDefinition(
'isDefaultPrevented',
functionThatReturnsFalse,
),
);
Object.defineProperty(
this,
'isPropagationStopped',
getPooledWarningPropertyDefinition(
'isPropagationStopped',
functionThatReturnsFalse,
),
);
Object.defineProperty(
this,
'preventDefault',
getPooledWarningPropertyDefinition('preventDefault', () => {}),
);
Object.defineProperty(
this,
'stopPropagation',
getPooledWarningPropertyDefinition('stopPropagation', () => {}),
);
}
}
// TODO: remove calls to this.
},
});
@@ -262,53 +195,6 @@ SyntheticEvent.extend = function(Interface) {
addEventPoolingTo(SyntheticEvent);
/**
* Helper to nullify syntheticEvent instance properties when destructing
*
* @param {String} propName
* @param {?object} getVal
* @return {object} defineProperty object
*/
function getPooledWarningPropertyDefinition(propName, getVal) {
const isFunction = typeof getVal === 'function';
return {
configurable: true,
set: set,
get: get,
};
function set(val) {
const action = isFunction ? 'setting the method' : 'setting the property';
warn(action, 'This is effectively a no-op');
return val;
}
function get() {
const action = isFunction
? 'accessing the method'
: 'accessing the property';
const result = isFunction
? 'This is a no-op function'
: 'This is set to null';
warn(action, result);
return getVal;
}
function warn(action, result) {
if (__DEV__) {
console.error(
"This synthetic event is reused for performance reasons. If you're seeing this, " +
"you're %s `%s` on a released/nullified synthetic event. %s. " +
'If you must keep the original synthetic event around, use event.persist(). ' +
'See https://fb.me/react-event-pooling for more information.',
action,
propName,
result,
);
}
}
}
function createOrGetPooledEvent(
dispatchConfig,
targetInst,
@@ -317,19 +203,7 @@ function createOrGetPooledEvent(
) {
const EventConstructor = this;
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
if (EventConstructor.eventPool.length) {
const instance = EventConstructor.eventPool.pop();
EventConstructor.call(
instance,
dispatchConfig,
targetInst,
nativeEvent,
nativeInst,
);
return instance;
}
}
// TODO: remove this indirection.
return new EventConstructor(
dispatchConfig,
targetInst,
@@ -338,29 +212,10 @@ function createOrGetPooledEvent(
);
}
function releasePooledEvent(event) {
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
const EventConstructor = this;
invariant(
event instanceof EventConstructor,
'Trying to release an event instance into a pool of a different type.',
);
event.destructor();
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event);
}
}
}
function addEventPoolingTo(EventConstructor) {
EventConstructor.getPooled = createOrGetPooledEvent;
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
EventConstructor.eventPool = [];
EventConstructor.release = releasePooledEvent;
}
// TODO: remove calls to this.
}
export default SyntheticEvent;
-45
View File
@@ -1,45 +0,0 @@
/**
* 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.
*
* @flow
*/
import invariant from 'shared/invariant';
/**
* Accumulates items that must not be null or undefined.
*
* This is used to conserve memory by avoiding array allocations.
*
* @return {*|array<*>} An accumulation of items.
*/
function accumulate<T>(
current: ?(T | Array<T>),
next: T | Array<T>,
): T | Array<T> {
invariant(
next != null,
'accumulate(...): Accumulated items must not be null or undefined.',
);
if (current == null) {
return next;
}
// Both are not empty. Warning: Never call x.concat(y) when you are not
// certain that x is an Array (x could be a string with concat method).
if (Array.isArray(current)) {
return current.concat(next);
}
if (Array.isArray(next)) {
return [current].concat(next);
}
return [current, next];
}
export default accumulate;
+3 -173
View File
@@ -18,17 +18,14 @@ import {
import SyntheticEvent from '../legacy-events/SyntheticEvent';
import invariant from 'shared/invariant';
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
import * as DOMTopLevelEventTypes from '../events/DOMTopLevelEventTypes';
import act from './ReactTestUtilsAct';
import forEachAccumulated from '../legacy-events/forEachAccumulated';
import accumulateInto from '../legacy-events/accumulateInto';
import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
import {
rethrowCaughtError,
invokeGuardedCallbackAndCatchFirstError,
} from 'shared/ReactErrorUtils';
const {findDOMNode} = ReactDOM;
// Keep in sync with ReactDOM.js, and ReactTestUtilsAct.js:
const [
getInstanceFromNode,
@@ -39,10 +36,9 @@ const [
/* eslint-enable no-unused-vars */
eventNameDispatchConfigs,
enqueueStateRestore,
restoreStateIfNeeded,
dispatchEvent,
/* eslint-disable no-unused-vars */
flushPassiveEffects,
restoreStateIfNeeded /* eslint-disable no-unused-vars */, // TODO: remove.
,
/* dispatchEvent */ flushPassiveEffects,
IsThisRendererActing,
/* eslint-enable no-unused-vars */
] = ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.Events;
@@ -50,7 +46,6 @@ const [
function Event(suffix) {}
let hasWarnedAboutDeprecatedMockComponent = false;
let didWarnSimulateNativeDeprecated = false;
/**
* @class ReactTestUtils
@@ -667,171 +662,6 @@ function buildSimulators() {
buildSimulators();
if (!enableModernEventSystem) {
SimulateNative = {};
/**
* Simulates a top level event being dispatched from a raw event that occurred
* on an `Element` node.
* @param {number} topLevelType A number from `TopLevelEventTypes`
* @param {!Element} node The dom to simulate an event occurring on.
* @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
*/
const simulateNativeEventOnNode = function(
topLevelType,
node,
fakeNativeEvent,
) {
fakeNativeEvent.target = node;
const PLUGIN_EVENT_SYSTEM = 1;
dispatchEvent(topLevelType, PLUGIN_EVENT_SYSTEM, null, fakeNativeEvent);
};
/**
* Simulates a top level event being dispatched from a raw event that occurred
* on the `ReactDOMComponent` `comp`.
* @param {Object} topLevelType A type from `BrowserEventConstants.topLevelTypes`.
* @param {!ReactDOMComponent} comp
* @param {?Event} fakeNativeEvent Fake native event to use in SyntheticEvent.
*/
const simulateNativeEventOnDOMComponent = function(
topLevelType,
comp,
fakeNativeEvent,
) {
simulateNativeEventOnNode(topLevelType, findDOMNode(comp), fakeNativeEvent);
};
/**
* Exports:
*
* - `SimulateNative.click(Element/ReactDOMComponent)`
* - `SimulateNative.mouseMove(Element/ReactDOMComponent)`
* - `SimulateNative.mouseIn/ReactDOMComponent)`
* - `SimulateNative.mouseOut(Element/ReactDOMComponent)`
* - ... (All keys from `BrowserEventConstants.topLevelTypes`)
*
* Note: Top level event types are a subset of the entire set of handler types
* (which include a broader set of "synthetic" events). For example, onDragDone
* is a synthetic event. Except when testing an event plugin or React's event
* handling code specifically, you probably want to use Simulate
* to dispatch synthetic events.
*/
const makeNativeSimulator = function(eventType, topLevelType) {
return function(domComponentOrNode, nativeEventData) {
if (__DEV__) {
if (!didWarnSimulateNativeDeprecated) {
didWarnSimulateNativeDeprecated = true;
console.warn(
'ReactTestUtils.SimulateNative is an undocumented API that does not match ' +
'how the browser dispatches events, and will be removed in a future major ' +
'version of React. If you rely on it for testing, consider attaching the root ' +
'DOM container to the document during the test, and then dispatching native browser ' +
'events by calling `node.dispatchEvent()` on the DOM nodes. Make sure to set ' +
'the `bubbles` flag to `true` when creating the native browser event.',
);
}
}
const fakeNativeEvent = new Event(eventType);
Object.assign(fakeNativeEvent, nativeEventData);
if (isDOMComponent(domComponentOrNode)) {
simulateNativeEventOnDOMComponent(
topLevelType,
domComponentOrNode,
fakeNativeEvent,
);
} else if (domComponentOrNode.tagName) {
// Will allow on actual dom nodes.
simulateNativeEventOnNode(
topLevelType,
domComponentOrNode,
fakeNativeEvent,
);
}
};
};
[
[DOMTopLevelEventTypes.TOP_ABORT, 'abort'],
[DOMTopLevelEventTypes.TOP_ANIMATION_END, 'animationEnd'],
[DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION, 'animationIteration'],
[DOMTopLevelEventTypes.TOP_ANIMATION_START, 'animationStart'],
[DOMTopLevelEventTypes.TOP_BLUR, 'blur'],
[DOMTopLevelEventTypes.TOP_CAN_PLAY_THROUGH, 'canPlayThrough'],
[DOMTopLevelEventTypes.TOP_CAN_PLAY, 'canPlay'],
[DOMTopLevelEventTypes.TOP_CANCEL, 'cancel'],
[DOMTopLevelEventTypes.TOP_CHANGE, 'change'],
[DOMTopLevelEventTypes.TOP_CLICK, 'click'],
[DOMTopLevelEventTypes.TOP_CLOSE, 'close'],
[DOMTopLevelEventTypes.TOP_COMPOSITION_END, 'compositionEnd'],
[DOMTopLevelEventTypes.TOP_COMPOSITION_START, 'compositionStart'],
[DOMTopLevelEventTypes.TOP_COMPOSITION_UPDATE, 'compositionUpdate'],
[DOMTopLevelEventTypes.TOP_CONTEXT_MENU, 'contextMenu'],
[DOMTopLevelEventTypes.TOP_COPY, 'copy'],
[DOMTopLevelEventTypes.TOP_CUT, 'cut'],
[DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, 'doubleClick'],
[DOMTopLevelEventTypes.TOP_DRAG_END, 'dragEnd'],
[DOMTopLevelEventTypes.TOP_DRAG_ENTER, 'dragEnter'],
[DOMTopLevelEventTypes.TOP_DRAG_EXIT, 'dragExit'],
[DOMTopLevelEventTypes.TOP_DRAG_LEAVE, 'dragLeave'],
[DOMTopLevelEventTypes.TOP_DRAG_OVER, 'dragOver'],
[DOMTopLevelEventTypes.TOP_DRAG_START, 'dragStart'],
[DOMTopLevelEventTypes.TOP_DRAG, 'drag'],
[DOMTopLevelEventTypes.TOP_DROP, 'drop'],
[DOMTopLevelEventTypes.TOP_DURATION_CHANGE, 'durationChange'],
[DOMTopLevelEventTypes.TOP_EMPTIED, 'emptied'],
[DOMTopLevelEventTypes.TOP_ENCRYPTED, 'encrypted'],
[DOMTopLevelEventTypes.TOP_ENDED, 'ended'],
[DOMTopLevelEventTypes.TOP_ERROR, 'error'],
[DOMTopLevelEventTypes.TOP_FOCUS, 'focus'],
[DOMTopLevelEventTypes.TOP_INPUT, 'input'],
[DOMTopLevelEventTypes.TOP_KEY_DOWN, 'keyDown'],
[DOMTopLevelEventTypes.TOP_KEY_PRESS, 'keyPress'],
[DOMTopLevelEventTypes.TOP_KEY_UP, 'keyUp'],
[DOMTopLevelEventTypes.TOP_LOAD_START, 'loadStart'],
[DOMTopLevelEventTypes.TOP_LOAD_START, 'loadStart'],
[DOMTopLevelEventTypes.TOP_LOAD, 'load'],
[DOMTopLevelEventTypes.TOP_LOADED_DATA, 'loadedData'],
[DOMTopLevelEventTypes.TOP_LOADED_METADATA, 'loadedMetadata'],
[DOMTopLevelEventTypes.TOP_MOUSE_DOWN, 'mouseDown'],
[DOMTopLevelEventTypes.TOP_MOUSE_MOVE, 'mouseMove'],
[DOMTopLevelEventTypes.TOP_MOUSE_OUT, 'mouseOut'],
[DOMTopLevelEventTypes.TOP_MOUSE_OVER, 'mouseOver'],
[DOMTopLevelEventTypes.TOP_MOUSE_UP, 'mouseUp'],
[DOMTopLevelEventTypes.TOP_PASTE, 'paste'],
[DOMTopLevelEventTypes.TOP_PAUSE, 'pause'],
[DOMTopLevelEventTypes.TOP_PLAY, 'play'],
[DOMTopLevelEventTypes.TOP_PLAYING, 'playing'],
[DOMTopLevelEventTypes.TOP_PROGRESS, 'progress'],
[DOMTopLevelEventTypes.TOP_RATE_CHANGE, 'rateChange'],
[DOMTopLevelEventTypes.TOP_SCROLL, 'scroll'],
[DOMTopLevelEventTypes.TOP_SEEKED, 'seeked'],
[DOMTopLevelEventTypes.TOP_SEEKING, 'seeking'],
[DOMTopLevelEventTypes.TOP_SELECTION_CHANGE, 'selectionChange'],
[DOMTopLevelEventTypes.TOP_STALLED, 'stalled'],
[DOMTopLevelEventTypes.TOP_SUSPEND, 'suspend'],
[DOMTopLevelEventTypes.TOP_TEXT_INPUT, 'textInput'],
[DOMTopLevelEventTypes.TOP_TIME_UPDATE, 'timeUpdate'],
[DOMTopLevelEventTypes.TOP_TOGGLE, 'toggle'],
[DOMTopLevelEventTypes.TOP_TOUCH_CANCEL, 'touchCancel'],
[DOMTopLevelEventTypes.TOP_TOUCH_END, 'touchEnd'],
[DOMTopLevelEventTypes.TOP_TOUCH_MOVE, 'touchMove'],
[DOMTopLevelEventTypes.TOP_TOUCH_START, 'touchStart'],
[DOMTopLevelEventTypes.TOP_TRANSITION_END, 'transitionEnd'],
[DOMTopLevelEventTypes.TOP_VOLUME_CHANGE, 'volumeChange'],
[DOMTopLevelEventTypes.TOP_WAITING, 'waiting'],
[DOMTopLevelEventTypes.TOP_WHEEL, 'wheel'],
].forEach(([topLevelType, eventType]) => {
/**
* @param {!Element|ReactDOMComponent} domComponentOrNode
* @param {?Event} nativeEventData Fake native event to use in SyntheticEvent.
*/
SimulateNative[eventType] = makeNativeSimulator(eventType, topLevelType);
});
}
export {
renderIntoDocument,
isElement,
@@ -21,7 +21,6 @@ function initializeModules(hasPointerEvents) {
setPointerEvent(hasPointerEvents);
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableModernEventSystem = true;
ReactFeatureFlags.enableCreateEventHandleAPI = true;
React = require('react');
ReactDOM = require('react-dom');
@@ -24,7 +24,6 @@ function initializeModules(hasPointerEvents) {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableScopeAPI = true;
ReactFeatureFlags.enableModernEventSystem = true;
ReactFeatureFlags.enableCreateEventHandleAPI = true;
React = require('react');
ReactDOM = require('react-dom');
@@ -8,7 +8,6 @@
/* eslint valid-typeof: 0 */
import invariant from 'shared/invariant';
import {enableModernEventSystem} from 'shared/ReactFeatureFlags';
const EVENT_POOL_SIZE = 10;
@@ -77,10 +76,8 @@ function SyntheticEvent(
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;
if (!enableModernEventSystem) {
this._dispatchListeners = null;
this._dispatchInstances = null;
}
this._dispatchListeners = null;
this._dispatchInstances = null;
const Interface = this.constructor.Interface;
for (const propName in Interface) {
@@ -157,10 +154,7 @@ Object.assign(SyntheticEvent.prototype, {
* won't be added back into the pool.
*/
persist: function() {
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
this.isPersistent = functionThatReturnsTrue;
}
this.isPersistent = functionThatReturnsTrue;
},
/**
@@ -168,69 +162,64 @@ Object.assign(SyntheticEvent.prototype, {
*
* @return {boolean} True if this should not be released, false otherwise.
*/
isPersistent: enableModernEventSystem
? functionThatReturnsTrue
: functionThatReturnsFalse,
isPersistent: functionThatReturnsFalse,
/**
* `PooledClass` looks for `destructor` on each instance it releases.
*/
destructor: function() {
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
const Interface = this.constructor.Interface;
for (const propName in Interface) {
if (__DEV__) {
Object.defineProperty(
this,
propName,
getPooledWarningPropertyDefinition(propName, Interface[propName]),
);
} else {
this[propName] = null;
}
}
this.dispatchConfig = null;
this._targetInst = null;
this.nativeEvent = null;
this.isDefaultPrevented = functionThatReturnsFalse;
this.isPropagationStopped = functionThatReturnsFalse;
this._dispatchListeners = null;
this._dispatchInstances = null;
const Interface = this.constructor.Interface;
for (const propName in Interface) {
if (__DEV__) {
Object.defineProperty(
this,
'nativeEvent',
getPooledWarningPropertyDefinition('nativeEvent', null),
);
Object.defineProperty(
this,
'isDefaultPrevented',
getPooledWarningPropertyDefinition(
'isDefaultPrevented',
functionThatReturnsFalse,
),
);
Object.defineProperty(
this,
'isPropagationStopped',
getPooledWarningPropertyDefinition(
'isPropagationStopped',
functionThatReturnsFalse,
),
);
Object.defineProperty(
this,
'preventDefault',
getPooledWarningPropertyDefinition('preventDefault', () => {}),
);
Object.defineProperty(
this,
'stopPropagation',
getPooledWarningPropertyDefinition('stopPropagation', () => {}),
propName,
getPooledWarningPropertyDefinition(propName, Interface[propName]),
);
} else {
this[propName] = null;
}
}
this.dispatchConfig = null;
this._targetInst = null;
this.nativeEvent = null;
this.isDefaultPrevented = functionThatReturnsFalse;
this.isPropagationStopped = functionThatReturnsFalse;
this._dispatchListeners = null;
this._dispatchInstances = null;
if (__DEV__) {
Object.defineProperty(
this,
'nativeEvent',
getPooledWarningPropertyDefinition('nativeEvent', null),
);
Object.defineProperty(
this,
'isDefaultPrevented',
getPooledWarningPropertyDefinition(
'isDefaultPrevented',
functionThatReturnsFalse,
),
);
Object.defineProperty(
this,
'isPropagationStopped',
getPooledWarningPropertyDefinition(
'isPropagationStopped',
functionThatReturnsFalse,
),
);
Object.defineProperty(
this,
'preventDefault',
getPooledWarningPropertyDefinition('preventDefault', () => {}),
);
Object.defineProperty(
this,
'stopPropagation',
getPooledWarningPropertyDefinition('stopPropagation', () => {}),
);
}
},
});
@@ -316,19 +305,16 @@ function createOrGetPooledEvent(
nativeInst,
) {
const EventConstructor = this;
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
if (EventConstructor.eventPool.length) {
const instance = EventConstructor.eventPool.pop();
EventConstructor.call(
instance,
dispatchConfig,
targetInst,
nativeEvent,
nativeInst,
);
return instance;
}
if (EventConstructor.eventPool.length) {
const instance = EventConstructor.eventPool.pop();
EventConstructor.call(
instance,
dispatchConfig,
targetInst,
nativeEvent,
nativeInst,
);
return instance;
}
return new EventConstructor(
dispatchConfig,
@@ -339,28 +325,21 @@ function createOrGetPooledEvent(
}
function releasePooledEvent(event) {
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
const EventConstructor = this;
invariant(
event instanceof EventConstructor,
'Trying to release an event instance into a pool of a different type.',
);
event.destructor();
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event);
}
const EventConstructor = this;
invariant(
event instanceof EventConstructor,
'Trying to release an event instance into a pool of a different type.',
);
event.destructor();
if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
EventConstructor.eventPool.push(event);
}
}
function addEventPoolingTo(EventConstructor) {
EventConstructor.getPooled = createOrGetPooledEvent;
// Modern event system doesn't use pooling.
if (!enableModernEventSystem) {
EventConstructor.eventPool = [];
EventConstructor.release = releasePooledEvent;
}
EventConstructor.eventPool = [];
EventConstructor.release = releasePooledEvent;
}
export default SyntheticEvent;
-3
View File
@@ -111,9 +111,6 @@ export const disableModulePatternComponents = false;
// We should remove this flag once the above flag becomes enabled
export const warnUnstableRenderSubtreeIntoContainer = false;
// Modern event system where events get registered at roots
export const enableModernEventSystem = false;
// Support legacy Primer support on internal FB www
export const enableLegacyFBSupport = false;
@@ -39,7 +39,6 @@ export const enableTrustedTypesIntegration = false;
export const disableTextareaChildren = false;
export const disableModulePatternComponents = false;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
@@ -38,7 +38,6 @@ export const enableTrustedTypesIntegration = false;
export const disableTextareaChildren = false;
export const disableModulePatternComponents = false;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
@@ -38,7 +38,6 @@ export const enableTrustedTypesIntegration = false;
export const disableTextareaChildren = false;
export const disableModulePatternComponents = false;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
@@ -38,7 +38,6 @@ export const enableTrustedTypesIntegration = false;
export const disableTextareaChildren = false;
export const disableModulePatternComponents = true;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
@@ -38,7 +38,6 @@ export const enableTrustedTypesIntegration = false;
export const disableTextareaChildren = false;
export const disableModulePatternComponents = false;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
@@ -38,7 +38,6 @@ export const enableTrustedTypesIntegration = false;
export const disableTextareaChildren = __EXPERIMENTAL__;
export const disableModulePatternComponents = true;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
@@ -71,8 +71,6 @@ export const disableTextareaChildren = __EXPERIMENTAL__;
export const warnUnstableRenderSubtreeIntoContainer = false;
export const enableModernEventSystem = true;
// Enable forked reconciler. Piggy-backing on the "variant" global so that we
// don't have to add another test dimension. The build system will compile this
// to the correct value.