From b61174fb7b09580c1ec2a8f55e73204b706d2935 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 5 Aug 2020 15:13:29 +0100 Subject: [PATCH] Remove the deprecated React Flare event system (#19520) --- packages/react-art/src/ReactARTHostConfig.js | 20 - .../react-debug-tools/src/ReactDebugHooks.js | 19 - .../ReactHooksInspection-test.internal.js | 46 - .../src/backend/ReactSymbols.js | 3 - ...DOMServerPartialHydration-test.internal.js | 165 +- ...MServerSelectiveHydration-test.internal.js | 75 +- .../react-dom/src/client/ReactDOMComponent.js | 86 +- .../src/client/ReactDOMEventHandle.js | 7 +- .../src/client/ReactDOMHostConfig.js | 53 +- .../src/events/DOMPluginEventSystem.js | 12 +- .../DeprecatedDOMEventResponderSystem.js | 642 -------- .../react-dom/src/events/EventSystemFlags.js | 16 +- .../src/events/ReactDOMEventListener.js | 83 +- .../src/events/ReactDOMEventReplaying.js | 34 +- .../src/events/ReactDOMUpdateBatching.js | 38 +- ...edDOMEventResponderSystem-test.internal.js | 1002 ------------ .../src/events/checkPassiveEvents.js | 4 +- .../src/server/ReactPartialRenderer.js | 4 - .../src/server/ReactPartialRendererHooks.js | 9 - packages/react-dom/src/shared/DOMProperty.js | 8 +- .../shared/ReactControlledValuePropTypes.js | 8 +- .../react-dom/src/shared/ReactDOMTypes.js | 60 - packages/react-interactions/events/README.md | 105 -- .../react-interactions/events/context-menu.js | 10 - .../events/deprecated-focus.js | 10 - .../events/docs/ContextMenu.md | 54 - .../react-interactions/events/docs/Focus.md | 64 - .../events/docs/FocusWithin.md | 48 - .../react-interactions/events/docs/Hover.md | 75 - .../react-interactions/events/docs/Press.md | 121 -- packages/react-interactions/events/hover.js | 10 - packages/react-interactions/events/input.js | 10 - .../react-interactions/events/press-legacy.js | 10 - .../events/src/dom/ContextMenu.js | 126 -- .../events/src/dom/DeprecatedFocus.js | 718 --------- .../events/src/dom/Hover.js | 391 ----- .../events/src/dom/Input.js | 225 --- .../events/src/dom/PressLegacy.js | 924 ----------- .../react-interactions/events/src/dom/Tap.js | 734 --------- .../__tests__/ContextMenu-test.internal.js | 204 --- .../src/dom/__tests__/Focus-test.internal.js | 403 ----- .../__tests__/FocusWithin-test.internal.js | 574 ------- .../src/dom/__tests__/Hover-test.internal.js | 430 ------ .../src/dom/__tests__/Input-test.internal.js | 1001 ------------ .../MixedResponders-test-internal.js | 255 ---- .../__tests__/PressLegacy-test.internal.js | 1346 ----------------- .../src/dom/__tests__/Tap-test.internal.js | 1010 ------------- .../events/src/dom/shared/index.js | 87 -- packages/react-interactions/events/tap.js | 10 - .../src/ReactFabricHostConfig.js | 16 - .../src/ReactNativeHostConfig.js | 16 - .../ResponderEventPlugin-test.internal.js | 3 +- .../src/legacy-events/ReactGenericBatching.js | 2 +- .../src/createReactNoop.js | 8 - .../react-reconciler/src/ReactFiber.new.js | 2 - .../react-reconciler/src/ReactFiber.old.js | 2 - .../src/ReactFiberCommitWork.new.js | 27 - .../src/ReactFiberCommitWork.old.js | 27 - .../src/ReactFiberCompleteWork.new.js | 59 +- .../src/ReactFiberCompleteWork.old.js | 59 +- .../src/ReactFiberDeprecatedEvents.new.js | 234 --- .../src/ReactFiberDeprecatedEvents.old.js | 234 --- .../src/ReactFiberHooks.new.js | 67 - .../src/ReactFiberHooks.old.js | 67 - .../src/ReactFiberWorkLoop.new.js | 4 +- .../src/ReactFiberWorkLoop.old.js | 4 +- .../src/ReactInternalTypes.js | 11 - .../src/__tests__/ReactScope-test.internal.js | 1 - .../src/forks/ReactFiberHostConfig.custom.js | 4 - .../src/ReactTestHostConfig.js | 47 +- packages/react/index.classic.fb.js | 3 - packages/react/index.js | 2 - packages/react/index.modern.fb.js | 3 - packages/react/src/React.js | 5 - packages/react/src/ReactEventResponder.js | 46 - packages/react/src/ReactHooks.js | 20 - packages/shared/ReactFeatureFlags.js | 3 - packages/shared/ReactSymbols.js | 2 - packages/shared/ReactTypes.js | 31 - .../forks/ReactFeatureFlags.native-fb.js | 1 - .../forks/ReactFeatureFlags.native-oss.js | 1 - .../forks/ReactFeatureFlags.test-renderer.js | 1 - .../ReactFeatureFlags.test-renderer.native.js | 1 - .../ReactFeatureFlags.test-renderer.www.js | 1 - .../shared/forks/ReactFeatureFlags.testing.js | 1 - .../forks/ReactFeatureFlags.testing.www.js | 1 - .../shared/forks/ReactFeatureFlags.www.js | 2 - packages/shared/isValidElementType.js | 2 - scripts/error-codes/codes.json | 6 - scripts/jest/setupTests.www.js | 1 - scripts/rollup/build.js | 6 - scripts/rollup/bundles.js | 68 - 92 files changed, 125 insertions(+), 12325 deletions(-) delete mode 100644 packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js delete mode 100644 packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js delete mode 100644 packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js delete mode 100644 packages/react-interactions/events/README.md delete mode 100644 packages/react-interactions/events/context-menu.js delete mode 100644 packages/react-interactions/events/deprecated-focus.js delete mode 100644 packages/react-interactions/events/docs/ContextMenu.md delete mode 100644 packages/react-interactions/events/docs/Focus.md delete mode 100644 packages/react-interactions/events/docs/FocusWithin.md delete mode 100644 packages/react-interactions/events/docs/Hover.md delete mode 100644 packages/react-interactions/events/docs/Press.md delete mode 100644 packages/react-interactions/events/hover.js delete mode 100644 packages/react-interactions/events/input.js delete mode 100644 packages/react-interactions/events/press-legacy.js delete mode 100644 packages/react-interactions/events/src/dom/ContextMenu.js delete mode 100644 packages/react-interactions/events/src/dom/DeprecatedFocus.js delete mode 100644 packages/react-interactions/events/src/dom/Hover.js delete mode 100644 packages/react-interactions/events/src/dom/Input.js delete mode 100644 packages/react-interactions/events/src/dom/PressLegacy.js delete mode 100644 packages/react-interactions/events/src/dom/Tap.js delete mode 100644 packages/react-interactions/events/src/dom/__tests__/ContextMenu-test.internal.js delete mode 100644 packages/react-interactions/events/src/dom/__tests__/Focus-test.internal.js delete mode 100644 packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js delete mode 100644 packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js delete mode 100644 packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js delete mode 100644 packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js delete mode 100644 packages/react-interactions/events/src/dom/__tests__/PressLegacy-test.internal.js delete mode 100644 packages/react-interactions/events/src/dom/__tests__/Tap-test.internal.js delete mode 100644 packages/react-interactions/events/src/dom/shared/index.js delete mode 100644 packages/react-interactions/events/tap.js delete mode 100644 packages/react-reconciler/src/ReactFiberDeprecatedEvents.new.js delete mode 100644 packages/react-reconciler/src/ReactFiberDeprecatedEvents.old.js delete mode 100644 packages/react/src/ReactEventResponder.js diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js index 3362a3fd85..459f714b90 100644 --- a/packages/react-art/src/ReactARTHostConfig.js +++ b/packages/react-art/src/ReactARTHostConfig.js @@ -10,10 +10,6 @@ import Mode from 'art/modes/current'; import invariant from 'shared/invariant'; import {TYPES, EVENT_TYPES, childrenAsString} from './ReactARTInternals'; -import type { - ReactEventResponder, - ReactEventResponderInstance, -} from 'shared/ReactTypes'; const pooledTransform = new Transform(); @@ -429,22 +425,6 @@ export function clearContainer(container) { // TODO Implement this } -export function DEPRECATED_mountResponderInstance( - responder: ReactEventResponder, - responderInstance: ReactEventResponderInstance, - props: Object, - state: Object, - instance: Object, -) { - throw new Error('Not yet implemented.'); -} - -export function DEPRECATED_unmountResponderInstance( - responderInstance: ReactEventResponderInstance, -): void { - throw new Error('Not yet implemented.'); -} - export function getFundamentalComponentInstance(fundamentalInstance) { throw new Error('Not yet implemented.'); } diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 4dbaaccbf3..31e03102b4 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -13,8 +13,6 @@ import type { MutableSourceSubscribeFn, ReactContext, ReactProviderType, - ReactEventResponder, - ReactEventResponderListener, } from 'shared/ReactTypes'; import type { Fiber, @@ -260,22 +258,6 @@ function useMutableSource( return value; } -function useResponder( - responder: ReactEventResponder, - listenerProps: Object, -): ReactEventResponderListener { - // Don't put the actual event responder object in, just its displayName - const value = { - responder: responder.displayName || 'EventResponder', - props: listenerProps, - }; - hookLog.push({primitive: 'Responder', stackError: new Error(), value}); - return { - responder, - props: listenerProps, - }; -} - function useTransition( config: SuspenseConfig | null | void, ): [(() => void) => void, boolean] { @@ -335,7 +317,6 @@ const Dispatcher: DispatcherType = { useReducer, useRef, useState, - useResponder, useTransition, useMutableSource, useDeferredValue, diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js deleted file mode 100644 index d5874c5621..0000000000 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.internal.js +++ /dev/null @@ -1,46 +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 - * @jest-environment node - */ - -'use strict'; - -let React; -let ReactDebugTools; - -describe('ReactHooksInspection', () => { - beforeEach(() => { - jest.resetModules(); - const ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableDeprecatedFlareAPI = true; - React = require('react'); - ReactDebugTools = require('react-debug-tools'); - }); - - // @gate experimental - it('should inspect a simple useResponder hook', () => { - const TestResponder = React.DEPRECATED_createResponder('TestResponder', {}); - - function Foo(props) { - const listener = React.DEPRECATED_useResponder(TestResponder, { - preventDefault: false, - }); - return
Hello world
; - } - const tree = ReactDebugTools.inspectHooks(Foo, {}); - expect(tree).toEqual([ - { - isStateEditable: false, - id: 0, - name: 'Responder', - value: {props: {preventDefault: false}, responder: 'TestResponder'}, - subHooks: [], - }, - ]); - }); -}); diff --git a/packages/react-devtools-shared/src/backend/ReactSymbols.js b/packages/react-devtools-shared/src/backend/ReactSymbols.js index 677c0a9d66..70629f3bff 100644 --- a/packages/react-devtools-shared/src/backend/ReactSymbols.js +++ b/packages/react-devtools-shared/src/backend/ReactSymbols.js @@ -58,9 +58,6 @@ export const PROFILER_SYMBOL_STRING = 'Symbol(react.profiler)'; export const PROVIDER_NUMBER = 0xeacd; export const PROVIDER_SYMBOL_STRING = 'Symbol(react.provider)'; -export const RESPONDER_NUMBER = 0xead6; -export const RESPONDER_SYMBOL_STRING = 'Symbol(react.responder)'; - export const SCOPE_NUMBER = 0xead7; export const SCOPE_SYMBOL_STRING = 'Symbol(react.scope)'; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index e9831d1ae1..1d70475041 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -17,7 +17,6 @@ let ReactFeatureFlags; let Suspense; let SuspenseList; let act; -let useHover; function dispatchMouseEvent(to, from) { if (!to) { @@ -76,7 +75,7 @@ describe('ReactDOMServerPartialHydration', () => { ReactFeatureFlags = require('shared/ReactFeatureFlags'); ReactFeatureFlags.enableSuspenseCallback = true; - ReactFeatureFlags.enableDeprecatedFlareAPI = true; + ReactFeatureFlags.enableCreateEventHandleAPI = true; React = require('react'); ReactDOM = require('react-dom'); @@ -2202,8 +2201,10 @@ describe('ReactDOMServerPartialHydration', () => { }); // @gate experimental - it('does not invoke an event on a hydrated EventResponder until it commits', async () => { + it('does not invoke an event on a hydrated event handle until it commits', async () => { + const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; + let isServerRendering = true; let resolve; const promise = new Promise(resolvePromise => (resolve = resolvePromise)); @@ -2216,17 +2217,15 @@ describe('ReactDOMServerPartialHydration', () => { } const onEvent = jest.fn(); - const TestResponder = React.DEPRECATED_createResponder( - 'TestEventResponder', - { - targetEventTypes: ['click'], - onEvent, - }, - ); function Button() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - return Click me; + const ref = React.useRef(null); + if (!isServerRendering) { + React.useLayoutEffect(() => { + return setClick(ref.current, onEvent); + }); + } + return Click me; } function App() { @@ -2253,6 +2252,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; + isServerRendering = false; const root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); @@ -2364,23 +2364,25 @@ describe('ReactDOMServerPartialHydration', () => { }); // @gate experimental - it('invokes discrete events on nested suspense boundaries in a root (responder system)', async () => { + it('invokes discrete events on nested suspense boundaries in a root (createEventHandle)', async () => { let suspend = false; + let isServerRendering = true; let resolve; const promise = new Promise(resolvePromise => (resolve = resolvePromise)); const onEvent = jest.fn(); - const TestResponder = React.DEPRECATED_createResponder( - 'TestEventResponder', - { - targetEventTypes: ['click'], - onEvent, - }, - ); + const setClick = ReactDOM.unstable_createEventHandle('click'); function Button() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - return Click me; + const ref = React.useRef(null); + + if (!isServerRendering) { + React.useLayoutEffect(() => { + return setClick(ref.current, onEvent); + }); + } + + return Click me; } function Child() { @@ -2416,6 +2418,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; + isServerRendering = false; const root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); @@ -2704,124 +2707,6 @@ describe('ReactDOMServerPartialHydration', () => { document.body.removeChild(container); }); - // @gate experimental - it('blocks only on the last continuous event (Responder system)', async () => { - useHover = require('react-interactions/events/hover').useHover; - - let suspend1 = false; - let resolve1; - const promise1 = new Promise(resolvePromise => (resolve1 = resolvePromise)); - let suspend2 = false; - let resolve2; - const promise2 = new Promise(resolvePromise => (resolve2 = resolvePromise)); - - function First({text}) { - if (suspend1) { - throw promise1; - } else { - return 'Hello'; - } - } - - function Second({text}) { - if (suspend2) { - throw promise2; - } else { - return 'World'; - } - } - - const ops = []; - - function App() { - const listener1 = useHover({ - onHoverStart() { - ops.push('Hover Start First'); - }, - onHoverEnd() { - ops.push('Hover End First'); - }, - }); - const listener2 = useHover({ - onHoverStart() { - ops.push('Hover Start Second'); - }, - onHoverEnd() { - ops.push('Hover End Second'); - }, - }); - return ( -
- - - {/* We suspend after to test what happens when we eager - attach the listener. */} - - - - - - - -
- ); - } - - const finalHTML = ReactDOMServer.renderToString(); - const container = document.createElement('div'); - container.innerHTML = finalHTML; - - // We need this to be in the document since we'll dispatch events on it. - document.body.appendChild(container); - - const appDiv = container.getElementsByTagName('div')[0]; - const firstSpan = appDiv.getElementsByTagName('span')[0]; - const secondSpan = appDiv.getElementsByTagName('span')[1]; - expect(firstSpan.textContent).toBe(''); - expect(secondSpan.textContent).toBe('World'); - - // On the client we don't have all data yet but we want to start - // hydrating anyway. - suspend1 = true; - suspend2 = true; - const root = ReactDOM.createRoot(container, {hydrate: true}); - root.render(); - - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - dispatchMouseEvent(appDiv, null); - dispatchMouseEvent(firstSpan, appDiv); - dispatchMouseEvent(secondSpan, firstSpan); - - // Neither target is yet hydrated. - expect(ops).toEqual([]); - - // Resolving the second promise so that rendering can complete. - suspend2 = false; - resolve2(); - await promise2; - - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - // We've unblocked the current hover target so we should be - // able to replay it now. - expect(ops).toEqual(['Hover Start Second']); - - // Resolving the first promise has no effect now. - suspend1 = false; - resolve1(); - await promise1; - - Scheduler.unstable_flushAll(); - jest.runAllTimers(); - - expect(ops).toEqual(['Hover Start Second']); - - document.body.removeChild(container); - }); - // @gate experimental it('finishes normal pri work before continuing to hydrate a retry', async () => { let suspend = false; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index 7cd66a0bb0..3a948a0651 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -96,8 +96,7 @@ describe('ReactDOMServerSelectiveHydration', () => { jest.resetModuleRegistry(); const ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableDeprecatedFlareAPI = true; - + ReactFeatureFlags.enableCreateEventHandleAPI = true; React = require('react'); ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); @@ -348,18 +347,22 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate experimental - it('hydrates the target boundary synchronously during a click (flare)', async () => { - const usePress = require('react-interactions/events/press-legacy').usePress; + it('hydrates the target boundary synchronously during a click (createEventHandle)', async () => { + const setClick = ReactDOM.unstable_createEventHandle('click'); + let isServerRendering = true; function Child({text}) { + const ref = React.useRef(null); Scheduler.unstable_yieldValue(text); - const listener = usePress({ - onPress() { - Scheduler.unstable_yieldValue('Clicked ' + text); - }, - }); + if (!isServerRendering) { + React.useLayoutEffect(() => { + return setClick(ref.current, () => { + Scheduler.unstable_yieldValue('Clicked ' + text); + }); + }); + } - return {text}; + return {text}; } function App() { @@ -386,7 +389,10 @@ describe('ReactDOMServerSelectiveHydration', () => { container.innerHTML = finalHTML; + isServerRendering = false; + const root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(); // Nothing has been hydrated so far. @@ -398,11 +404,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // This should synchronously hydrate the root App and the second suspense // boundary. - const preventDefault = jest.fn(); - target.virtualclick({preventDefault}); - - // The event should have been canceled because we called preventDefault. - expect(preventDefault).toHaveBeenCalled(); + target.virtualclick(); // We rendered App, B and then invoked the event without rendering A. expect(Scheduler).toHaveYielded(['App', 'B', 'Clicked B']); @@ -414,24 +416,29 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate experimental - it('hydrates at higher pri if sync did not work first time (flare)', async () => { - const usePress = require('react-interactions/events/press-legacy').usePress; + it('hydrates at higher pri if sync did not work first time (createEventHandle)', async () => { let suspend = false; + let isServerRendering = true; let resolve; const promise = new Promise(resolvePromise => (resolve = resolvePromise)); + const setClick = ReactDOM.unstable_createEventHandle('click'); function Child({text}) { + const ref = React.useRef(null); if ((text === 'A' || text === 'D') && suspend) { throw promise; } Scheduler.unstable_yieldValue(text); - const listener = usePress({ - onPress() { - Scheduler.unstable_yieldValue('Clicked ' + text); - }, - }); - return {text}; + if (!isServerRendering) { + React.useLayoutEffect(() => { + return setClick(ref.current, () => { + Scheduler.unstable_yieldValue('Clicked ' + text); + }); + }); + } + + return {text}; } function App() { @@ -467,6 +474,7 @@ describe('ReactDOMServerSelectiveHydration', () => { const spanD = container.getElementsByTagName('span')[3]; suspend = true; + isServerRendering = false; // A and D will be suspended. We'll click on D which should take // priority, after we unsuspend. @@ -496,24 +504,28 @@ describe('ReactDOMServerSelectiveHydration', () => { }); // @gate experimental - it('hydrates at higher pri for secondary discrete events (flare)', async () => { - const usePress = require('react-interactions/events/press-legacy').usePress; + it('hydrates at higher pri for secondary discrete events (createEventHandle)', async () => { + const setClick = ReactDOM.unstable_createEventHandle('click'); let suspend = false; + let isServerRendering = true; let resolve; const promise = new Promise(resolvePromise => (resolve = resolvePromise)); function Child({text}) { + const ref = React.useRef(null); if ((text === 'A' || text === 'D') && suspend) { throw promise; } Scheduler.unstable_yieldValue(text); - const listener = usePress({ - onPress() { - Scheduler.unstable_yieldValue('Clicked ' + text); - }, - }); - return {text}; + if (!isServerRendering) { + React.useLayoutEffect(() => { + return setClick(ref.current, () => { + Scheduler.unstable_yieldValue('Clicked ' + text); + }); + }); + } + return {text}; } function App() { @@ -551,6 +563,7 @@ describe('ReactDOMServerSelectiveHydration', () => { const spanD = container.getElementsByTagName('span')[3]; suspend = true; + isServerRendering = false; // A and D will be suspended. We'll click on D which should take // priority, after we unsuspend. diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index d7c0d94afb..b82a41eb0f 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -7,8 +7,6 @@ * @flow */ -import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree'; - import { registrationNameDependencies, possibleRegistrationNames, @@ -16,11 +14,6 @@ import { import {canUseDOM} from 'shared/ExecutionEnvironment'; import invariant from 'shared/invariant'; -import { - setListenToResponderEventTypes, - addResponderEventSystemEvent, - removeTrappedEventListener, -} from '../events/DeprecatedDOMEventResponderSystem'; import { getValueForAttribute, @@ -81,16 +74,12 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; -import { - enableDeprecatedFlareAPI, - enableTrustedTypesIntegration, -} from 'shared/ReactFeatureFlags'; +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; import { listenToReactEvent, mediaEventTypes, listenToNonDelegatedEvent, } from '../events/DOMPluginEventSystem'; -import {getEventListenerMap} from './ReactDOMComponentTree'; let didWarnInvalidHydration = false; let didWarnScriptTags = false; @@ -102,7 +91,6 @@ const AUTOFOCUS = 'autoFocus'; const CHILDREN = 'children'; const STYLE = 'style'; const HTML = '__html'; -const DEPRECATED_flareListeners = 'DEPRECATED_flareListeners'; const {html: HTML_NAMESPACE} = Namespaces; @@ -358,7 +346,6 @@ function setInitialDOMProperties( setTextContent(domElement, '' + nextProp); } } else if ( - (enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -728,7 +715,6 @@ export function diffProperties( } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) { // Noop. This is handled by the clear text mechanism. } else if ( - (enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -817,7 +803,6 @@ export function diffProperties( (updatePayload = updatePayload || []).push(propKey, '' + nextProp); } } else if ( - (enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { @@ -1083,7 +1068,6 @@ export function diffHydratedProperties( if (suppressHydrationWarning) { // Don't bother comparing. We're ignoring all these warnings. } else if ( - (enableDeprecatedFlareAPI && propKey === DEPRECATED_flareListeners) || propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING || // Controlled attributes are not validated @@ -1317,71 +1301,3 @@ export function restoreControlledState( return; } } - -function endsWith(subject: string, search: string): boolean { - const length = subject.length; - return subject.substring(length - search.length, length) === search; -} - -export function listenToEventResponderEventTypes( - eventTypes: Array, - document: Document, -): void { - if (enableDeprecatedFlareAPI) { - // Get the listening Map for this element. We use this to track - // what events we're listening to. - const listenerMap = getEventListenerMap(document); - - // Go through each target event type of the event responder - for (let i = 0, length = eventTypes.length; i < length; ++i) { - const eventType = eventTypes[i]; - const isPassive = !endsWith(eventType, '_active'); - const eventKey = isPassive ? eventType + '_passive' : eventType; - const targetEventType = isPassive - ? eventType - : eventType.substring(0, eventType.length - 7); - if (!listenerMap.has(eventKey)) { - if (isPassive) { - const activeKey = targetEventType + '_active'; - // If we have an active event listener, do not register - // a passive event listener. We use the same active event - // listener. - if (listenerMap.has(activeKey)) { - continue; - } - } else { - // If we have a passive event listener, remove the - // existing passive event listener before we add the - // active event listener. - const passiveKey = targetEventType + '_passive'; - const passiveItem = ((listenerMap.get( - passiveKey, - ): any): ElementListenerMapEntry | void); - if (passiveItem !== undefined) { - removeTrappedEventListener( - document, - (targetEventType: any), - true, - passiveItem.listener, - ); - listenerMap.delete(passiveKey); - } - } - const eventListener = addResponderEventSystemEvent( - document, - targetEventType, - isPassive, - ); - listenerMap.set(eventKey, { - passive: isPassive, - listener: eventListener, - }); - } - } - } -} - -// We can remove this once the event API is stable and out of a flag -if (enableDeprecatedFlareAPI) { - setListenToResponderEventTypes(listenToEventResponderEventTypes); -} diff --git a/packages/react-dom/src/client/ReactDOMEventHandle.js b/packages/react-dom/src/client/ReactDOMEventHandle.js index 4c178fb701..19e69126ae 100644 --- a/packages/react-dom/src/client/ReactDOMEventHandle.js +++ b/packages/react-dom/src/client/ReactDOMEventHandle.js @@ -28,10 +28,7 @@ import { } from '../events/DOMPluginEventSystem'; import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags'; -import { - PLUGIN_EVENT_SYSTEM, - IS_EVENT_HANDLE_NON_MANAGED_NODE, -} from '../events/EventSystemFlags'; +import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../events/EventSystemFlags'; import { enableScopeAPI, @@ -164,7 +161,7 @@ function registerReactDOMEvent( null, isPassiveListener, listenerPriority, - PLUGIN_EVENT_SYSTEM | IS_EVENT_HANDLE_NON_MANAGED_NODE, + IS_EVENT_HANDLE_NON_MANAGED_NODE, ); } else { invariant( diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 24e65753ac..22d38dea04 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -16,11 +16,7 @@ import type { } from 'react-reconciler/src/ReactTestSelectors'; import type {RootType} from './ReactDOMRoot'; import type {ReactScopeInstance} from 'shared/ReactTypes'; -import type { - ReactDOMEventResponder, - ReactDOMEventResponderInstance, - ReactDOMFundamentalComponentInstance, -} from '../shared/ReactDOMTypes'; +import type {ReactDOMFundamentalComponentInstance} from '../shared/ReactDOMTypes'; import { precacheFiberNode, @@ -45,7 +41,6 @@ import { warnForDeletedHydratableText, warnForInsertedHydratedElement, warnForInsertedHydratedText, - listenToEventResponderEventTypes, } from './ReactDOMComponent'; import {getSelectionInformation, restoreSelection} from './ReactInputSelection'; import setTextContent from './setTextContent'; @@ -65,15 +60,10 @@ import { import dangerousStyleValue from '../shared/dangerousStyleValue'; import {REACT_OPAQUE_ID_TYPE} from 'shared/ReactSymbols'; -import { - mountEventResponder, - unmountEventResponder, -} from '../events/DeprecatedDOMEventResponderSystem'; import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying'; import { enableSuspenseServerRenderer, - enableDeprecatedFlareAPI, enableFundamentalAPI, enableCreateEventHandleAPI, enableScopeAPI, @@ -232,7 +222,7 @@ export function prepareForCommit(containerInfo: Container): Object | null { eventsEnabled = ReactBrowserEventEmitterIsEnabled(); selectionInformation = getSelectionInformation(); let activeInstance = null; - if (enableDeprecatedFlareAPI || enableCreateEventHandleAPI) { + if (enableCreateEventHandleAPI) { const focusedElem = selectionInformation.focusedElem; if (focusedElem !== null) { activeInstance = getClosestInstanceFromNode(focusedElem); @@ -243,7 +233,7 @@ export function prepareForCommit(containerInfo: Container): Object | null { } export function beforeActiveInstanceBlur(): void { - if (enableDeprecatedFlareAPI || enableCreateEventHandleAPI) { + if (enableCreateEventHandleAPI) { ReactBrowserEventEmitterSetEnabled(true); dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem); ReactBrowserEventEmitterSetEnabled(false); @@ -251,7 +241,7 @@ export function beforeActiveInstanceBlur(): void { } export function afterActiveInstanceBlur(): void { - if (enableDeprecatedFlareAPI || enableCreateEventHandleAPI) { + if (enableCreateEventHandleAPI) { ReactBrowserEventEmitterSetEnabled(true); dispatchAfterDetachedBlur((selectionInformation: any).focusedElem); ReactBrowserEventEmitterSetEnabled(false); @@ -510,7 +500,7 @@ function createEvent(type: DOMEventName, bubbles: boolean): Event { } function dispatchBeforeDetachedBlur(target: HTMLElement): void { - if (enableDeprecatedFlareAPI || enableCreateEventHandleAPI) { + if (enableCreateEventHandleAPI) { const event = createEvent('beforeblur', true); // Dispatch "beforeblur" directly on the target, // so it gets picked up by the event system and @@ -520,7 +510,7 @@ function dispatchBeforeDetachedBlur(target: HTMLElement): void { } function dispatchAfterDetachedBlur(target: HTMLElement): void { - if (enableDeprecatedFlareAPI || enableCreateEventHandleAPI) { + if (enableCreateEventHandleAPI) { const event = createEvent('afterblur', false); // So we know what was detached, make the relatedTarget the // detached target on the "afterblur" event. @@ -975,37 +965,6 @@ export function didNotFindHydratableSuspenseInstance( } } -export function DEPRECATED_mountResponderInstance( - responder: ReactDOMEventResponder, - responderInstance: ReactDOMEventResponderInstance, - responderProps: Object, - responderState: Object, - instance: Instance, -): ReactDOMEventResponderInstance { - // Listen to events - const doc = instance.ownerDocument; - const {targetEventTypes} = ((responder: any): ReactDOMEventResponder); - if (targetEventTypes !== null) { - listenToEventResponderEventTypes(targetEventTypes, doc); - } - mountEventResponder( - responder, - responderInstance, - responderProps, - responderState, - ); - return responderInstance; -} - -export function DEPRECATED_unmountResponderInstance( - responderInstance: ReactDOMEventResponderInstance, -): void { - if (enableDeprecatedFlareAPI) { - // TODO stop listening to targetEventTypes - unmountEventResponder(responderInstance); - } -} - export function getFundamentalComponentInstance( fundamentalInstance: ReactDOMFundamentalComponentInstance, ): Instance { diff --git a/packages/react-dom/src/events/DOMPluginEventSystem.js b/packages/react-dom/src/events/DOMPluginEventSystem.js index 7eb027747a..1b23cde2f3 100644 --- a/packages/react-dom/src/events/DOMPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMPluginEventSystem.js @@ -22,7 +22,6 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import {registrationNameDependencies} from './EventRegistry'; import { - PLUGIN_EVENT_SYSTEM, IS_CAPTURE_PHASE, IS_EVENT_HANDLE_NON_MANAGED_NODE, IS_NON_DELEGATED, @@ -64,7 +63,6 @@ import { addEventBubbleListenerWithPassiveFlag, addEventCaptureListenerWithPassiveFlag, } from './EventListener'; -import {removeTrappedEventListener} from './DeprecatedDOMEventResponderSystem'; import {topLevelEventsToReactNames} from './DOMEventProperties'; import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin'; import * as ChangeEventPlugin from './plugins/ChangeEventPlugin'; @@ -318,7 +316,7 @@ export function listenToNonDelegatedEvent( const listener = addTrappedEventListener( targetElement, domEventName, - PLUGIN_EVENT_SYSTEM | IS_NON_DELEGATED, + IS_NON_DELEGATED, isCapturePhaseListener, ); listenerMap.set(listenerMapKey, {passive: false, listener}); @@ -332,7 +330,7 @@ export function listenToNativeEvent( targetElement: Element | null, isPassiveListener?: boolean, listenerPriority?: EventPriority, - eventSystemFlags?: EventSystemFlags = PLUGIN_EVENT_SYSTEM, + eventSystemFlags?: EventSystemFlags = 0, ): void { let target = rootContainerElement; // selectionchange needs to be attached to the document @@ -381,11 +379,11 @@ export function listenToNativeEvent( // If we should upgrade, then we need to remove the existing trapped // event listener for the target container. if (shouldUpgrade) { - removeTrappedEventListener( + removeEventListener( target, domEventName, - isCapturePhaseListener, ((listenerEntry: any): ElementListenerMapEntry).listener, + isCapturePhaseListener, ); } if (isCapturePhaseListener) { @@ -549,7 +547,7 @@ function deferClickToDocumentForLegacyFBSupport( addTrappedEventListener( targetContainer, domEventName, - PLUGIN_EVENT_SYSTEM | IS_LEGACY_FB_SUPPORT_MODE, + IS_LEGACY_FB_SUPPORT_MODE, false, isDeferredListenerForLegacyFBSupport, ); diff --git a/packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js b/packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js deleted file mode 100644 index 73d3ea7331..0000000000 --- a/packages/react-dom/src/events/DeprecatedDOMEventResponderSystem.js +++ /dev/null @@ -1,642 +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 EventSystemFlags, - IS_PASSIVE, - PASSIVE_NOT_SUPPORTED, - RESPONDER_EVENT_SYSTEM, -} from './EventSystemFlags'; -import type {AnyNativeEvent} from '../events/PluginModuleType'; -import { - HostComponent, - ScopeComponent, - HostPortal, -} from 'react-reconciler/src/ReactWorkTags'; -import type {EventPriority} from 'shared/ReactTypes'; -import type { - ReactDOMEventResponder, - ReactDOMEventResponderInstance, - ReactDOMResponderContext, - ReactDOMResponderEvent, -} from '../shared/ReactDOMTypes'; -import type {DOMEventName} from '../events/DOMEventNames'; -import { - batchedEventUpdates, - discreteUpdates, - flushDiscreteUpdatesIfNeeded, - executeUserEventHandler, -} from './ReactDOMUpdateBatching'; -import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; -import invariant from 'shared/invariant'; - -import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; -import {enqueueStateRestore} from './ReactDOMControlledComponent'; -import {createEventListenerWrapper} from './ReactDOMEventListener'; -import {passiveBrowserEventsSupported} from './checkPassiveEvents'; -import { - addEventCaptureListener, - addEventCaptureListenerWithPassiveFlag, - removeEventListener, -} from './EventListener'; - -import { - ContinuousEvent, - UserBlockingEvent, - DiscreteEvent, -} from 'shared/ReactTypes'; - -// Intentionally not named imports because Rollup would use dynamic dispatch for -// CommonJS interop named imports. -import * as Scheduler from 'scheduler'; - -import { - InputContinuousLanePriority, - getCurrentUpdateLanePriority, - setCurrentUpdateLanePriority, -} from 'react-reconciler/src/ReactFiberLane'; - -const { - unstable_UserBlockingPriority: UserBlockingPriority, - unstable_runWithPriority: runWithPriority, -} = Scheduler; - -export let listenToResponderEventTypesImpl; - -export function setListenToResponderEventTypes( - _listenToResponderEventTypesImpl: Function, -) { - listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl; -} - -const rootEventTypesToEventResponderInstances: Map< - DOMEventName | string, - Set, -> = new Map(); - -type PropagationBehavior = 0 | 1; - -const DoNotPropagateToNextResponder = 0; -const PropagateToNextResponder = 1; - -let currentTimeStamp = 0; -let currentInstance: null | ReactDOMEventResponderInstance = null; -let currentDocument: null | Document = null; -let currentPropagationBehavior: PropagationBehavior = DoNotPropagateToNextResponder; - -const eventResponderContext: ReactDOMResponderContext = { - dispatchEvent( - eventValue: any, - eventListener: any => void, - eventPriority: EventPriority, - ): void { - validateResponderContext(); - validateEventValue(eventValue); - switch (eventPriority) { - case DiscreteEvent: { - flushDiscreteUpdatesIfNeeded(currentTimeStamp); - discreteUpdates(() => - executeUserEventHandler(eventListener, eventValue), - ); - break; - } - case UserBlockingEvent: { - const previousPriority = getCurrentUpdateLanePriority(); - try { - setCurrentUpdateLanePriority(InputContinuousLanePriority); - runWithPriority(UserBlockingPriority, () => - executeUserEventHandler(eventListener, eventValue), - ); - } finally { - setCurrentUpdateLanePriority(previousPriority); - } - break; - } - case ContinuousEvent: { - executeUserEventHandler(eventListener, eventValue); - break; - } - } - }, - isTargetWithinResponder(target: null | Element | Document): boolean { - validateResponderContext(); - if (target != null) { - let fiber = getClosestInstanceFromNode(target); - const responderFiber = ((currentInstance: any): ReactDOMEventResponderInstance) - .fiber; - - while (fiber !== null) { - if (fiber === responderFiber || fiber.alternate === responderFiber) { - return true; - } - fiber = fiber.return; - } - } - return false; - }, - isTargetWithinResponderScope(target: null | Element | Document): boolean { - validateResponderContext(); - const componentInstance = ((currentInstance: any): ReactDOMEventResponderInstance); - const responder = componentInstance.responder; - - if (target != null) { - let fiber = getClosestInstanceFromNode(target); - const responderFiber = ((currentInstance: any): ReactDOMEventResponderInstance) - .fiber; - - while (fiber !== null) { - if (fiber === responderFiber || fiber.alternate === responderFiber) { - return true; - } - if (doesFiberHaveResponder(fiber, responder)) { - return false; - } - fiber = fiber.return; - } - } - return false; - }, - isTargetWithinNode( - childTarget: Element | Document, - parentTarget: Element | Document, - ): boolean { - validateResponderContext(); - const childFiber = getClosestInstanceFromNode(childTarget); - const parentFiber = getClosestInstanceFromNode(parentTarget); - - if (childFiber != null && parentFiber != null) { - const parentAlternateFiber = parentFiber.alternate; - let node = childFiber; - while (node !== null) { - if (node === parentFiber || node === parentAlternateFiber) { - return true; - } - node = node.return; - } - return false; - } - // Fallback to DOM APIs - return parentTarget.contains(childTarget); - }, - addRootEventTypes(rootEventTypes: Array): void { - validateResponderContext(); - listenToResponderEventTypesImpl(rootEventTypes, currentDocument); - for (let i = 0; i < rootEventTypes.length; i++) { - const rootEventType = rootEventTypes[i]; - const eventResponderInstance = ((currentInstance: any): ReactDOMEventResponderInstance); - DEPRECATED_registerRootEventType(rootEventType, eventResponderInstance); - } - }, - removeRootEventTypes(rootEventTypes: Array): void { - validateResponderContext(); - for (let i = 0; i < rootEventTypes.length; i++) { - const rootEventType = rootEventTypes[i]; - const rootEventResponders = rootEventTypesToEventResponderInstances.get( - rootEventType, - ); - const rootEventTypesSet = ((currentInstance: any): ReactDOMEventResponderInstance) - .rootEventTypes; - if (rootEventTypesSet !== null) { - rootEventTypesSet.delete(rootEventType); - } - if (rootEventResponders !== undefined) { - rootEventResponders.delete( - ((currentInstance: any): ReactDOMEventResponderInstance), - ); - } - } - }, - getActiveDocument, - objectAssign: Object.assign, - getTimeStamp(): number { - validateResponderContext(); - return currentTimeStamp; - }, - isTargetWithinHostComponent( - target: Element | Document, - elementType: string, - ): boolean { - validateResponderContext(); - let fiber = getClosestInstanceFromNode(target); - - while (fiber !== null) { - if (fiber.tag === HostComponent && fiber.type === elementType) { - return true; - } - fiber = fiber.return; - } - return false; - }, - continuePropagation() { - currentPropagationBehavior = PropagateToNextResponder; - }, - enqueueStateRestore, - getResponderNode(): Element | null { - validateResponderContext(); - const responderFiber = ((currentInstance: any): ReactDOMEventResponderInstance) - .fiber; - if (responderFiber.tag === ScopeComponent) { - return null; - } - return responderFiber.stateNode; - }, -}; - -function validateEventValue(eventValue: any): void { - if (typeof eventValue === 'object' && eventValue !== null) { - const {target, type, timeStamp} = eventValue; - - if (target == null || type == null || timeStamp == null) { - throw new Error( - 'context.dispatchEvent: "target", "timeStamp", and "type" fields on event object are required.', - ); - } - const showWarning = name => { - if (__DEV__) { - console.error( - '%s is not available on event objects created from event responder modules (React Flare). ' + - 'Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.%s }`', - name, - name, - ); - } - }; - eventValue.isDefaultPrevented = () => { - if (__DEV__) { - showWarning('isDefaultPrevented()'); - } - }; - eventValue.isPropagationStopped = () => { - if (__DEV__) { - showWarning('isPropagationStopped()'); - } - }; - // $FlowFixMe: we don't need value, Flow thinks we do - Object.defineProperty(eventValue, 'nativeEvent', { - get() { - if (__DEV__) { - showWarning('nativeEvent'); - } - }, - }); - } -} - -function doesFiberHaveResponder( - fiber: Fiber, - responder: ReactDOMEventResponder, -): boolean { - const tag = fiber.tag; - if (tag === HostComponent || tag === ScopeComponent) { - const dependencies = fiber.dependencies; - if (dependencies !== null) { - const respondersMap = dependencies.responders; - if (respondersMap !== null && respondersMap.has(responder)) { - return true; - } - } - } - return false; -} - -function getActiveDocument(): Document { - return ((currentDocument: any): Document); -} - -function createDOMResponderEvent( - domEventName: string, - nativeEvent: AnyNativeEvent, - nativeEventTarget: Element | Document, - passive: boolean, -): ReactDOMResponderEvent { - const {buttons, pointerType} = (nativeEvent: any); - let eventPointerType = ''; - - if (pointerType !== undefined) { - eventPointerType = pointerType; - } else if (nativeEvent.key !== undefined) { - eventPointerType = 'keyboard'; - } else if (buttons !== undefined) { - eventPointerType = 'mouse'; - } else if ((nativeEvent: any).changedTouches !== undefined) { - eventPointerType = 'touch'; - } - - return { - nativeEvent: nativeEvent, - passive, - pointerType: eventPointerType, - target: nativeEventTarget, - type: domEventName, - }; -} - -function responderEventTypesContainType( - eventTypes: Array, - type: string, - isPassive: boolean, -): boolean { - for (let i = 0, len = eventTypes.length; i < len; i++) { - const eventType = eventTypes[i]; - if (eventType === type || (!isPassive && eventType === type + '_active')) { - return true; - } - } - return false; -} - -function validateResponderTargetEventTypes( - eventType: string, - responder: ReactDOMEventResponder, - isPassive: boolean, -): boolean { - const {targetEventTypes} = responder; - // Validate the target event type exists on the responder - if (targetEventTypes !== null) { - return responderEventTypesContainType( - targetEventTypes, - eventType, - isPassive, - ); - } - return false; -} - -function traverseAndHandleEventResponderInstances( - domEventName: string, - targetFiber: null | Fiber, - nativeEvent: AnyNativeEvent, - nativeEventTarget: Document | Element, - eventSystemFlags: EventSystemFlags, -): void { - const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0; - const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0; - const isPassive = isPassiveEvent || !isPassiveSupported; - - // Trigger event responders in this order: - // - Bubble target responder phase - // - Root responder phase - - const visitedResponders = new Set(); - const responderEvent = createDOMResponderEvent( - domEventName, - nativeEvent, - nativeEventTarget, - isPassiveEvent, - ); - let node = targetFiber; - let insidePortal = false; - while (node !== null) { - const tag = node.tag; - const dependencies = node.dependencies; - if (tag === HostPortal) { - insidePortal = true; - } else if ( - (tag === HostComponent || tag === ScopeComponent) && - dependencies !== null - ) { - const respondersMap = dependencies.responders; - if (respondersMap !== null) { - const responderInstances = Array.from(respondersMap.values()); - for (let i = 0, length = responderInstances.length; i < length; i++) { - const responderInstance = responderInstances[i]; - const {props, responder, state} = responderInstance; - if ( - !visitedResponders.has(responder) && - validateResponderTargetEventTypes( - domEventName, - responder, - isPassive, - ) && - (!insidePortal || responder.targetPortalPropagation) - ) { - visitedResponders.add(responder); - const onEvent = responder.onEvent; - if (onEvent !== null) { - currentInstance = responderInstance; - onEvent(responderEvent, eventResponderContext, props, state); - if (currentPropagationBehavior === PropagateToNextResponder) { - visitedResponders.delete(responder); - currentPropagationBehavior = DoNotPropagateToNextResponder; - } - } - } - } - } - } - node = node.return; - } - // Root phase - const passive = rootEventTypesToEventResponderInstances.get(domEventName); - const rootEventResponderInstances = []; - if (passive !== undefined) { - rootEventResponderInstances.push(...Array.from(passive)); - } - if (!isPassive) { - const active = rootEventTypesToEventResponderInstances.get( - domEventName + '_active', - ); - if (active !== undefined) { - rootEventResponderInstances.push(...Array.from(active)); - } - } - if (rootEventResponderInstances.length > 0) { - const responderInstances = Array.from(rootEventResponderInstances); - - for (let i = 0; i < responderInstances.length; i++) { - const responderInstance = responderInstances[i]; - const {props, responder, state} = responderInstance; - const onRootEvent = responder.onRootEvent; - if (onRootEvent !== null) { - currentInstance = responderInstance; - onRootEvent(responderEvent, eventResponderContext, props, state); - } - } - } -} - -export function mountEventResponder( - responder: ReactDOMEventResponder, - responderInstance: ReactDOMEventResponderInstance, - props: Object, - state: Object, -) { - const onMount = responder.onMount; - if (onMount !== null) { - const previousInstance = currentInstance; - currentInstance = responderInstance; - try { - onMount(eventResponderContext, props, state); - } finally { - currentInstance = previousInstance; - } - } -} - -export function unmountEventResponder( - responderInstance: ReactDOMEventResponderInstance, -): void { - const responder = ((responderInstance.responder: any): ReactDOMEventResponder); - const onUnmount = responder.onUnmount; - if (onUnmount !== null) { - const {props, state} = responderInstance; - const previousInstance = currentInstance; - currentInstance = responderInstance; - try { - onUnmount(eventResponderContext, props, state); - } finally { - currentInstance = previousInstance; - } - } - const rootEventTypesSet = responderInstance.rootEventTypes; - if (rootEventTypesSet !== null) { - const rootEventTypes = Array.from(rootEventTypesSet); - - for (let i = 0; i < rootEventTypes.length; i++) { - const topLevelEventType = rootEventTypes[i]; - const rootEventResponderInstances = rootEventTypesToEventResponderInstances.get( - topLevelEventType, - ); - if (rootEventResponderInstances !== undefined) { - rootEventResponderInstances.delete(responderInstance); - } - } - } -} - -function validateResponderContext(): void { - invariant( - currentInstance !== null, - 'An event responder context was used outside of an event cycle.', - ); -} - -export function DEPRECATED_dispatchEventForResponderEventSystem( - domEventName: string, - targetFiber: null | Fiber, - nativeEvent: AnyNativeEvent, - nativeEventTarget: Document | Element, - eventSystemFlags: EventSystemFlags, -): void { - if (enableDeprecatedFlareAPI) { - const previousInstance = currentInstance; - const previousTimeStamp = currentTimeStamp; - const previousDocument = currentDocument; - const previousPropagationBehavior = currentPropagationBehavior; - currentPropagationBehavior = DoNotPropagateToNextResponder; - // nodeType 9 is DOCUMENT_NODE - currentDocument = - (nativeEventTarget: any).nodeType === 9 - ? ((nativeEventTarget: any): Document) - : (nativeEventTarget: any).ownerDocument; - // We might want to control timeStamp another way here - currentTimeStamp = (nativeEvent: any).timeStamp; - try { - batchedEventUpdates(() => { - traverseAndHandleEventResponderInstances( - domEventName, - targetFiber, - nativeEvent, - nativeEventTarget, - eventSystemFlags, - ); - }); - } finally { - currentInstance = previousInstance; - currentTimeStamp = previousTimeStamp; - currentDocument = previousDocument; - currentPropagationBehavior = previousPropagationBehavior; - } - } -} - -export function addRootEventTypesForResponderInstance( - responderInstance: ReactDOMEventResponderInstance, - rootEventTypes: Array, -): void { - for (let i = 0; i < rootEventTypes.length; i++) { - const rootEventType = rootEventTypes[i]; - DEPRECATED_registerRootEventType(rootEventType, responderInstance); - } -} - -function DEPRECATED_registerRootEventType( - rootEventType: string, - eventResponderInstance: ReactDOMEventResponderInstance, -): void { - let rootEventResponderInstances = rootEventTypesToEventResponderInstances.get( - rootEventType, - ); - if (rootEventResponderInstances === undefined) { - rootEventResponderInstances = new Set(); - rootEventTypesToEventResponderInstances.set( - rootEventType, - rootEventResponderInstances, - ); - } - let rootEventTypesSet = eventResponderInstance.rootEventTypes; - if (rootEventTypesSet === null) { - rootEventTypesSet = eventResponderInstance.rootEventTypes = new Set(); - } - invariant( - !rootEventTypesSet.has(rootEventType), - 'addRootEventTypes() found a duplicate root event ' + - 'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' + - 'array or because of a previous addRootEventTypes() using this root event type.', - rootEventType, - ); - rootEventTypesSet.add(rootEventType); - rootEventResponderInstances.add(eventResponderInstance); -} - -export function addResponderEventSystemEvent( - document: Document, - domEventName: string, - passive: boolean, -): any => void { - let eventFlags = RESPONDER_EVENT_SYSTEM; - - // If passive option is not supported, then the event will be - // active and not passive, but we flag it as using not being - // supported too. This way the responder event plugins know, - // and can provide polyfills if needed. - if (passive) { - if (passiveBrowserEventsSupported) { - eventFlags |= IS_PASSIVE; - } else { - eventFlags |= PASSIVE_NOT_SUPPORTED; - passive = false; - } - } - // Check if interactive and wrap in discreteUpdates - const listener = createEventListenerWrapper( - document, - ((domEventName: any): DOMEventName), - eventFlags, - ); - if (passiveBrowserEventsSupported) { - return addEventCaptureListenerWithPassiveFlag( - document, - domEventName, - listener, - passive, - ); - } else { - return addEventCaptureListener(document, domEventName, listener); - } -} - -export function removeTrappedEventListener( - targetContainer: EventTarget, - domEventName: DOMEventName, - capture: boolean, - listener: any => void, -): void { - removeEventListener(targetContainer, domEventName, listener, capture); -} diff --git a/packages/react-dom/src/events/EventSystemFlags.js b/packages/react-dom/src/events/EventSystemFlags.js index a6b34ab3b3..150a73d35d 100644 --- a/packages/react-dom/src/events/EventSystemFlags.js +++ b/packages/react-dom/src/events/EventSystemFlags.js @@ -9,16 +9,12 @@ export type EventSystemFlags = number; -export const PLUGIN_EVENT_SYSTEM = 1; -export const RESPONDER_EVENT_SYSTEM = 1 << 1; -export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1 << 2; -export const IS_NON_DELEGATED = 1 << 3; -export const IS_CAPTURE_PHASE = 1 << 4; -export const IS_PASSIVE = 1 << 5; -export const IS_REPLAYED = 1 << 6; -export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 7; -// This is used by React Flare -export const PASSIVE_NOT_SUPPORTED = 1 << 8; +export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1; +export const IS_NON_DELEGATED = 1 << 1; +export const IS_CAPTURE_PHASE = 1 << 2; +export const IS_PASSIVE = 1 << 3; +export const IS_REPLAYED = 1 << 4; +export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 5; export const SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE = IS_LEGACY_FB_SUPPORT_MODE | IS_REPLAYED | IS_CAPTURE_PHASE; diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index 959f219bbe..5066cc005a 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -17,7 +17,6 @@ import type {DOMEventName} from '../events/DOMEventNames'; // CommonJS interop named imports. import * as Scheduler from 'scheduler'; -import {DEPRECATED_dispatchEventForResponderEventSystem} from './DeprecatedDOMEventResponderSystem'; import { isReplayableDiscreteEvent, queueDiscreteEvent, @@ -34,17 +33,12 @@ import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags'; import { type EventSystemFlags, IS_LEGACY_FB_SUPPORT_MODE, - PLUGIN_EVENT_SYSTEM, - RESPONDER_EVENT_SYSTEM, } from './EventSystemFlags'; import getEventTarget from './getEventTarget'; import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree'; -import { - enableDeprecatedFlareAPI, - enableLegacyFBSupport, -} from 'shared/ReactFeatureFlags'; +import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags'; import { UserBlockingEvent, ContinuousEvent, @@ -136,7 +130,7 @@ function dispatchDiscreteEvent( // flushed for this event and we don't need to do it again. (eventSystemFlags & IS_LEGACY_FB_SUPPORT_MODE) === 0 ) { - flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp); + flushDiscreteUpdatesIfNeeded(); } discreteUpdates( dispatchEvent, @@ -238,35 +232,13 @@ export function dispatchEvent( // This is not replayable so we'll invoke it but without a target, // in case the event system needs to trace it. - if (enableDeprecatedFlareAPI) { - if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { - dispatchEventForPluginEventSystem( - domEventName, - eventSystemFlags, - nativeEvent, - null, - targetContainer, - ); - } - if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { - // React Flare event system - DEPRECATED_dispatchEventForResponderEventSystem( - (domEventName: any), - null, - nativeEvent, - getEventTarget(nativeEvent), - eventSystemFlags, - ); - } - } else { - dispatchEventForPluginEventSystem( - domEventName, - eventSystemFlags, - nativeEvent, - null, - targetContainer, - ); - } + dispatchEventForPluginEventSystem( + domEventName, + eventSystemFlags, + nativeEvent, + null, + targetContainer, + ); } // Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked. @@ -318,36 +290,13 @@ export function attemptToDispatchEvent( } } } - - if (enableDeprecatedFlareAPI) { - if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) { - dispatchEventForPluginEventSystem( - domEventName, - eventSystemFlags, - nativeEvent, - targetInst, - targetContainer, - ); - } - if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) { - // React Flare event system - DEPRECATED_dispatchEventForResponderEventSystem( - (domEventName: any), - targetInst, - nativeEvent, - nativeEventTarget, - eventSystemFlags, - ); - } - } else { - dispatchEventForPluginEventSystem( - domEventName, - eventSystemFlags, - nativeEvent, - targetInst, - targetContainer, - ); - } + dispatchEventForPluginEventSystem( + domEventName, + 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 6504e238c9..ded99d1961 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -10,15 +10,11 @@ import type {AnyNativeEvent} from '../events/PluginModuleType'; import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMEventName} from '../events/DOMEventNames'; -import type {ElementListenerMap} from '../client/ReactDOMComponentTree'; import type {EventSystemFlags} from './EventSystemFlags'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {LanePriority} from 'react-reconciler/src/ReactFiberLane'; -import { - enableDeprecatedFlareAPI, - enableSelectiveHydration, -} from 'shared/ReactFeatureFlags'; +import {enableSelectiveHydration} from 'shared/ReactFeatureFlags'; import { unstable_runWithPriority as runWithPriority, unstable_scheduleCallback as scheduleCallback, @@ -34,7 +30,6 @@ import {attemptToDispatchEvent} from './ReactDOMEventListener'; import { getInstanceFromNode, getClosestInstanceFromNode, - getEventListenerMap, } from '../client/ReactDOMComponentTree'; import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags'; @@ -88,7 +83,6 @@ type PointerEvent = Event & { import {IS_REPLAYED} from './EventSystemFlags'; import {listenToNativeEvent} from './DOMPluginEventSystem'; -import {addResponderEventSystemEvent} from './DeprecatedDOMEventResponderSystem'; type QueuedReplayableEvent = {| blockedOn: null | Container | SuspenseInstance, @@ -186,43 +180,17 @@ function trapReplayableEventForContainer( listenToNativeEvent(domEventName, false, ((container: any): Element), null); } -function trapReplayableEventForDocument( - domEventName: DOMEventName, - document: Document, - listenerMap: ElementListenerMap, -) { - if (enableDeprecatedFlareAPI) { - // Trap events for the responder system. - // TODO: Ideally we shouldn't need these to be active but - // if we only have a passive listener, we at least need it - // to still pretend to be active so that Flare gets those - // events. - const activeEventKey = domEventName + '_active'; - if (!listenerMap.has(activeEventKey)) { - const listener = addResponderEventSystemEvent( - document, - domEventName, - false, - ); - listenerMap.set(activeEventKey, {passive: false, listener}); - } - } -} - export function eagerlyTrapReplayableEvents( container: Container, document: Document, ) { - const listenerMapForDoc = getEventListenerMap(document); // Discrete discreteReplayableEvents.forEach(domEventName => { trapReplayableEventForContainer(domEventName, container); - trapReplayableEventForDocument(domEventName, document, listenerMapForDoc); }); // Continuous continuousReplayableEvents.forEach(domEventName => { trapReplayableEventForContainer(domEventName, container); - trapReplayableEventForDocument(domEventName, document, listenerMapForDoc); }); } diff --git a/packages/react-dom/src/events/ReactDOMUpdateBatching.js b/packages/react-dom/src/events/ReactDOMUpdateBatching.js index b4fbc9d1d8..68d33a8c65 100644 --- a/packages/react-dom/src/events/ReactDOMUpdateBatching.js +++ b/packages/react-dom/src/events/ReactDOMUpdateBatching.js @@ -10,9 +10,6 @@ import { restoreStateIfNeeded, } from './ReactDOMControlledComponent'; -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; -import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; - // Used as a way to call batchedUpdates when we don't have a reference to // the renderer. Such as when we're dispatching events or if third party // libraries need to call batchedUpdates. Eventually, this API will go away when @@ -77,18 +74,6 @@ export function batchedEventUpdates(fn, a, b) { } } -// This is for the React Flare event system -export function executeUserEventHandler(fn: any => void, value: any): void { - const previouslyInEventHandler = isInsideEventHandler; - try { - isInsideEventHandler = true; - const type = typeof value === 'object' && value !== null ? value.type : ''; - invokeGuardedCallbackAndCatchFirstError(type, fn, undefined, value); - } finally { - isInsideEventHandler = previouslyInEventHandler; - } -} - export function discreteUpdates(fn, a, b, c, d) { const prevIsInsideEventHandler = isInsideEventHandler; isInsideEventHandler = true; @@ -102,27 +87,8 @@ export function discreteUpdates(fn, a, b, c, d) { } } -let lastFlushedEventTimeStamp = 0; -export function flushDiscreteUpdatesIfNeeded(timeStamp: number) { - // event.timeStamp isn't overly reliable due to inconsistencies in - // how different browsers have historically provided the time stamp. - // Some browsers provide high-resolution time stamps for all events, - // some provide low-resolution time stamps for all events. FF < 52 - // even mixes both time stamps together. Some browsers even report - // negative time stamps or time stamps that are 0 (iOS9) in some cases. - // Given we are only comparing two time stamps with equality (!==), - // we are safe from the resolution differences. If the time stamp is 0 - // we bail-out of preventing the flush, which can affect semantics, - // such as if an earlier flush removes or adds event listeners that - // are fired in the subsequent flush. However, this is the same - // behaviour as we had before this change, so the risks are low. - if ( - !isInsideEventHandler && - (!enableDeprecatedFlareAPI || - timeStamp === 0 || - lastFlushedEventTimeStamp !== timeStamp) - ) { - lastFlushedEventTimeStamp = timeStamp; +export function flushDiscreteUpdatesIfNeeded() { + if (!isInsideEventHandler) { flushDiscreteUpdatesImpl(); } } diff --git a/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js deleted file mode 100644 index fc8a8875c6..0000000000 --- a/packages/react-dom/src/events/__tests__/DeprecatedDOMEventResponderSystem-test.internal.js +++ /dev/null @@ -1,1002 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - */ - -'use strict'; - -let React; -let ReactFeatureFlags; -let ReactDOM; -let ReactDOMServer; -let ReactTestRenderer; -let Scheduler; - -// FIXME: What should the public API be for setting an event's priority? Right -// now it's an enum but is that what we want? Hard coding this for now. -const DiscreteEvent = 0; - -function createEventResponder({ - onEvent, - onRootEvent, - targetEventTypes, - onMount, - onUnmount, - getInitialState, - targetPortalPropagation, -}) { - return React.DEPRECATED_createResponder('TestEventResponder', { - targetEventTypes, - onEvent, - onRootEvent, - onMount, - onUnmount, - getInitialState, - targetPortalPropagation, - }); -} - -const createEvent = (type, data) => { - const event = document.createEvent('CustomEvent'); - event.initCustomEvent(type, true, true); - if (data != null) { - Object.entries(data).forEach(([key, value]) => { - event[key] = value; - }); - } - return event; -}; - -function dispatchEvent(element, type) { - const event = document.createEvent('Event'); - event.initEvent(type, true, true); - element.dispatchEvent(event); -} - -function dispatchClickEvent(element) { - dispatchEvent(element, 'click'); -} - -// This is a new feature in Fiber so I put it in its own test file. It could -// probably move to one of the other test files once it is official. -describe('DOMEventResponderSystem', () => { - let container; - - beforeEach(() => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableDeprecatedFlareAPI = true; - ReactFeatureFlags.enableScopeAPI = true; - React = require('react'); - ReactDOM = require('react-dom'); - ReactDOMServer = require('react-dom/server'); - Scheduler = require('scheduler'); - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - container = null; - }); - - // @gate experimental - it('can mount and render correctly with the ReactTestRenderer', () => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableDeprecatedFlareAPI = true; - React = require('react'); - ReactTestRenderer = require('react-test-renderer'); - const TestResponder = createEventResponder({}); - - function Test() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - - return
Hello world
; - } - const renderer = ReactTestRenderer.create(); - expect(renderer).toMatchRenderedOutput(
Hello world
); - }); - - // @gate experimental - it('can render correctly with the ReactDOMServer', () => { - const TestResponder = createEventResponder({}); - - function Test() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - - return
Hello world
; - } - const output = ReactDOMServer.renderToString(); - expect(output).toBe(`
Hello world
`); - }); - - // @gate experimental - it('can render correctly with the ReactDOMServer hydration', () => { - const onEvent = jest.fn(); - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent, - }); - const ref = React.createRef(); - - function Test() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - - return ( -
- - Hello world - -
- ); - } - const output = ReactDOMServer.renderToString(); - expect(output).toBe( - `
Hello world
`, - ); - container.innerHTML = output; - ReactDOM.hydrate(, container); - dispatchClickEvent(ref.current); - expect(onEvent).toHaveBeenCalledTimes(1); - }); - - // @gate experimental - it('the event responders should fire on click event', () => { - let eventResponderFiredCount = 0; - const eventLog = []; - const buttonRef = React.createRef(); - - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - eventResponderFiredCount++; - eventLog.push({ - name: event.type, - passive: event.passive, - phase: 'bubble', - }); - }, - }); - - function Test() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - - return ( - - ); - } - - ReactDOM.render(, container); - expect(container.innerHTML).toBe(''); - - // Clicking the button should trigger the event responder onEvent() twice - let buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - expect(eventResponderFiredCount).toBe(1); - expect(eventLog.length).toBe(1); - expect(eventLog).toEqual([ - { - name: 'click', - passive: true, - phase: 'bubble', - }, - ]); - - // Unmounting the container and clicking should not increment anything - ReactDOM.render(null, container); - dispatchClickEvent(buttonElement); - expect(eventResponderFiredCount).toBe(1); - - // Re-rendering the container and clicking should increase the counters again - ReactDOM.render(, container); - buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - expect(eventResponderFiredCount).toBe(2); - }); - - // @gate experimental - it('the event responders should fire on click event (passive events forced)', () => { - // JSDOM does not support passive events, so this manually overrides the value to be true - const checkPassiveEvents = require('react-dom/src/events/checkPassiveEvents'); - checkPassiveEvents.passiveBrowserEventsSupported = true; - - const eventLog = []; - const buttonRef = React.createRef(); - - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - eventLog.push({ - name: event.type, - passive: event.passive, - phase: 'bubble', - }); - }, - }); - - function Test() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - - return ( - - ); - } - - ReactDOM.render(, container); - - // Clicking the button should trigger the event responder onEvent() - const buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - expect(eventLog.length).toBe(1); - expect(eventLog).toEqual([ - { - name: 'click', - passive: true, - phase: 'bubble', - }, - ]); - }); - - // @gate experimental - it('nested event responders should not fire multiple times', () => { - let eventResponderFiredCount = 0; - let eventLog = []; - const buttonRef = React.createRef(); - - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - eventResponderFiredCount++; - eventLog.push({ - name: event.type, - passive: event.passive, - phase: 'bubble', - }); - }, - }); - - function Test() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - const listener2 = React.DEPRECATED_useResponder(TestResponder, {}); - - return ( - - ); - } - - expect(() => { - ReactDOM.render(, container); - }).toErrorDev( - 'Duplicate event responder "TestEventResponder" found in event listeners. ' + - 'Event listeners passed to elements cannot use the same event responder more than once.', - ); - - // Clicking the button should trigger the event responder onEvent() - let buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - expect(eventResponderFiredCount).toBe(1); - expect(eventLog.length).toBe(1); - expect(eventLog).toEqual([ - { - name: 'click', - passive: true, - phase: 'bubble', - }, - ]); - - eventLog = []; - - function Test2() { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - - return ( -
- -
- ); - } - - ReactDOM.render(, container); - - // Clicking the button should trigger the event responder onEvent() - buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - expect(eventResponderFiredCount).toBe(2); - expect(eventLog.length).toBe(1); - - expect(eventLog).toEqual([ - { - name: 'click', - passive: true, - phase: 'bubble', - }, - ]); - }); - - // @gate experimental - it('nested event responders should fire in the correct order', () => { - let eventLog = []; - const buttonRef = React.createRef(); - - const TestResponderA = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - eventLog.push(`A [bubble]`); - }, - }); - - const TestResponderB = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - eventLog.push(`B [bubble]`); - }, - }); - - function Test() { - const listener = React.DEPRECATED_useResponder(TestResponderA, {}); - const listener2 = React.DEPRECATED_useResponder(TestResponderB, {}); - - return ( - - ); - } - - ReactDOM.render(, container); - - // Clicking the button should trigger the event responder onEvent() - let buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - - expect(eventLog).toEqual(['A [bubble]', 'B [bubble]']); - - eventLog = []; - - function Test2() { - const listener = React.DEPRECATED_useResponder(TestResponderA, {}); - const listener2 = React.DEPRECATED_useResponder(TestResponderB, {}); - - return ( -
- -
- ); - } - - ReactDOM.render(, container); - - // Clicking the button should trigger the event responder onEvent() - buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - - expect(eventLog).toEqual(['B [bubble]', 'A [bubble]']); - }); - - // @gate experimental - it('nested event responders should fire in the correct order #2', () => { - const eventLog = []; - const buttonRef = React.createRef(); - - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - eventLog.push(`${props.name} [bubble]`); - }, - }); - - const Test = () => { - const listener = React.DEPRECATED_useResponder(TestResponder, { - name: 'A', - }); - const listener2 = React.DEPRECATED_useResponder(TestResponder, { - name: 'B', - }); - return ( -
- -
- ); - }; - - ReactDOM.render(, container); - - // Clicking the button should trigger the event responder onEvent() - const buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - - expect(eventLog).toEqual(['B [bubble]']); - }); - - // @gate experimental - it('custom event dispatching for click -> magicClick works', () => { - const eventLog = []; - const buttonRef = React.createRef(); - - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - const syntheticEvent = { - target: event.target, - type: 'magicclick', - phase: 'bubble', - timeStamp: context.getTimeStamp(), - }; - context.dispatchEvent( - syntheticEvent, - props.onMagicClick, - DiscreteEvent, - ); - }, - }); - - function handleMagicEvent(e) { - eventLog.push('magic event fired', e.type, e.phase); - } - - const Test = () => { - const listener = React.DEPRECATED_useResponder(TestResponder, { - onMagicClick: handleMagicEvent, - }); - - return ( - - ); - }; - - ReactDOM.render(, container); - - // Clicking the button should trigger the event responder onEvent() - const buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - - expect(eventLog).toEqual(['magic event fired', 'magicclick', 'bubble']); - }); - - // @gate experimental - it('the event responder onMount() function should fire', () => { - let onMountFired = 0; - - const TestResponder = createEventResponder({ - targetEventTypes: [], - onMount: () => { - onMountFired++; - }, - }); - - const TestResponder2 = createEventResponder({ - targetEventTypes: [], - onMount: () => { - onMountFired++; - }, - }); - - function Test({toggle}) { - const listener = React.DEPRECATED_useResponder(TestResponder, {}); - const listener2 = React.DEPRECATED_useResponder(TestResponder2, {}); - if (toggle) { - return - - ); - }; - - ReactDOM.render(, container); - - const buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - - expect(clickEventComponent1Fired).toBe(1); - expect(clickEventComponent2Fired).toBe(1); - expect(eventLog.length).toBe(2); - expect(eventLog).toEqual([ - { - name: 'click', - passive: false, - }, - { - name: 'click', - passive: false, - }, - ]); - }); - - // @gate experimental - it('the event responder system should warn on accessing invalid properties', () => { - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent: (event, context, props) => { - const syntheticEvent = { - target: event.target, - type: 'click', - timeStamp: context.getTimeStamp(), - }; - context.dispatchEvent(syntheticEvent, props.onClick, DiscreteEvent); - }, - }); - - let handler; - const buttonRef = React.createRef(); - const Test = () => { - const listener = React.DEPRECATED_useResponder(TestResponder, { - onClick: handler, - }); - - return ( - - ); - }; - expect(() => { - handler = event => { - event.isDefaultPrevented(); - }; - ReactDOM.render(, container); - dispatchClickEvent(buttonRef.current); - }).toErrorDev( - 'Warning: isDefaultPrevented() is not available on event objects created from event responder modules ' + - '(React Flare).' + - ' Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.isDefaultPrevented() }`', - {withoutStack: true}, - ); - expect(() => { - handler = event => { - event.isPropagationStopped(); - }; - ReactDOM.render(, container); - dispatchClickEvent(buttonRef.current); - }).toErrorDev( - 'Warning: isPropagationStopped() is not available on event objects created from event responder modules ' + - '(React Flare).' + - ' Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.isPropagationStopped() }`', - {withoutStack: true}, - ); - expect(() => { - handler = event => { - return event.nativeEvent; - }; - ReactDOM.render(, container); - dispatchClickEvent(buttonRef.current); - }).toErrorDev( - 'Warning: nativeEvent is not available on event objects created from event responder modules ' + - '(React Flare).' + - ' Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.nativeEvent }`', - {withoutStack: true}, - ); - expect(container.innerHTML).toBe(''); - }); - - // @gate experimental - it('should work with event responder hooks', () => { - const buttonRef = React.createRef(); - const eventLogs = []; - const TestResponder = createEventResponder({ - targetEventTypes: ['foo'], - onEvent: (event, context, props) => { - const fooEvent = { - target: event.target, - type: 'foo', - timeStamp: context.getTimeStamp(), - }; - context.dispatchEvent(fooEvent, props.onFoo, DiscreteEvent); - }, - }); - - const Test = () => { - const listener = React.DEPRECATED_useResponder(TestResponder, { - onFoo: e => eventLogs.push('hook'), - }); - - return - ); - } - - const root = ReactDOM.createRoot(container); - root.render(); - expect(Scheduler).toFlushAndYield(['Test']); - - // Click the button - dispatchClickEvent(ref.current); - expect(log).toEqual([{counter: 0}]); - - // Clear log - log.length = 0; - - // Increase counter - root.render(); - // Yield before committing - expect(Scheduler).toFlushAndYieldThrough(['Test']); - - // Click the button again - dispatchClickEvent(ref.current); - expect(log).toEqual([{counter: 0}]); - - // Clear log - log.length = 0; - - // Commit - expect(Scheduler).toFlushAndYield([]); - dispatchClickEvent(ref.current); - expect(log).toEqual([{counter: 1}]); - }); - - // @gate experimental - it('should correctly pass through event properties', () => { - const timeStamps = []; - const ref = React.createRef(); - const eventLog = []; - const logEvent = event => { - const propertiesWeCareAbout = { - counter: event.counter, - target: event.target, - timeStamp: event.timeStamp, - type: event.type, - }; - timeStamps.push(event.timeStamp); - eventLog.push(propertiesWeCareAbout); - }; - let counter = 0; - - const TestResponder = createEventResponder({ - targetEventTypes: ['click'], - onEvent(event, context, props) { - const obj = { - counter, - timeStamp: context.getTimeStamp(), - target: context.getResponderNode(), - type: 'click-test', - }; - context.dispatchEvent(obj, props.onClick, DiscreteEvent); - }, - }); - - const Component = () => { - const listener = React.DEPRECATED_useResponder(TestResponder, { - onClick: logEvent, - }); - return - ); - } - - ReactDOM.render(, container); - expect(container.innerHTML).toBe(''); - - const buttonElement = buttonRef.current; - dispatchClickEvent(buttonElement); - expect(eventResponderFiredCount).toBe(1); - dispatchClickEvent(buttonElement); - expect(eventResponderFiredCount).toBe(2); - }); -}); diff --git a/packages/react-dom/src/events/checkPassiveEvents.js b/packages/react-dom/src/events/checkPassiveEvents.js index da31b7d440..415a13395c 100644 --- a/packages/react-dom/src/events/checkPassiveEvents.js +++ b/packages/react-dom/src/events/checkPassiveEvents.js @@ -8,13 +8,13 @@ */ import {canUseDOM} from 'shared/ExecutionEnvironment'; -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; +import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags'; export let passiveBrowserEventsSupported = false; // Check if browser support events with passive listeners // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support -if (enableDeprecatedFlareAPI && canUseDOM) { +if (enableCreateEventHandleAPI && canUseDOM) { try { const options = {}; // $FlowFixMe: Ignore Flow complaining about needing a value diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 56c81fbb0f..1754555dd5 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -23,7 +23,6 @@ import { disableModulePatternComponents, enableSuspenseServerRenderer, enableFundamentalAPI, - enableDeprecatedFlareAPI, enableScopeAPI, } from 'shared/ReactFeatureFlags'; @@ -368,9 +367,6 @@ function createOpenTagMarkup( if (!hasOwnProperty.call(props, propKey)) { continue; } - if (enableDeprecatedFlareAPI && propKey === 'DEPRECATED_flareListeners') { - continue; - } let propValue = props[propKey]; if (propValue == null) { continue; diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index a6050d91c6..8cc7be4716 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -14,7 +14,6 @@ import type { MutableSourceGetSnapshotFn, MutableSourceSubscribeFn, ReactContext, - ReactEventResponderListener, } from 'shared/ReactTypes'; import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig'; import type PartialRenderer from './ReactPartialRenderer'; @@ -457,13 +456,6 @@ export function useCallback( return useMemo(() => callback, deps); } -function useResponder(responder, props): ReactEventResponderListener { - return { - props, - responder, - }; -} - // TODO Decide on how to implement this hook for server rendering. // If a mutation occurs during render, consider triggering a Suspense boundary // and falling back to client rendering. @@ -521,7 +513,6 @@ export const Dispatcher: DispatcherType = { useEffect: noop, // Debugging effect useDebugValue: noop, - useResponder, useDeferredValue, useTransition, useOpaqueIdentifier, diff --git a/packages/react-dom/src/shared/DOMProperty.js b/packages/react-dom/src/shared/DOMProperty.js index b763861e0e..2d9a7914f9 100644 --- a/packages/react-dom/src/shared/DOMProperty.js +++ b/packages/react-dom/src/shared/DOMProperty.js @@ -7,10 +7,7 @@ * @flow */ -import { - enableDeprecatedFlareAPI, - enableFilterEmptyStringAttributesDOM, -} from 'shared/ReactFeatureFlags'; +import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags'; type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6; @@ -252,9 +249,6 @@ const reservedProps = [ 'suppressHydrationWarning', 'style', ]; -if (enableDeprecatedFlareAPI) { - reservedProps.push('DEPRECATED_flareListeners'); -} reservedProps.forEach(name => { properties[name] = new PropertyInfoRecord( diff --git a/packages/react-dom/src/shared/ReactControlledValuePropTypes.js b/packages/react-dom/src/shared/ReactControlledValuePropTypes.js index c15c4a94aa..62c7709496 100644 --- a/packages/react-dom/src/shared/ReactControlledValuePropTypes.js +++ b/packages/react-dom/src/shared/ReactControlledValuePropTypes.js @@ -5,8 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags'; - const hasReadOnlyValue = { button: true, checkbox: true, @@ -29,8 +27,7 @@ export function checkControlledValueProps( props.onInput || props.readOnly || props.disabled || - props.value == null || - (enableDeprecatedFlareAPI && props.DEPRECATED_flareListeners) + props.value == null ) ) { console.error( @@ -46,8 +43,7 @@ export function checkControlledValueProps( props.onChange || props.readOnly || props.disabled || - props.checked == null || - (enableDeprecatedFlareAPI && props.DEPRECATED_flareListeners) + props.checked == null ) ) { console.error( diff --git a/packages/react-dom/src/shared/ReactDOMTypes.js b/packages/react-dom/src/shared/ReactDOMTypes.js index 7ae85387ee..774e776b1a 100644 --- a/packages/react-dom/src/shared/ReactDOMTypes.js +++ b/packages/react-dom/src/shared/ReactDOMTypes.js @@ -9,75 +9,15 @@ import type { ReactFundamentalComponentInstance, - ReactEventResponder, - ReactEventResponderInstance, - EventPriority, ReactScopeInstance, } from 'shared/ReactTypes'; import type {DOMEventName} from '../events/DOMEventNames'; -type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | Touch; - -export type PointerType = - | '' - | 'mouse' - | 'keyboard' - | 'pen' - | 'touch' - | 'trackpad'; - -export type ReactDOMResponderEvent = { - nativeEvent: AnyNativeEvent, - passive: boolean, - pointerType: PointerType, - target: Element | Document, - type: string, - ... -}; - -export type ReactDOMEventResponder = ReactEventResponder< - ReactDOMResponderEvent, - ReactDOMResponderContext, ->; - -export type ReactDOMEventResponderInstance = ReactEventResponderInstance< - ReactDOMResponderEvent, - ReactDOMResponderContext, ->; - export type ReactDOMFundamentalComponentInstance = ReactFundamentalComponentInstance< any, any, >; -export type ReactDOMResponderContext = { - dispatchEvent: ( - eventValue: any, - listener: (any) => void, - eventPriority: EventPriority, - ) => void, - isTargetWithinNode: ( - childTarget: Element | Document, - parentTarget: Element | Document, - ) => boolean, - isTargetWithinResponder: (null | Element | Document) => boolean, - isTargetWithinResponderScope: (null | Element | Document) => boolean, - addRootEventTypes: (rootEventTypes: Array) => void, - removeRootEventTypes: (rootEventTypes: Array) => void, - getActiveDocument(): Document, - objectAssign: Function, - getTimeStamp: () => number, - isTargetWithinHostComponent: ( - target: Element | Document, - elementType: string, - ) => boolean, - continuePropagation(): void, - // Used for controller components - enqueueStateRestore(Element | Document): void, - getResponderNode(): Element | null, - ... -}; - export type ReactDOMEventHandle = ( target: EventTarget | ReactScopeInstance, callback: (SyntheticEvent) => void, diff --git a/packages/react-interactions/events/README.md b/packages/react-interactions/events/README.md deleted file mode 100644 index 6f49251a3f..0000000000 --- a/packages/react-interactions/events/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# `react-interactions/events` - -*This package is experimental. It is intended for use with the experimental React -events API that is not available in open source builds.* - -Event Responders attach to a host node. They listen to native browser events -dispatched on the host node of their child and transform those events into -high-level events for applications. - -The core API is documented below. Documentation for individual Event Responders -can be found [here](./docs). - -## Event Responder Interface - -Note: React Responders require the internal React flag `enableDeprecatedFlareAPI`. - -An Event Responder Interface is defined using an object. Each responder can define DOM -events to listen to, handle the synthetic responder events, dispatch custom -events, and implement a state machine. - -```js -// types -type ResponderEventType = string; - -type ResponderEvent = {| - nativeEvent: any, - target: Element | Document, - pointerType: string, - type: string, - passive: boolean, -|}; - -type CustomEvent = { - type: string, - target: Element, - ... -} -``` - -### getInitialState?: (props: null | Object) => Object - -The initial state of that the Event Responder is created with. - -### onEvent?: (event: ResponderEvent, context: ResponderContext, props, state) - -Called during the bubble phase of the `targetEventTypes` dispatched on DOM -elements within the Event Responder. - -### onMount?: (context: ResponderContext, props, state) - -Called after an Event Responder in mounted. - -### onRootEvent?: (event: ResponderEvent, context: ResponderContext, props, state) - -Called when any of the `rootEventTypes` are dispatched on the root of the app. - -### onUnmount?: (context: ResponderContext, props, state) - -Called before an Event Responder in unmounted. - -### rootEventTypes?: Array - -Defines the DOM events to listen to on the root of the app. - -### targetEventTypes?: Array - -Defines the DOM events to listen to within the Event Responder subtree. - -## ResponderContext - -The Event Responder Context is exposed via the `context` argument for certain methods -on the `EventResponder` object. - -### addRootEventTypes(eventTypes: Array) - -This can be used to dynamically listen to events on the root of the app only -when it is necessary to do so. - -### dispatchEvent(propName: string, event: CustomEvent, { discrete: boolean }) - -Dispatches a custom synthetic event. The `type` and `target` are required -fields if the event is an object, but any other fields can be defined on the `event` that will be passed -to the `listener`. You can also pass a value that is not an object, but a `boolean`. For example: - -```js -const event = { type: 'press', target, pointerType, x, y }; -context.dispatchEvent('onPress', event, DiscreteEvent); -``` - -### isTargetWithinNode(target: Element, element: Element): boolean - -Returns `true` if `target` is a child of `element`. - -### isTargetWithinResponder(target: Element): boolean - -Returns `true` is the target element is within the subtree of the Event Responder. - -### isTargetWithinResponderScope(target: Element): boolean - -Returns `true` is the target element is within the current Event Responder's scope. If the target element -is within the scope of the same responder, but owned by another Event Responder instance, this will return `false`. - -### removeRootEventTypes(eventTypes: Array) - -Remove the root event types added with `addRootEventTypes`. diff --git a/packages/react-interactions/events/context-menu.js b/packages/react-interactions/events/context-menu.js deleted file mode 100644 index d5a0953841..0000000000 --- a/packages/react-interactions/events/context-menu.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -export * from './src/dom/ContextMenu'; diff --git a/packages/react-interactions/events/deprecated-focus.js b/packages/react-interactions/events/deprecated-focus.js deleted file mode 100644 index 4a65690ca8..0000000000 --- a/packages/react-interactions/events/deprecated-focus.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -export * from './src/dom/DeprecatedFocus'; diff --git a/packages/react-interactions/events/docs/ContextMenu.md b/packages/react-interactions/events/docs/ContextMenu.md deleted file mode 100644 index 99e8f1a140..0000000000 --- a/packages/react-interactions/events/docs/ContextMenu.md +++ /dev/null @@ -1,54 +0,0 @@ -# ContextMenu - -The `useContextMenu` hooks responds to context-menu events. - -```js -// Example -const Button = (props) => { - const contextmenu = useContextMenu({ - disabled, - onContextMenu, - preventDefault - }); - - return ( -
- {props.children} -
- ); -}; -``` - -## Types - -```js -type ContextMenuEvent = { - altKey: boolean, - buttons: 0 | 1 | 2, - ctrlKey: boolean, - metaKey: boolean, - pageX: number, - pageY: number, - pointerType: PointerType, - shiftKey: boolean, - target: Element, - timeStamp: number, - type: 'contextmenu', - x: number, - y: number, -} -``` - -## Props - -### disabled: boolean = false - -Disables the responder. - -### onContextMenu: (e: ContextMenuEvent) => void - -Called when the user performs a gesture to display a context menu. - -### preventDefault: boolean = true - -Prevents the native behavior (i.e., context menu). diff --git a/packages/react-interactions/events/docs/Focus.md b/packages/react-interactions/events/docs/Focus.md deleted file mode 100644 index 4d9f7e8b1c..0000000000 --- a/packages/react-interactions/events/docs/Focus.md +++ /dev/null @@ -1,64 +0,0 @@ -# Focus - -The `useFocus` hook responds to focus and blur events on its child. Focus events -are dispatched for all input types, with the exception of `onFocusVisibleChange` -which is only dispatched when focusing with a keyboard. - -Focus events do not propagate between `useFocus` event responders. - -```js -// Example -const Button = (props) => { - const [ isFocusVisible, setFocusVisible ] = useState(false); - const focus = useFocus({ - onBlur={props.onBlur} - onFocus={props.onFocus} - onFocusVisibleChange={setFocusVisible} - }); - - return ( - } - {show && } - {show && } - {show && } - {!show && } - {show && } - - - - ); - }; - - ReactDOM.render(, container); - - inputRef.current.focus(); - expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0); - expect(onAfterBlurWithin).toHaveBeenCalledTimes(0); - ReactDOM.render(, container); - expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1); - expect(onAfterBlurWithin).toHaveBeenCalledTimes(1); - }); - - // @gate experimental - it('is called after a nested focused element is unmounted (with scope query)', () => { - const TestScope = React.unstable_Scope; - const testScopeQuery = (type, props) => true; - let targetNodes; - let targetNode; - - const Component = ({show}) => { - const scopeRef = React.useRef(null); - const listener = useFocusWithin({ - onBeforeBlurWithin(event) { - const scope = scopeRef.current; - targetNode = innerRef.current; - targetNodes = scope.DO_NOT_USE_queryAllNodes(testScopeQuery); - }, - }); - - return ( - - {show && } - - ); - }; - - ReactDOM.render(, container); - - const inner = innerRef.current; - const target = createEventTarget(inner); - target.keydown({key: 'Tab'}); - target.focus(); - ReactDOM.render(, container); - expect(targetNodes).toEqual([targetNode]); - }); - - // @gate experimental - it('is called after a focused suspended element is hidden', () => { - const Suspense = React.Suspense; - let suspend = false; - let resolve; - const promise = new Promise(resolvePromise => (resolve = resolvePromise)); - - function Child() { - if (suspend) { - throw promise; - } else { - return ; - } - } - - const Component = ({show}) => { - const listener = useFocusWithin({ - onBeforeBlurWithin, - onAfterBlurWithin, - }); - - return ( -
- - - -
- ); - }; - - const root = ReactDOM.createRoot(container2); - act(() => { - root.render(); - }); - jest.runAllTimers(); - expect(container2.innerHTML).toBe('
'); - - const inner = innerRef.current; - const target = createEventTarget(inner); - target.keydown({key: 'Tab'}); - target.focus(); - expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0); - expect(onAfterBlurWithin).toHaveBeenCalledTimes(0); - - suspend = true; - act(() => { - root.render(); - }); - jest.runAllTimers(); - expect(container2.innerHTML).toBe( - '
Loading...
', - ); - expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1); - expect(onAfterBlurWithin).toHaveBeenCalledTimes(1); - resolve(); - }); - - // @gate experimental - it('is called after a focused suspended element is hidden then shown', () => { - const Suspense = React.Suspense; - let suspend = false; - let resolve; - const promise = new Promise(resolvePromise => (resolve = resolvePromise)); - const buttonRef = React.createRef(); - - function Child() { - if (suspend) { - throw promise; - } else { - return ; - } - } - - const Component = ({show}) => { - const listener = useFocusWithin({ - onBeforeBlurWithin, - onAfterBlurWithin, - }); - - return ( -
- Loading...}> - - -
- ); - }; - - const root = ReactDOM.createRoot(container2); - - act(() => { - root.render(); - }); - jest.runAllTimers(); - - expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0); - expect(onAfterBlurWithin).toHaveBeenCalledTimes(0); - - suspend = true; - act(() => { - root.render(); - }); - jest.runAllTimers(); - expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0); - expect(onAfterBlurWithin).toHaveBeenCalledTimes(0); - - act(() => { - root.render(); - }); - jest.runAllTimers(); - expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0); - expect(onAfterBlurWithin).toHaveBeenCalledTimes(0); - - buttonRef.current.focus(); - suspend = false; - act(() => { - root.render(); - }); - jest.runAllTimers(); - expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1); - expect(onAfterBlurWithin).toHaveBeenCalledTimes(1); - - resolve(); - }); - }); - - // @gate experimental - it('expect displayName to show up for event component', () => { - expect(FocusWithinResponder.displayName).toBe('FocusWithin'); - }); -}); diff --git a/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js deleted file mode 100644 index 19f6269f2d..0000000000 --- a/packages/react-interactions/events/src/dom/__tests__/Hover-test.internal.js +++ /dev/null @@ -1,430 +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'; - -import {createEventTarget, setPointerEvent} from 'dom-event-testing-library'; - -let React; -let ReactFeatureFlags; -let ReactDOM; -let HoverResponder; -let useHover; - -function initializeModules(hasPointerEvents) { - jest.resetModules(); - setPointerEvent(hasPointerEvents); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableDeprecatedFlareAPI = true; - React = require('react'); - ReactDOM = require('react-dom'); - - // TODO: This import throws outside of experimental mode. Figure out better - // strategy for gated imports. - if (__EXPERIMENTAL__) { - HoverResponder = require('react-interactions/events/hover').HoverResponder; - useHover = require('react-interactions/events/hover').useHover; - } -} - -const forcePointerEvents = true; -const table = [[forcePointerEvents], [!forcePointerEvents]]; - -describe.each(table)('Hover responder', hasPointerEvents => { - let container; - - beforeEach(() => { - initializeModules(hasPointerEvents); - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - ReactDOM.render(null, container); - document.body.removeChild(container); - container = null; - }); - - describe('disabled', () => { - let onHoverChange, onHoverStart, onHoverMove, onHoverEnd, ref; - - const componentInit = () => { - onHoverChange = jest.fn(); - onHoverStart = jest.fn(); - onHoverMove = jest.fn(); - onHoverEnd = jest.fn(); - ref = React.createRef(); - const Component = () => { - const listener = useHover({ - disabled: true, - onHoverChange, - onHoverStart, - onHoverMove, - onHoverEnd, - }); - return
; - }; - ReactDOM.render(, container); - }; - - // @gate experimental - it('does not call callbacks', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerenter(); - target.pointerexit(); - expect(onHoverChange).not.toBeCalled(); - expect(onHoverStart).not.toBeCalled(); - expect(onHoverMove).not.toBeCalled(); - expect(onHoverEnd).not.toBeCalled(); - }); - }); - - describe('onHoverStart', () => { - let onHoverStart, ref; - - const componentInit = () => { - onHoverStart = jest.fn(); - ref = React.createRef(); - const Component = () => { - const listener = useHover({ - onHoverStart: onHoverStart, - }); - return
; - }; - ReactDOM.render(, container); - }; - - // @gate experimental - it('is called for mouse pointers', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerenter(); - expect(onHoverStart).toHaveBeenCalledTimes(1); - }); - - // @gate experimental - it('is not called for touch pointers', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerdown({pointerType: 'touch'}); - target.pointerup({pointerType: 'touch'}); - expect(onHoverStart).not.toBeCalled(); - }); - - // @gate experimental - it('is called if a mouse pointer is used after a touch pointer', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerdown({pointerType: 'touch'}); - target.pointerup({pointerType: 'touch'}); - target.pointerenter(); - expect(onHoverStart).toHaveBeenCalledTimes(1); - }); - }); - - describe('onHoverChange', () => { - let onHoverChange, ref; - - const componentInit = () => { - onHoverChange = jest.fn(); - ref = React.createRef(); - const Component = () => { - const listener = useHover({ - onHoverChange, - }); - return
; - }; - ReactDOM.render(, container); - }; - - // @gate experimental - it('is called for mouse pointers', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerenter(); - expect(onHoverChange).toHaveBeenCalledTimes(1); - expect(onHoverChange).toHaveBeenCalledWith(true); - target.pointerexit(); - expect(onHoverChange).toHaveBeenCalledTimes(2); - expect(onHoverChange).toHaveBeenCalledWith(false); - }); - - // @gate experimental - it('is not called for touch pointers', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerdown({pointerType: 'touch'}); - target.pointerup({pointerType: 'touch'}); - expect(onHoverChange).not.toBeCalled(); - }); - }); - - describe('onHoverEnd', () => { - let onHoverEnd, ref; - - const componentInit = () => { - onHoverEnd = jest.fn(); - ref = React.createRef(); - const Component = () => { - const listener = useHover({ - onHoverEnd, - }); - return
; - }; - ReactDOM.render(, container); - }; - - // @gate experimental - it('is called for mouse pointers', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerenter(); - target.pointerexit(); - expect(onHoverEnd).toHaveBeenCalledTimes(1); - }); - - if (hasPointerEvents) { - // @gate experimental - it('is called once for cancelled mouse pointers', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerenter(); - target.pointercancel(); - expect(onHoverEnd).toHaveBeenCalledTimes(1); - - // only called once if cancel follows exit - onHoverEnd.mockReset(); - target.pointerenter(); - target.pointerexit(); - target.pointercancel(); - expect(onHoverEnd).toHaveBeenCalledTimes(1); - }); - } - - // @gate experimental - it('is not called for touch pointers', () => { - componentInit(); - const target = createEventTarget(ref.current); - target.pointerdown({pointerType: 'touch'}); - target.pointerup({pointerType: 'touch'}); - expect(onHoverEnd).not.toBeCalled(); - }); - - // @gate experimental - it('should correctly work with React Portals', () => { - componentInit(); - const portalNode = document.createElement('div'); - const divRef = React.createRef(); - const spanRef = React.createRef(); - - function Test() { - const listener = useHover({ - onHoverEnd, - }); - return ( -
- {ReactDOM.createPortal(, portalNode)} -
- ); - } - ReactDOM.render(, container); - const div = createEventTarget(divRef.current); - div.pointerenter(); - const span = createEventTarget(spanRef.current); - span.pointerexit(); - expect(onHoverEnd).not.toBeCalled(); - const body = createEventTarget(document.body); - body.pointerexit(); - expect(onHoverEnd).toBeCalled(); - }); - }); - - describe('onHoverMove', () => { - // @gate experimental - it('is called after the active pointer moves"', () => { - const onHoverMove = jest.fn(); - const ref = React.createRef(); - const Component = () => { - const listener = useHover({ - onHoverMove, - }); - return
; - }; - ReactDOM.render(, container); - - const target = createEventTarget(ref.current); - target.pointerenter(); - target.pointerhover({x: 0, y: 0}); - target.pointerhover({x: 1, y: 1}); - expect(onHoverMove).toHaveBeenCalledTimes(2); - expect(onHoverMove).toHaveBeenCalledWith( - expect.objectContaining({type: 'hovermove'}), - ); - }); - }); - - describe('nested Hover components', () => { - // @gate experimental - it('not propagate by default', () => { - const events = []; - const innerRef = React.createRef(); - const outerRef = React.createRef(); - const createEventHandler = msg => () => { - events.push(msg); - }; - - const Inner = () => { - const listener = useHover({ - onHoverStart: createEventHandler('inner: onHoverStart'), - onHoverEnd: createEventHandler('inner: onHoverEnd'), - onHoverChange: createEventHandler('inner: onHoverChange'), - }); - return
; - }; - - const Outer = () => { - const listener = useHover({ - onHoverStart: createEventHandler('outer: onHoverStart'), - onHoverEnd: createEventHandler('outer: onHoverEnd'), - onHoverChange: createEventHandler('outer: onHoverChange'), - }); - return ( -
- -
- ); - }; - ReactDOM.render(, container); - - const innerNode = innerRef.current; - const outerNode = outerRef.current; - const innerTarget = createEventTarget(innerNode); - const outerTarget = createEventTarget(outerNode); - - outerTarget.pointerenter({relatedTarget: container}); - outerTarget.pointerexit({relatedTarget: innerNode}); - innerTarget.pointerenter({relatedTarget: outerNode}); - innerTarget.pointerexit({relatedTarget: outerNode}); - outerTarget.pointerenter({relatedTarget: innerNode}); - outerTarget.pointerexit({relatedTarget: container}); - - expect(events).toEqual([ - 'outer: onHoverStart', - 'outer: onHoverChange', - 'outer: onHoverEnd', - 'outer: onHoverChange', - 'inner: onHoverStart', - 'inner: onHoverChange', - 'inner: onHoverEnd', - 'inner: onHoverChange', - 'outer: onHoverStart', - 'outer: onHoverChange', - 'outer: onHoverEnd', - 'outer: onHoverChange', - ]); - }); - }); - - // @gate experimental - it('expect displayName to show up for event component', () => { - expect(HoverResponder.displayName).toBe('Hover'); - }); - - // @gate experimental - it('should correctly pass through event properties', () => { - const timeStamps = []; - const ref = React.createRef(); - const eventLog = []; - const logEvent = event => { - const propertiesWeCareAbout = { - x: event.x, - y: event.y, - pageX: event.pageX, - pageY: event.pageY, - clientX: event.clientX, - clientY: event.clientY, - pointerType: event.pointerType, - target: event.target, - timeStamp: event.timeStamp, - type: event.type, - }; - timeStamps.push(event.timeStamp); - eventLog.push(propertiesWeCareAbout); - }; - const Component = () => { - const listener = useHover({ - onHoverStart: logEvent, - onHoverEnd: logEvent, - onHoverMove: logEvent, - }); - return
; - }; - ReactDOM.render(, container); - - const node = ref.current; - const target = createEventTarget(node); - - target.pointerenter({x: 10, y: 10}); - target.pointerhover({x: 10, y: 10}); - target.pointerhover({x: 20, y: 20}); - target.pointerexit({x: 20, y: 20}); - - expect(eventLog).toEqual([ - { - x: 10, - y: 10, - pageX: 10, - pageY: 10, - clientX: 10, - clientY: 10, - target: node, - timeStamp: timeStamps[0], - type: 'hoverstart', - pointerType: 'mouse', - }, - { - x: 10, - y: 10, - pageX: 10, - pageY: 10, - clientX: 10, - clientY: 10, - target: node, - timeStamp: timeStamps[1], - type: 'hovermove', - pointerType: 'mouse', - }, - { - x: 20, - y: 20, - pageX: 20, - pageY: 20, - clientX: 20, - clientY: 20, - target: node, - timeStamp: timeStamps[2], - type: 'hovermove', - pointerType: 'mouse', - }, - { - x: 20, - y: 20, - pageX: 20, - pageY: 20, - clientX: 20, - clientY: 20, - target: node, - timeStamp: timeStamps[3], - type: 'hoverend', - pointerType: 'mouse', - }, - ]); - }); -}); diff --git a/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js b/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js deleted file mode 100644 index bcbe4d5224..0000000000 --- a/packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js +++ /dev/null @@ -1,1001 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @emails react-core - */ - -'use strict'; - -let React; -let ReactFeatureFlags; -let ReactDOM; -let InputResponder; -let useInput; -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; - -const modulesInit = () => { - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableDeprecatedFlareAPI = true; - - React = require('react'); - ReactDOM = require('react-dom'); - Scheduler = require('scheduler'); - - // TODO: This import throws outside of experimental mode. Figure out better - // strategy for gated imports. - if (__EXPERIMENTAL__) { - InputResponder = require('react-interactions/events/input').InputResponder; - useInput = require('react-interactions/events/input').useInput; - } -}; - -describe('Input event responder', () => { - let container; - - beforeEach(() => { - jest.resetModules(); - modulesInit(); - - container = document.createElement('div'); - document.body.appendChild(container); - }); - - afterEach(() => { - document.body.removeChild(container); - container = null; - }); - - describe('disabled', () => { - let onChange, onValueChange, ref; - - const componentInit = () => { - onChange = jest.fn(); - onValueChange = jest.fn(); - ref = React.createRef(); - - function Component() { - const listener = useInput({ - disabled: true, - onChange, - onValueChange, - }); - return ; - } - ReactDOM.render(, container); - }; - - // @gate experimental - it('prevents custom events being dispatched', () => { - componentInit(); - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - expect(onChange).not.toBeCalled(); - expect(onValueChange).not.toBeCalled(); - }); - }); - - // These were taken from the original ChangeEventPlugin-test. - // They've been updated and cleaned up for React Flare. - describe('onChange', () => { - // 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. - - // @gate experimental - it('should consider initial text value to be current', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - - if (ReactFeatureFlags.disableInputAttributeSyncing) { - // TODO: figure out why. This might be a bug. - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - } else { - expect(onChangeCalled).toBe(0); - expect(onValueChangeCalled).toBe(0); - } - }); - - // @gate experimental - it('should consider initial checkbox checked=true to be current', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - 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(ref.current, false); - ref.current.dispatchEvent( - new MouseEvent('click', {bubbles: true, cancelable: true}), - ); - - // There should be no React change events because the value stayed the same. - expect(onChangeCalled).toBe(0); - expect(onValueChangeCalled).toBe(0); - }); - - // @gate experimental - it('should consider initial checkbox checked=false to be current', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - 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(ref.current, true); - ref.current.dispatchEvent( - new MouseEvent('click', {bubbles: true, cancelable: true}), - ); - // There should be no React change events because the value stayed the same. - expect(onChangeCalled).toBe(0); - expect(onValueChangeCalled).toBe(0); - }); - - // @gate experimental - it('should fire change for checkbox input', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - - ref.current.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(ref.current.checked).toBe(true); - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - - expect(ref.current.checked).toBe(true); - ref.current.dispatchEvent( - new MouseEvent('click', {bubbles: true, cancelable: true}), - ); - expect(ref.current.checked).toBe(false); - expect(onChangeCalled).toBe(2); - expect(onValueChangeCalled).toBe(2); - }); - - // @gate experimental - it('should not fire change setting the value programmatically', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - - // Set it programmatically. - ref.current.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. - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - expect(ref.current.value).toBe('bar'); - // In this case we don't expect to get a React event. - expect(onChangeCalled).toBe(0); - expect(onValueChangeCalled).toBe(0); - - // However, we can simulate user typing by calling the underlying setter. - setUntrackedValue.call(ref.current, 'foo'); - // Now, when the event fires, the real input value ('foo') differs from the - // "current" one we previously recorded ('bar'). - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - expect(ref.current.value).toBe('foo'); - // In this case React should fire an event for it. - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - - // Verify again that extra events without real changes are ignored. - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - }); - - // @gate experimental - it('should not distinguish equal string and number values', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - 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'). - ref.current.value = 42; - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - expect(ref.current.value).toBe('42'); - expect(onChangeCalled).toBe(0); - expect(onValueChangeCalled).toBe(0); - }); - - // See a similar input test above for a detailed description of why. - // @gate experimental - it('should not fire change when setting checked programmatically', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - - // Set the value, updating the "current" value that React tracks to true. - ref.current.checked = true; - // Under the hood, uncheck the box so that the click will "check" it again. - setUntrackedChecked.call(ref.current, false); - ref.current.dispatchEvent( - new MouseEvent('click', {bubbles: true, cancelable: true}), - ); - expect(ref.current.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(onChangeCalled).toBe(0); - expect(onValueChangeCalled).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). - ref.current.click(); - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - }); - - // @gate experimental - it('should only fire change for checked radio button once', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - - setUntrackedChecked.call(ref.current, true); - ref.current.dispatchEvent( - new Event('click', {bubbles: true, cancelable: true}), - ); - ref.current.dispatchEvent( - new Event('click', {bubbles: true, cancelable: true}), - ); - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - }); - - // @gate experimental - it('should track radio button cousins in a group', () => { - let onChangeCalled1 = 0; - let onValueChangeCalled1 = 0; - let onChangeCalled2 = 0; - let onValueChangeCalled2 = 0; - const ref = React.createRef(); - - function onChange1(e) { - onChangeCalled1++; - expect(e.type).toBe('change'); - } - - function onValueChange1(e) { - onValueChangeCalled1++; - } - - function onChange2(e) { - onChangeCalled2++; - expect(e.type).toBe('change'); - } - - function onValueChange2(e) { - onValueChangeCalled2++; - } - - function Radio1() { - const listener = useInput({ - onChange: onChange1, - onValueChange: onValueChange1, - }); - return ( - - ); - } - - function Radio2() { - const listener = useInput({ - onChange: onChange2, - onValueChange: onValueChange2, - }); - return ( - - ); - } - - function Component() { - return ( -
- - -
- ); - } - ReactDOM.render(, container); - - const option1 = ref.current.childNodes[0]; - const option2 = ref.current.childNodes[1]; - - // Select first option. - option1.click(); - expect(onChangeCalled1).toBe(1); - expect(onValueChangeCalled1).toBe(1); - expect(onChangeCalled2).toBe(0); - expect(onValueChangeCalled2).toBe(0); - - // Select second option. - option2.click(); - expect(onChangeCalled1).toBe(1); - expect(onValueChangeCalled1).toBe(1); - expect(onChangeCalled2).toBe(1); - expect(onValueChangeCalled2).toBe(1); - - // Select the first option. - // It should receive the React change event again. - option1.click(); - expect(onChangeCalled1).toBe(2); - expect(onValueChangeCalled1).toBe(2); - expect(onChangeCalled2).toBe(1); - expect(onValueChangeCalled2).toBe(1); - }); - - // @gate experimental - it('should deduplicate input value change events', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - ['text', 'number', 'range'].forEach(type => { - onChangeCalled = 0; - onValueChangeCalled = 0; - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - // Should be ignored (no change): - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - setUntrackedValue.call(ref.current, '42'); - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - // Should be ignored (no change): - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - ReactDOM.unmountComponentAtNode(container); - - onChangeCalled = 0; - onValueChangeCalled = 0; - function Component2() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - // Should be ignored (no change): - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - setUntrackedValue.call(ref.current, '42'); - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - // Should be ignored (no change): - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - ReactDOM.unmountComponentAtNode(container); - - onChangeCalled = 0; - onValueChangeCalled = 0; - function Component3() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - // Should be ignored (no change): - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - setUntrackedValue.call(ref.current, '42'); - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - // Should be ignored (no change): - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - expect(onChangeCalled).toBe(1); - expect(onValueChangeCalled).toBe(1); - ReactDOM.unmountComponentAtNode(container); - }); - }); - - // @gate experimental - it('should listen for both change and input events when supported', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - - setUntrackedValue.call(ref.current, 10); - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - - setUntrackedValue.call(ref.current, 20); - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - - expect(onChangeCalled).toBe(2); - expect(onValueChangeCalled).toBe(2); - }); - - // @gate experimental - it('should only fire events when the value changes for range inputs', () => { - let onChangeCalled = 0; - let onValueChangeCalled = 0; - const ref = React.createRef(); - - function onChange(e) { - onChangeCalled++; - expect(e.type).toBe('change'); - } - - function onValueChange(e) { - onValueChangeCalled++; - } - - function Component() { - const listener = useInput({ - onChange, - onValueChange, - }); - return ( - - ); - } - ReactDOM.render(, container); - - setUntrackedValue.call(ref.current, '40'); - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - - setUntrackedValue.call(ref.current, 'foo'); - ref.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - - expect(onChangeCalled).toBe(2); - expect(onValueChangeCalled).toBe(2); - }); - - // @gate experimental || build === "production" - 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 ref = React.createRef(); - const div = document.createElement('div'); - // Mount - ReactDOM.render( - } />, - div, - ); - // Update - ReactDOM.render( - } />, - div, - ); - // Change - ref.current.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true}), - ); - // Unmount - ReactDOM.unmountComponentAtNode(div); - } finally { - document.createElement = originalCreateElement; - } - }); - - describe('concurrent mode', () => { - // @gate experimental - // @gate experimental - it('text input', () => { - const root = ReactDOM.createRoot(container); - let input; - - function Component({innerRef, onChange, controlledValue}) { - const listener = useInput({ - onChange, - }); - return ( - - ); - } - - 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)} - controlledValue={controlledValue} - /> - ); - } - } - - // 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 - // @gate experimental - it('checkbox input', () => { - const root = ReactDOM.createRoot(container); - let input; - - function Component({innerRef, onChange, controlledValue}) { - const listener = useInput({ - onChange, - }); - return ( - - ); - } - - 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)} - /> - ); - } - } - - // 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 - // @gate experimental - it('textarea', () => { - const root = ReactDOM.createRoot(container); - let textarea; - - function Component({innerRef, onChange, controlledValue}) { - const listener = useInput({ - onChange, - }); - return ( -