mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Enable modern event system and delete dead code (#19230)
This commit is contained in:
+1
-4
@@ -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
|
||||
|
||||
@@ -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([
|
||||
|
||||
+21
-66
@@ -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', {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
-763
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
-239
@@ -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);
|
||||
});
|
||||
});
|
||||
-831
@@ -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);
|
||||
});
|
||||
});
|
||||
-1
@@ -54,7 +54,6 @@ describe('ChangeEventPlugin', () => {
|
||||
postMessageCallback(postMessageEvent);
|
||||
}
|
||||
};
|
||||
ReactFeatureFlags.enableModernEventSystem = true;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
-3
@@ -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');
|
||||
|
||||
-167
@@ -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);
|
||||
});
|
||||
});
|
||||
-493
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+3
-4
@@ -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',
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
-1
@@ -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');
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user