diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js index d8a6c6d2ff7..0a4b5a36cbf 100644 --- a/packages/react-dom/src/__tests__/ReactDOM-test.js +++ b/packages/react-dom/src/__tests__/ReactDOM-test.js @@ -357,10 +357,7 @@ describe('ReactDOM', () => { ReactDOM.render(, 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 diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index bcf7bde39c0..db4ad0e8470 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -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([ diff --git a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js index 0333fd75618..c2cec8b1cae 100644 --- a/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMEventListener-test.js @@ -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(
, 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. - - // 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'); - } + // 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'); // 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 . 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 , 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 - // . So far nobody asked us for it. - expect(handleImgLoadStart).toHaveBeenCalledTimes(0); - } + // As of the modern event system refactor, we now support + // this on . 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', { diff --git a/packages/react-dom/src/__tests__/ReactTestUtils-test.js b/packages/react-dom/src/__tests__/ReactTestUtils-test.js index a69c0465653..20940d7523e 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtils-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtils-test.js @@ -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(
, 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() { diff --git a/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js b/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js index 2b5ab93aba7..6927a212cf0 100644 --- a/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js +++ b/packages/react-dom/src/__tests__/ReactTreeTraversal-test.js @@ -11,7 +11,6 @@ let React; let ReactDOM; -const ReactFeatureFlags = require('shared/ReactFeatureFlags'); const ChildComponent = ({id, eventHandler}) => (
{ 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'); - - const expectedCalls = [ - ['P', 'mouseenter'], - ['P_P1', 'mouseenter'], - ['P_P1_C1__DIV', 'mouseenter'], - ]; - - enterNode.dispatchEvent( - new MouseEvent('mouseover', { - bubbles: true, - cancelable: true, - relatedTarget: outerNode1, - }), - ); - - expect(mockFn.mock.calls).toEqual(expectedCalls); - }); - } else { - it('should enter from the window', () => { - const enterNode = document.getElementById('P_P1_C1__DIV'); - - const expectedCalls = [ - ['P', 'mouseenter'], - ['P_P1', 'mouseenter'], - ['P_P1_C1__DIV', 'mouseenter'], - ]; - - outerNode1.dispatchEvent( - new MouseEvent('mouseout', { - bubbles: true, - cancelable: true, - relatedTarget: enterNode, - }), - ); - - expect(mockFn.mock.calls).toEqual(expectedCalls); - }); - } - - // This will not work with the modern event system that - // attaches event listeners to roots as the event below - // is being triggered on a node that React does not listen - // to any more. Instead we should fire mouseover. - if (ReactFeatureFlags.enableModernEventSystem) { - it('should enter from the window to the shallowest', () => { - const enterNode = document.getElementById('P'); - - const expectedCalls = [['P', 'mouseenter']]; - - enterNode.dispatchEvent( - new MouseEvent('mouseover', { - bubbles: true, - cancelable: true, - relatedTarget: outerNode1, - }), - ); - - expect(mockFn.mock.calls).toEqual(expectedCalls); - }); - } else { - it('should enter from the window to the shallowest', () => { - const enterNode = document.getElementById('P'); - - const expectedCalls = [['P', 'mouseenter']]; - - outerNode1.dispatchEvent( - new MouseEvent('mouseout', { - bubbles: true, - cancelable: true, - relatedTarget: enterNode, - }), - ); - - expect(mockFn.mock.calls).toEqual(expectedCalls); - }); - } + it('should enter from the window', () => { + const enterNode = document.getElementById('P_P1_C1__DIV'); + + const expectedCalls = [ + ['P', 'mouseenter'], + ['P_P1', 'mouseenter'], + ['P_P1_C1__DIV', 'mouseenter'], + ]; + + enterNode.dispatchEvent( + new MouseEvent('mouseover', { + bubbles: true, + cancelable: true, + relatedTarget: outerNode1, + }), + ); + + expect(mockFn.mock.calls).toEqual(expectedCalls); + }); + + 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); + }); it('should leave to the window', () => { const leaveNode = document.getElementById('P_P1_C1__DIV'); diff --git a/packages/react-dom/src/client/ReactDOMClientInjection.js b/packages/react-dom/src/client/ReactDOMClientInjection.js index c61d4b072c1..0f2532c1b65 100644 --- a/packages/react-dom/src/client/ReactDOMClientInjection.js +++ b/packages/react-dom/src/client/ReactDOMClientInjection.js @@ -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'; - -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, - ); +import {injectEventPlugins} from '../legacy-events/EventPluginRegistry'; - /** - * 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, +]); diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index fc16a49d7c7..af1f1184911 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -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'); diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index a7039702b8d..2c096466962 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -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( diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index 549f7c3e600..31a869ba2d7 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -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 ) { diff --git a/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js deleted file mode 100644 index fde6a1c6505..00000000000 --- a/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js +++ /dev/null @@ -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, -|}; - -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 | null { - let events = null; - const legacyPlugins = ((plugins: any): Array>); - 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).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, -): void { - forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); -} - -export function accumulateEnterLeaveDispatches( - leave: ReactSyntheticEvent, - enter: ReactSyntheticEvent, - from: Fiber, - to: Fiber, -) { - traverseEnterLeave(from, to, accumulateDispatches, leave, enter); -} diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index d906ea8b541..c83e3cd284c 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -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; diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index f544755937e..9d4cd1a9e30 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -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); }); } diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js index ce7d0069052..9e5b09e8b07 100644 --- a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js @@ -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; diff --git a/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js index adef2ac7051..4bc4d570197 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticClipboardEvent-test.js @@ -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( -
, - 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'); - }); - } }); }); diff --git a/packages/react-dom/src/events/__tests__/SyntheticEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticEvent-test.js index 718173ef7f1..78e576e4e0c 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticEvent-test.js @@ -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(
, 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(
, 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(
, 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(
, 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(
, 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(
, 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(
, 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(
, 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(
, container); - - const event = document.createEvent('Event'); - event.initEvent('click', true, true); - - node.dispatchEvent(event); - - expect(syntheticEvent.foo).toBe('bar'); - expect(expectedCount).toBe(1); - }); }); diff --git a/packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.js index fb99edef4de..6cfd34f5d09 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.js @@ -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( -
, - 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'); - }); - } }); }); diff --git a/packages/react-dom/src/events/__tests__/SyntheticWheelEvent-test.js b/packages/react-dom/src/events/__tests__/SyntheticWheelEvent-test.js index 64f496c85e2..1ae7c5b7686 100644 --- a/packages/react-dom/src/events/__tests__/SyntheticWheelEvent-test.js +++ b/packages/react-dom/src/events/__tests__/SyntheticWheelEvent-test.js @@ -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(
, container); - - container.firstChild.dispatchEvent( - new MouseEvent('wheel', { - bubbles: true, - }), - ); - - expect(events.length).toBe(1); - expect(events[0].type).toBe('wheel'); - }); - } }); diff --git a/packages/react-dom/src/events/plugins/LegacyBeforeInputEventPlugin.js b/packages/react-dom/src/events/plugins/LegacyBeforeInputEventPlugin.js deleted file mode 100644 index 1175fd56fcc..00000000000 --- a/packages/react-dom/src/events/plugins/LegacyBeforeInputEventPlugin.js +++ /dev/null @@ -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; diff --git a/packages/react-dom/src/events/plugins/LegacyChangeEventPlugin.js b/packages/react-dom/src/events/plugins/LegacyChangeEventPlugin.js deleted file mode 100644 index a03c3819a42..00000000000 --- a/packages/react-dom/src/events/plugins/LegacyChangeEventPlugin.js +++ /dev/null @@ -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; diff --git a/packages/react-dom/src/events/plugins/LegacyEnterLeaveEventPlugin.js b/packages/react-dom/src/events/plugins/LegacyEnterLeaveEventPlugin.js deleted file mode 100644 index 9b23efd8882..00000000000 --- a/packages/react-dom/src/events/plugins/LegacyEnterLeaveEventPlugin.js +++ /dev/null @@ -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; diff --git a/packages/react-dom/src/events/plugins/LegacySelectEventPlugin.js b/packages/react-dom/src/events/plugins/LegacySelectEventPlugin.js deleted file mode 100644 index 289994ed2b7..00000000000 --- a/packages/react-dom/src/events/plugins/LegacySelectEventPlugin.js +++ /dev/null @@ -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; diff --git a/packages/react-dom/src/events/plugins/LegacySimpleEventPlugin.js b/packages/react-dom/src/events/plugins/LegacySimpleEventPlugin.js deleted file mode 100644 index b4e1aa89632..00000000000 --- a/packages/react-dom/src/events/plugins/LegacySimpleEventPlugin.js +++ /dev/null @@ -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 = [ - 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 = { - // 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; diff --git a/packages/react-dom/src/events/plugins/__tests__/LegacyChangeEventPlugin-test.js b/packages/react-dom/src/events/plugins/__tests__/LegacyChangeEventPlugin-test.js deleted file mode 100644 index 5c81747996b..00000000000 --- a/packages/react-dom/src/events/plugins/__tests__/LegacyChangeEventPlugin-test.js +++ /dev/null @@ -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( - , - 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( - , - 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( - , - 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( - , - 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( - , - 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( - , - 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( - , - 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(, 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( - , - 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( -
- - -
, - 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(, 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(, 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(, 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( - , - 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( - , - 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(, div); - // Update - ReactDOM.render(, 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 = el)} - type="text" - value={controlledValue} - onChange={this.onChange} - /> - ); - } - } - - // Initial mount. Test that this is async. - root.render(); - // 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 = el)} - type="checkbox" - checked={controlledValue} - onChange={this.onChange} - /> - ); - } - } - - // Initial mount. Test that this is async. - root.render(); - // 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(); - 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 ( -