diff --git a/packages/react-dom/src/client/ReactDOMComponentTree.js b/packages/react-dom/src/client/ReactDOMComponentTree.js index d0edca73baee..fbf0a81e2906 100644 --- a/packages/react-dom/src/client/ReactDOMComponentTree.js +++ b/packages/react-dom/src/client/ReactDOMComponentTree.js @@ -20,7 +20,6 @@ import type { SuspenseInstance, Props, } from './ReactDOMHostConfig'; -import type {DOMEventName} from '../events/DOMEventNames'; import { HostComponent, @@ -44,16 +43,6 @@ const internalEventHandlersKey = '__reactEvents$' + randomKey; const internalEventHandlerListenersKey = '__reactListeners$' + randomKey; const internalEventHandlesSetKey = '__reactHandles$' + randomKey; -export type ElementListenerMap = Map< - DOMEventName | string, - ElementListenerMapEntry | null, ->; - -export type ElementListenerMapEntry = { - passive: void | boolean, - listener: any => void, -}; - export function precacheFiberNode( hostInst: Fiber, node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance, @@ -207,12 +196,12 @@ export function updateFiberProps( (node: any)[internalPropsKey] = props; } -export function getEventListenerMap(node: EventTarget): ElementListenerMap { - let elementListenerMap = (node: any)[internalEventHandlersKey]; - if (elementListenerMap === undefined) { - elementListenerMap = (node: any)[internalEventHandlersKey] = new Map(); +export function getEventListenerSet(node: EventTarget): Set { + let elementListenerSet = (node: any)[internalEventHandlersKey]; + if (elementListenerSet === undefined) { + elementListenerSet = (node: any)[internalEventHandlersKey] = new Set(); } - return elementListenerMap; + return elementListenerSet; } export function getFiberFromScopeInstance( diff --git a/packages/react-dom/src/client/ReactDOMEventHandle.js b/packages/react-dom/src/client/ReactDOMEventHandle.js index 52edc8aaf7e2..ed227252e153 100644 --- a/packages/react-dom/src/client/ReactDOMEventHandle.js +++ b/packages/react-dom/src/client/ReactDOMEventHandle.js @@ -8,13 +8,12 @@ */ import type {DOMEventName} from '../events/DOMEventNames'; -import type {EventPriority, ReactScopeInstance} from 'shared/ReactTypes'; +import type {ReactScopeInstance} from 'shared/ReactTypes'; import type { ReactDOMEventHandle, ReactDOMEventHandleListener, } from '../shared/ReactDOMTypes'; -import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties'; import {allNativeEvents} from '../events/EventRegistry'; import { getClosestInstanceFromNode, @@ -25,10 +24,7 @@ import { addEventHandleToTarget, } from './ReactDOMComponentTree'; import {ELEMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType'; -import { - listenToNativeEvent, - addEventTypeToDispatchConfig, -} from '../events/DOMPluginEventSystem'; +import {listenToNativeEvent} from '../events/DOMPluginEventSystem'; import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags'; import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../events/EventSystemFlags'; @@ -42,8 +38,6 @@ import invariant from 'shared/invariant'; type EventHandleOptions = {| capture?: boolean, - passive?: boolean, - priority?: EventPriority, |}; function getNearestRootOrPortalContainer(node: Fiber): null | Element { @@ -82,76 +76,76 @@ function createEventHandleListener( function registerEventOnNearestTargetContainer( targetFiber: Fiber, domEventName: DOMEventName, - isPassiveListener: boolean | void, - listenerPriority: EventPriority | void, isCapturePhaseListener: boolean, targetElement: Element | null, ): void { - // If it is, find the nearest root or portal and make it - // our event handle target container. - let targetContainer = getNearestRootOrPortalContainer(targetFiber); - if (targetContainer === null) { - invariant( - false, - 'ReactDOM.createEventHandle: setListener called on an target ' + - 'that did not have a corresponding root. This is likely a bug in React.', + if (!enableEagerRootListeners) { + // If it is, find the nearest root or portal and make it + // our event handle target container. + let targetContainer = getNearestRootOrPortalContainer(targetFiber); + if (targetContainer === null) { + if (__DEV__) { + console.error( + 'ReactDOM.createEventHandle: setListener called on an target ' + + 'that did not have a corresponding root. This is likely a bug in React.', + ); + } + return; + } + if (targetContainer.nodeType === COMMENT_NODE) { + targetContainer = ((targetContainer.parentNode: any): Element); + } + listenToNativeEvent( + domEventName, + isCapturePhaseListener, + targetContainer, + targetElement, ); } - if (targetContainer.nodeType === COMMENT_NODE) { - targetContainer = ((targetContainer.parentNode: any): Element); - } - listenToNativeEvent( - domEventName, - isCapturePhaseListener, - targetContainer, - targetElement, - isPassiveListener, - listenerPriority, - ); } function registerReactDOMEvent( target: EventTarget | ReactScopeInstance, domEventName: DOMEventName, - isPassiveListener: boolean | void, isCapturePhaseListener: boolean, - listenerPriority: EventPriority | void, ): void { // Check if the target is a DOM element. if ((target: any).nodeType === ELEMENT_NODE) { - const targetElement = ((target: any): Element); - // Check if the DOM element is managed by React. - const targetFiber = getClosestInstanceFromNode(targetElement); - if (targetFiber === null) { - invariant( - false, - 'ReactDOM.createEventHandle: setListener called on an element ' + - 'target that is not managed by React. Ensure React rendered the DOM element.', + if (!enableEagerRootListeners) { + const targetElement = ((target: any): Element); + // Check if the DOM element is managed by React. + const targetFiber = getClosestInstanceFromNode(targetElement); + if (targetFiber === null) { + if (__DEV__) { + console.error( + 'ReactDOM.createEventHandle: setListener called on an element ' + + 'target that is not managed by React. Ensure React rendered the DOM element.', + ); + } + return; + } + registerEventOnNearestTargetContainer( + targetFiber, + domEventName, + isCapturePhaseListener, + targetElement, ); } - registerEventOnNearestTargetContainer( - targetFiber, - domEventName, - isPassiveListener, - listenerPriority, - isCapturePhaseListener, - targetElement, - ); } else if (enableScopeAPI && isReactScope(target)) { - const scopeTarget = ((target: any): ReactScopeInstance); - const targetFiber = getFiberFromScopeInstance(scopeTarget); - if (targetFiber === null) { - // Scope is unmounted, do not proceed. - return; + if (!enableEagerRootListeners) { + const scopeTarget = ((target: any): ReactScopeInstance); + const targetFiber = getFiberFromScopeInstance(scopeTarget); + if (targetFiber === null) { + // Scope is unmounted, do not proceed. + return; + } + registerEventOnNearestTargetContainer( + targetFiber, + domEventName, + isCapturePhaseListener, + null, + ); } - registerEventOnNearestTargetContainer( - targetFiber, - domEventName, - isPassiveListener, - listenerPriority, - isCapturePhaseListener, - null, - ); } else if (isValidEventTarget(target)) { const eventTarget = ((target: any): EventTarget); // These are valid event targets, but they are also @@ -161,8 +155,6 @@ function registerReactDOMEvent( isCapturePhaseListener, eventTarget, null, - isPassiveListener, - listenerPriority, IS_EVENT_HANDLE_NON_MANAGED_NODE, ); } else { @@ -181,46 +173,27 @@ export function createEventHandle( if (enableCreateEventHandleAPI) { const domEventName = ((type: any): DOMEventName); - if (enableEagerRootListeners) { - // We cannot support arbitrary native events with eager root listeners - // because the eager strategy relies on knowing the whole list ahead of time. - // If we wanted to support this, we'd have to add code to keep track - // (or search) for all portal and root containers, and lazily add listeners - // to them whenever we see a previously unknown event. This seems like a lot - // of complexity for something we don't even have a particular use case for. - // Unfortunately, the downside of this invariant is that *removing* a native - // event from the list of known events has now become a breaking change for - // any code relying on the createEventHandle API. - invariant( - allNativeEvents.has(domEventName) || - domEventName === 'beforeblur' || - domEventName === 'afterblur', - 'Cannot call unstable_createEventHandle with "%s", as it is not an event known to React.', - domEventName, - ); - } + // We cannot support arbitrary native events with eager root listeners + // because the eager strategy relies on knowing the whole list ahead of time. + // If we wanted to support this, we'd have to add code to keep track + // (or search) for all portal and root containers, and lazily add listeners + // to them whenever we see a previously unknown event. This seems like a lot + // of complexity for something we don't even have a particular use case for. + // Unfortunately, the downside of this invariant is that *removing* a native + // event from the list of known events has now become a breaking change for + // any code relying on the createEventHandle API. + invariant( + allNativeEvents.has(domEventName), + 'Cannot call unstable_createEventHandle with "%s", as it is not an event known to React.', + domEventName, + ); let isCapturePhaseListener = false; - let isPassiveListener = undefined; // Undefined means to use the browser default - let listenerPriority; - if (options != null) { const optionsCapture = options.capture; - const optionsPassive = options.passive; - const optionsPriority = options.priority; - if (typeof optionsCapture === 'boolean') { isCapturePhaseListener = optionsCapture; } - if (typeof optionsPassive === 'boolean') { - isPassiveListener = optionsPassive; - } - if (typeof optionsPriority === 'number') { - listenerPriority = optionsPriority; - } - } - if (listenerPriority === undefined) { - listenerPriority = getEventPriorityForListenerSystem(domEventName); } const eventHandle = ( @@ -234,15 +207,7 @@ export function createEventHandle( ); if (!doesTargetHaveEventHandle(target, eventHandle)) { addEventHandleToTarget(target, eventHandle); - registerReactDOMEvent( - target, - domEventName, - isPassiveListener, - isCapturePhaseListener, - listenerPriority, - ); - // Add the event to our known event types list. - addEventTypeToDispatchConfig(domEventName); + registerReactDOMEvent(target, domEventName, isCapturePhaseListener); } const listener = createEventHandleListener( domEventName, diff --git a/packages/react-dom/src/events/DOMEventProperties.js b/packages/react-dom/src/events/DOMEventProperties.js index 8c5090ecaee4..abdd3a256e34 100644 --- a/packages/react-dom/src/events/DOMEventProperties.js +++ b/packages/react-dom/src/events/DOMEventProperties.js @@ -89,6 +89,10 @@ const otherDiscreteEvents: Array = [ ]; if (enableCreateEventHandleAPI) { + // Special case: these two events don't have on* React handler + // and are only accessible via the createEventHandle API. + topLevelEventsToReactNames.set('beforeblur', null); + topLevelEventsToReactNames.set('afterblur', null); otherDiscreteEvents.push('beforeblur', 'afterblur'); } @@ -202,7 +206,7 @@ export function getEventPriorityForListenerSystem( if (__DEV__) { console.warn( 'The event "%s" provided to createEventHandle() does not have a known priority type.' + - ' It is recommended to provide a "priority" option to specify a priority.', + ' This is likely a bug in React.', type, ); } diff --git a/packages/react-dom/src/events/DOMPluginEventSystem.js b/packages/react-dom/src/events/DOMPluginEventSystem.js index 97a37ace2e33..4bbcb6daad3e 100644 --- a/packages/react-dom/src/events/DOMPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMPluginEventSystem.js @@ -19,8 +19,6 @@ import type { KnownReactSyntheticEvent, ReactSyntheticEvent, } from './ReactSyntheticEventType'; -import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree'; -import type {EventPriority} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import {registrationNameDependencies, allNativeEvents} from './EventRegistry'; @@ -41,7 +39,7 @@ import { import getEventTarget from './getEventTarget'; import { getClosestInstanceFromNode, - getEventListenerMap, + getEventListenerSet, getEventHandlerListeners, } from '../client/ReactDOMComponentTree'; import {COMMENT_NODE} from '../shared/HTMLNodeType'; @@ -69,7 +67,6 @@ import { addEventBubbleListenerWithPassiveFlag, addEventCaptureListenerWithPassiveFlag, } from './EventListener'; -import {topLevelEventsToReactNames} from './DOMEventProperties'; import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin'; import * as ChangeEventPlugin from './plugins/ChangeEventPlugin'; import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin'; @@ -296,36 +293,24 @@ function dispatchEventsForPlugins( processDispatchQueue(dispatchQueue, eventSystemFlags); } -function shouldUpgradeListener( - listenerEntry: void | ElementListenerMapEntry, - passive: void | boolean, -): boolean { - return ( - listenerEntry !== undefined && listenerEntry.passive === true && !passive - ); -} - export function listenToNonDelegatedEvent( domEventName: DOMEventName, targetElement: Element, ): void { const isCapturePhaseListener = false; - const listenerMap = getEventListenerMap(targetElement); - const listenerMapKey = getListenerMapKey( + const listenerSet = getEventListenerSet(targetElement); + const listenerSetKey = getListenerSetKey( domEventName, isCapturePhaseListener, ); - const listenerEntry = ((listenerMap.get( - listenerMapKey, - ): any): ElementListenerMapEntry | void); - if (listenerEntry === undefined) { - const listener = addTrappedEventListener( + if (!listenerSet.has(listenerSetKey)) { + addTrappedEventListener( targetElement, domEventName, IS_NON_DELEGATED, isCapturePhaseListener, ); - listenerMap.set(listenerMapKey, {passive: false, listener}); + listenerSet.add(listenerSetKey); } } @@ -369,8 +354,6 @@ export function listenToNativeEvent( isCapturePhaseListener: boolean, rootContainerElement: EventTarget, targetElement: Element | null, - isPassiveListener?: boolean, - listenerPriority?: EventPriority, eventSystemFlags?: EventSystemFlags = 0, ): void { let target = rootContainerElement; @@ -384,21 +367,6 @@ export function listenToNativeEvent( ) { target = (rootContainerElement: any).ownerDocument; } - if (enablePassiveEventIntervention && isPassiveListener === undefined) { - // Browsers introduced an intervention, making these events - // passive by default on document. React doesn't bind them - // to document anymore, but changing this now would undo - // the performance wins from the change. So we emulate - // the existing behavior manually on the roots now. - // https://github.com/facebook/react/issues/19651 - if ( - domEventName === 'touchstart' || - domEventName === 'touchmove' || - domEventName === 'wheel' - ) { - isPassiveListener = true; - } - } // If the event can be delegated (or is capture phase), we can // register it to the root container. Otherwise, we should // register the event to the target element and mark it as @@ -423,42 +391,24 @@ export function listenToNativeEvent( eventSystemFlags |= IS_NON_DELEGATED; target = targetElement; } - const listenerMap = getEventListenerMap(target); - const listenerMapKey = getListenerMapKey( + const listenerSet = getEventListenerSet(target); + const listenerSetKey = getListenerSetKey( domEventName, isCapturePhaseListener, ); - const listenerEntry = ((listenerMap.get( - listenerMapKey, - ): any): ElementListenerMapEntry | void); - const shouldUpgrade = shouldUpgradeListener(listenerEntry, isPassiveListener); - // If the listener entry is empty or we should upgrade, then // we need to trap an event listener onto the target. - if (listenerEntry === undefined || shouldUpgrade) { - // If we should upgrade, then we need to remove the existing trapped - // event listener for the target container. - if (shouldUpgrade) { - removeEventListener( - target, - domEventName, - ((listenerEntry: any): ElementListenerMapEntry).listener, - isCapturePhaseListener, - ); - } + if (!listenerSet.has(listenerSetKey)) { if (isCapturePhaseListener) { eventSystemFlags |= IS_CAPTURE_PHASE; } - const listener = addTrappedEventListener( + addTrappedEventListener( target, domEventName, eventSystemFlags, isCapturePhaseListener, - false, - isPassiveListener, - listenerPriority, ); - listenerMap.set(listenerMapKey, {passive: isPassiveListener, listener}); + listenerSet.add(listenerSetKey); } } @@ -480,11 +430,13 @@ export function listenToReactEvent( const isPolyfillEventPlugin = dependenciesLength !== 1; if (isPolyfillEventPlugin) { - const listenerMap = getEventListenerMap(rootContainerElement); - // For optimization, we register plugins on the listener map, so we - // don't need to check each of their dependencies each time. - if (!listenerMap.has(reactEvent)) { - listenerMap.set(reactEvent, null); + const listenerSet = getEventListenerSet(rootContainerElement); + // When eager listeners are off, this Set has a dual purpose: it both + // captures which native listeners we registered (e.g. "click__bubble") + // and *React* lazy listeners (e.g. "onClick") so we don't do extra checks. + // This second usage does not exist in the eager mode. + if (!listenerSet.has(reactEvent)) { + listenerSet.add(reactEvent); for (let i = 0; i < dependenciesLength; i++) { listenToNativeEvent( dependencies[i], @@ -520,19 +472,29 @@ function addTrappedEventListener( eventSystemFlags: EventSystemFlags, isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport?: boolean, - isPassiveListener?: boolean, - listenerPriority?: EventPriority, -): any => void { +) { let listener = createEventListenerWrapperWithPriority( targetContainer, domEventName, eventSystemFlags, - listenerPriority, ); // If passive option is not supported, then the event will be // active and not passive. - if (isPassiveListener === true && !passiveBrowserEventsSupported) { - isPassiveListener = false; + let isPassiveListener = undefined; + if (enablePassiveEventIntervention && passiveBrowserEventsSupported) { + // Browsers introduced an intervention, making these events + // passive by default on document. React doesn't bind them + // to document anymore, but changing this now would undo + // the performance wins from the change. So we emulate + // the existing behavior manually on the roots now. + // https://github.com/facebook/react/issues/19651 + if ( + domEventName === 'touchstart' || + domEventName === 'touchmove' || + domEventName === 'wheel' + ) { + isPassiveListener = true; + } } targetContainer = @@ -564,6 +526,7 @@ function addTrappedEventListener( return originalListener.apply(this, p); }; } + // TODO: There are too many combinations here. Consolidate them. if (isCapturePhaseListener) { if (isPassiveListener !== undefined) { unsubscribeListener = addEventCaptureListenerWithPassiveFlag( @@ -595,7 +558,6 @@ function addTrappedEventListener( ); } } - return unsubscribeListener; } function deferClickToDocumentForLegacyFBSupport( @@ -1085,19 +1047,7 @@ export function accumulateEventHandleNonManagedNodeListeners( } } -export function addEventTypeToDispatchConfig(type: DOMEventName): void { - const reactName = topLevelEventsToReactNames.get(type); - // If we don't have a reactName, then we're dealing with - // an event type that React does not know about (i.e. a custom event). - // We need to register an event config for this or the SimpleEventPlugin - // will not appropriately provide a SyntheticEvent, so we use out empty - // dispatch config for custom events. - if (reactName === undefined) { - topLevelEventsToReactNames.set(type, null); - } -} - -export function getListenerMapKey( +export function getListenerSetKey( domEventName: DOMEventName, capture: boolean, ): string { diff --git a/packages/react-dom/src/events/EventRegistry.js b/packages/react-dom/src/events/EventRegistry.js index dfe938d11305..eed8b9dc4984 100644 --- a/packages/react-dom/src/events/EventRegistry.js +++ b/packages/react-dom/src/events/EventRegistry.js @@ -9,10 +9,15 @@ import type {DOMEventName} from './DOMEventNames'; -import {enableEagerRootListeners} from 'shared/ReactFeatureFlags'; +import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags'; export const allNativeEvents: Set = new Set(); +if (enableCreateEventHandleAPI) { + allNativeEvents.add('beforeblur'); + allNativeEvents.add('afterblur'); +} + /** * Mapping from registration name to event name */ @@ -60,9 +65,7 @@ export function registerDirectEvent( } } - if (enableEagerRootListeners) { - for (let i = 0; i < dependencies.length; i++) { - allNativeEvents.add(dependencies[i]); - } + for (let i = 0; i < dependencies.length; i++) { + allNativeEvents.add(dependencies[i]); } } diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index d8ff5300aaf4..3aacb54873d2 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -8,7 +8,6 @@ */ import type {AnyNativeEvent} from '../events/PluginModuleType'; -import type {EventPriority} from 'shared/ReactTypes'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig'; import type {DOMEventName} from '../events/DOMEventNames'; @@ -96,12 +95,8 @@ export function createEventListenerWrapperWithPriority( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, - priority?: EventPriority, ): Function { - const eventPriority = - priority === undefined - ? getEventPriorityForPluginSystem(domEventName) - : priority; + const eventPriority = getEventPriorityForPluginSystem(domEventName); let listenerWrapper; switch (eventPriority) { case DiscreteEvent: diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js index 1a125dc26062..6e4449961e60 100644 --- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js @@ -1949,106 +1949,6 @@ describe('DOMPluginEventSystem', () => { expect(log).toEqual([{counter: 1}]); }); - // @gate experimental - it('should correctly work for a basic "click" listener that upgrades', () => { - const clickEvent = jest.fn(); - const buttonRef = React.createRef(); - const button2Ref = React.createRef(); - const setClick1 = ReactDOM.unstable_createEventHandle('click', { - passive: false, - }); - const setClick2 = ReactDOM.unstable_createEventHandle('click', { - passive: true, - }); - - function Test2() { - React.useEffect(() => { - return setClick1(button2Ref.current, clickEvent); - }); - - return ; - } - - function Test({extra}) { - React.useEffect(() => { - return setClick2(buttonRef.current, clickEvent); - }); - - return ( - <> - - {extra && } - - ); - } - - ReactDOM.render(, container); - Scheduler.unstable_flushAll(); - - let button = buttonRef.current; - dispatchClickEvent(button); - expect(clickEvent).toHaveBeenCalledTimes(1); - - ReactDOM.render(, container); - Scheduler.unstable_flushAll(); - - clickEvent.mockClear(); - - button = button2Ref.current; - dispatchClickEvent(button); - expect(clickEvent).toHaveBeenCalledTimes(1); - }); - - // @gate experimental - it('should correctly work for a basic "click" listener that upgrades #2', () => { - const clickEvent = jest.fn(); - const buttonRef = React.createRef(); - const button2Ref = React.createRef(); - const setClick1 = ReactDOM.unstable_createEventHandle('click', { - passive: false, - }); - const setClick2 = ReactDOM.unstable_createEventHandle('click', { - passive: undefined, - }); - - function Test2() { - React.useEffect(() => { - return setClick1(button2Ref.current, clickEvent); - }); - - return ; - } - - function Test({extra}) { - React.useEffect(() => { - return setClick2(buttonRef.current, clickEvent); - }); - - return ( - <> - - {extra && } - - ); - } - - ReactDOM.render(, container); - Scheduler.unstable_flushAll(); - - let button = buttonRef.current; - dispatchClickEvent(button); - expect(clickEvent).toHaveBeenCalledTimes(1); - - ReactDOM.render(, container); - Scheduler.unstable_flushAll(); - - clickEvent.mockClear(); - - button = button2Ref.current; - dispatchClickEvent(button); - expect(clickEvent).toHaveBeenCalledTimes(1); - }); - // @gate experimental it('should correctly work for a basic "click" window listener', () => { const log = []; @@ -2391,109 +2291,15 @@ describe('DOMPluginEventSystem', () => { }); // @gate experimental - it('handles propagation of custom user events', () => { - const buttonRef = React.createRef(); - const divRef = React.createRef(); - const log = []; - const onCustomEvent = jest.fn(e => - log.push(['bubble', e.currentTarget]), + it('does not support custom user events', () => { + // With eager listeners, supporting custom events via this API doesn't make sense + // because we can't know a full list of them ahead of time. Let's check we throw + // since otherwise we'd end up with inconsistent behavior, like no portal bubbling. + expect(() => { + ReactDOM.unstable_createEventHandle('custom-event'); + }).toThrow( + 'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.', ); - const onCustomEventCapture = jest.fn(e => - log.push(['capture', e.currentTarget]), - ); - - let setCustomEventHandle; - if (gate(flags => flags.enableEagerRootListeners)) { - // With eager listeners, supporting custom events via this API doesn't make sense - // because we can't know a full list of them ahead of time. Let's check we throw - // since otherwise we'd end up with inconsistent behavior, like no portal bubbling. - expect(() => { - setCustomEventHandle = ReactDOM.unstable_createEventHandle( - 'custom-event', - ); - }).toThrow( - 'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.', - ); - } else { - // Test that we get a warning when we don't provide an explicit priority - expect(() => { - setCustomEventHandle = ReactDOM.unstable_createEventHandle( - 'custom-event', - ); - }).toWarnDev( - 'Warning: The event "custom-event" provided to createEventHandle() does not have a known priority type. ' + - 'It is recommended to provide a "priority" option to specify a priority.', - {withoutStack: true}, - ); - - setCustomEventHandle = ReactDOM.unstable_createEventHandle( - 'custom-event', - { - priority: 0, // Discrete - }, - ); - - const setCustomCaptureHandle = ReactDOM.unstable_createEventHandle( - 'custom-event', - { - capture: true, - priority: 0, // Discrete - }, - ); - - const Test = () => { - React.useEffect(() => { - const clearCustom1 = setCustomEventHandle( - buttonRef.current, - onCustomEvent, - ); - const clearCustom2 = setCustomCaptureHandle( - buttonRef.current, - onCustomEventCapture, - ); - const clearCustom3 = setCustomEventHandle( - divRef.current, - onCustomEvent, - ); - const clearCustom4 = setCustomCaptureHandle( - divRef.current, - onCustomEventCapture, - ); - - return () => { - clearCustom1(); - clearCustom2(); - clearCustom3(); - clearCustom4(); - }; - }); - - return ( - - ); - }; - - ReactDOM.render(, container); - Scheduler.unstable_flushAll(); - - const buttonElement = buttonRef.current; - dispatchEvent(buttonElement, 'custom-event'); - expect(onCustomEvent).toHaveBeenCalledTimes(1); - expect(onCustomEventCapture).toHaveBeenCalledTimes(1); - expect(log[0]).toEqual(['capture', buttonElement]); - expect(log[1]).toEqual(['bubble', buttonElement]); - - const divElement = divRef.current; - dispatchEvent(divElement, 'custom-event'); - expect(onCustomEvent).toHaveBeenCalledTimes(3); - expect(onCustomEventCapture).toHaveBeenCalledTimes(3); - expect(log[2]).toEqual(['capture', buttonElement]); - expect(log[3]).toEqual(['capture', divElement]); - expect(log[4]).toEqual(['bubble', divElement]); - expect(log[5]).toEqual(['bubble', buttonElement]); - } }); // @gate experimental @@ -3211,12 +3017,14 @@ describe('DOMPluginEventSystem', () => { }); // @gate experimental - it('should be able to register non-passive handlers for events affected by the intervention', () => { + it('should be able to register handlers for events affected by the intervention', () => { const rootContainer = document.createElement('div'); container.appendChild(rootContainer); + const allEvents = []; const defaultPreventedEvents = []; const handler = e => { + allEvents.push(e.type); if (e.defaultPrevented) defaultPreventedEvents.push(e.type); }; @@ -3227,15 +3035,11 @@ describe('DOMPluginEventSystem', () => { const ref = React.createRef(); const setTouchStart = ReactDOM.unstable_createEventHandle( 'touchstart', - {passive: false}, ); const setTouchMove = ReactDOM.unstable_createEventHandle( 'touchmove', - {passive: false}, ); - const setWheel = ReactDOM.unstable_createEventHandle('wheel', { - passive: false, - }); + const setWheel = ReactDOM.unstable_createEventHandle('wheel'); function Component() { React.useEffect(() => { @@ -3264,11 +3068,17 @@ describe('DOMPluginEventSystem', () => { dispatchEvent(ref.current, 'touchmove'); dispatchEvent(ref.current, 'wheel'); - expect(defaultPreventedEvents).toEqual([ - 'touchstart', - 'touchmove', - 'wheel', - ]); + expect(allEvents).toEqual(['touchstart', 'touchmove', 'wheel']); + // These events are passive by default, so we can't preventDefault. + if (gate(flags => flags.enablePassiveEventIntervention)) { + expect(defaultPreventedEvents).toEqual([]); + } else { + expect(defaultPreventedEvents).toEqual([ + 'touchstart', + 'touchmove', + 'wheel', + ]); + } }); }); }); diff --git a/packages/react-dom/src/events/plugins/SelectEventPlugin.js b/packages/react-dom/src/events/plugins/SelectEventPlugin.js index 7f8b72408f99..ff2c1a9d21f3 100644 --- a/packages/react-dom/src/events/plugins/SelectEventPlugin.js +++ b/packages/react-dom/src/events/plugins/SelectEventPlugin.js @@ -22,7 +22,7 @@ import {registerTwoPhaseEvent} from '../EventRegistry'; import getActiveElement from '../../client/getActiveElement'; import { getNodeFromInstance, - getEventListenerMap, + getEventListenerSet, } from '../../client/ReactDOMComponentTree'; import {hasSelectionCapabilities} from '../../client/ReactInputSelection'; import {DOCUMENT_NODE} from '../../shared/HTMLNodeType'; @@ -154,7 +154,7 @@ function extractEvents( targetContainer: EventTarget, ) { if (!enableEagerRootListeners) { - const eventListenerMap = getEventListenerMap(targetContainer); + const eventListenerSet = getEventListenerSet(targetContainer); // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. if ( @@ -163,8 +163,8 @@ function extractEvents( // event attached from the onChange plugin and we don't expose an // onSelectionChange event from React. domEventName !== 'selectionchange' && - !eventListenerMap.has('onSelect') && - !eventListenerMap.has('onSelectCapture') + !eventListenerSet.has('onSelect') && + !eventListenerSet.has('onSelectCapture') ) { return; } diff --git a/packages/react-interactions/events/src/dom/create-event-handle/Focus.js b/packages/react-interactions/events/src/dom/create-event-handle/Focus.js index 53e04c1ca846..214a25d0d30f 100644 --- a/packages/react-interactions/events/src/dom/create-event-handle/Focus.js +++ b/packages/react-interactions/events/src/dom/create-event-handle/Focus.js @@ -37,32 +37,6 @@ const isMac = ? /^Mac/.test(window.navigator.platform) : false; -const canUseDOM: boolean = !!( - typeof window !== 'undefined' && - typeof window.document !== 'undefined' && - typeof window.document.createElement !== 'undefined' -); - -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 (canUseDOM) { - try { - const options = {}; - // $FlowFixMe: Ignore Flow complaining about needing a value - Object.defineProperty(options, 'passive', { - get: function() { - passiveBrowserEventsSupported = true; - }, - }); - window.addEventListener('test', options, options); - window.removeEventListener('test', options, options); - } catch (e) { - passiveBrowserEventsSupported = false; - } -} - const hasPointerEvents = typeof window !== 'undefined' && window.PointerEvent != null; @@ -78,20 +52,13 @@ const globalFocusVisibleEvents = hasPointerEvents 'touchend', ]; -const passiveObject = {passive: true}; -const passiveObjectWithPriority = {passive: true, priority: 0}; - // Global state for tracking focus visible and emulation of mouse let isGlobalFocusVisible = true; let hasTrackedGlobalFocusVisible = false; function trackGlobalFocusVisible() { globalFocusVisibleEvents.forEach(type => { - window.addEventListener( - type, - handleGlobalFocusVisibleEvent, - passiveBrowserEventsSupported ? {capture: true, passive: true} : true, - ); + window.addEventListener(type, handleGlobalFocusVisibleEvent, true); }); } @@ -171,9 +138,9 @@ function setFocusVisibleListeners( function useFocusVisibleInputHandles() { return [ - useEvent('mousedown', passiveObject), - useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart', passiveObject), - useEvent('keydown', passiveObject), + useEvent('mousedown'), + useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart'), + useEvent('keydown'), ]; } @@ -200,8 +167,8 @@ export function useFocus( const stateRef = useRef( {isFocused: false, isFocusVisible: false}, ); - const focusHandle = useEvent('focusin', passiveObjectWithPriority); - const blurHandle = useEvent('focusout', passiveObjectWithPriority); + const focusHandle = useEvent('focusin'); + const blurHandle = useEvent('focusout'); const focusVisibleHandles = useFocusVisibleInputHandles(); useLayoutEffect(() => { @@ -297,10 +264,10 @@ export function useFocusWithin( const stateRef = useRef( {isFocused: false, isFocusVisible: false}, ); - const focusHandle = useEvent('focusin', passiveObjectWithPriority); - const blurHandle = useEvent('focusout', passiveObjectWithPriority); - const afterBlurHandle = useEvent('afterblur', passiveObject); - const beforeBlurHandle = useEvent('beforeblur', passiveObject); + const focusHandle = useEvent('focusin'); + const blurHandle = useEvent('focusout'); + const afterBlurHandle = useEvent('afterblur'); + const beforeBlurHandle = useEvent('beforeblur'); const focusVisibleHandles = useFocusVisibleInputHandles(); const useFocusWithinRef = useCallback( diff --git a/packages/react-interactions/events/src/dom/create-event-handle/useEvent.js b/packages/react-interactions/events/src/dom/create-event-handle/useEvent.js index e99a83fdd9df..5ef712e08212 100644 --- a/packages/react-interactions/events/src/dom/create-event-handle/useEvent.js +++ b/packages/react-interactions/events/src/dom/create-event-handle/useEvent.js @@ -25,8 +25,6 @@ export default function useEvent( event: string, options?: {| capture?: boolean, - passive?: boolean, - priority?: 0 | 1 | 2, |}, ): UseEventHandle { const handleRef = useRef(null);