diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 382581f81537..06ff7e52f899 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -7,6 +7,8 @@ * @flow */ +import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree'; + import { registrationNameDependencies, possibleRegistrationNames, @@ -82,7 +84,7 @@ import { enableDeprecatedFlareAPI, enableTrustedTypesIntegration, } from 'shared/ReactFeatureFlags'; -import {listenToEvent} from '../events/DOMModernPluginEventSystem'; +import {listenToReactPropEvent} from '../events/DOMModernPluginEventSystem'; import {getEventListenerMap} from './ReactDOMComponentTree'; let didWarnInvalidHydration = false; @@ -262,7 +264,7 @@ if (__DEV__) { export function ensureListeningTo( rootContainerInstance: Element | Node, - registrationName: string, + reactPropEvent: string, ): void { // If we have a comment node, then use the parent node, // which should be an element. @@ -279,7 +281,10 @@ export function ensureListeningTo( 'ensureListeningTo(): received a container that was not an element node. ' + 'This is likely a bug in React.', ); - listenToEvent(registrationName, ((rootContainerElement: any): Element)); + listenToReactPropEvent( + reactPropEvent, + ((rootContainerElement: any): Element), + ); } function getOwnerDocumentFromRootContainer( @@ -1267,7 +1272,9 @@ export function listenToEventResponderEventTypes( // existing passive event listener before we add the // active event listener. const passiveKey = targetEventType + '_passive'; - const passiveItem = listenerMap.get(passiveKey); + const passiveItem = ((listenerMap.get( + passiveKey, + ): any): ElementListenerMapEntry | void); if (passiveItem !== undefined) { removeTrappedEventListener( document, diff --git a/packages/react-dom/src/client/ReactDOMComponentTree.js b/packages/react-dom/src/client/ReactDOMComponentTree.js index 5381083e4c0c..606050f8f3b7 100644 --- a/packages/react-dom/src/client/ReactDOMComponentTree.js +++ b/packages/react-dom/src/client/ReactDOMComponentTree.js @@ -42,7 +42,7 @@ const internalEventHandlerListenersKey = '__reactListeners$' + randomKey; export type ElementListenerMap = Map< DOMTopLevelEventType | string, - ElementListenerMapEntry, + ElementListenerMapEntry | null, >; export type ElementListenerMapEntry = { diff --git a/packages/react-dom/src/client/ReactDOMEventHandle.js b/packages/react-dom/src/client/ReactDOMEventHandle.js index ae9b40d130a5..64c76718e0a3 100644 --- a/packages/react-dom/src/client/ReactDOMEventHandle.js +++ b/packages/react-dom/src/client/ReactDOMEventHandle.js @@ -26,6 +26,7 @@ import {ELEMENT_NODE} from '../shared/HTMLNodeType'; import { listenToTopLevelEvent, addEventTypeToDispatchConfig, + capturePhaseEvents, } from '../events/DOMModernPluginEventSystem'; import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags'; @@ -98,11 +99,13 @@ function registerEventOnNearestTargetContainer( ); } const listenerMap = getEventListenerMap(targetContainer); + const capture = capturePhaseEvents.has(topLevelType); listenToTopLevelEvent( topLevelType, targetContainer, listenerMap, PLUGIN_EVENT_SYSTEM, + capture, passive, priority, ); @@ -201,9 +204,9 @@ export function createEventHandle( eventTarget, listenerMap, PLUGIN_EVENT_SYSTEM | IS_TARGET_PHASE_ONLY, + capture, passive, priority, - capture, ); } else { invariant( diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index f064b0f9c215..f794a3ded865 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -81,7 +81,7 @@ import { import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags'; import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes'; import { - listenToEvent, + listenToReactPropEvent, clearEventHandleListenersForTarget, } from '../events/DOMModernPluginEventSystem'; @@ -1122,7 +1122,7 @@ export function makeOpaqueHydratingObject( } export function preparePortalMount(portalInstance: Instance): void { - listenToEvent('onMouseEnter', portalInstance); + listenToReactPropEvent('onMouseEnter', portalInstance); } export function prepareScopeUpdate( diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js index 7ce99083b21b..f1d95fe5a292 100644 --- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -30,6 +30,7 @@ import { LEGACY_FB_SUPPORT, IS_REPLAYED, IS_TARGET_PHASE_ONLY, + IS_CAPTURE_PHASE, } from './EventSystemFlags'; import { @@ -301,9 +302,9 @@ export function listenToTopLevelEvent( target: EventTarget, listenerMap: ElementListenerMap, eventSystemFlags: EventSystemFlags, + capture: boolean, passive?: boolean, priority?: EventPriority, - capture?: boolean, ): void { // TOP_SELECTION_CHANGE needs to be attached to the document // otherwise it won't capture incoming events that are only @@ -312,12 +313,10 @@ export function listenToTopLevelEvent( target = (target: any).ownerDocument || target; listenerMap = getEventListenerMap(target); } - capture = - capture === undefined ? capturePhaseEvents.has(topLevelType) : capture; const listenerMapKey = getListenerMapKey(topLevelType, capture); - const listenerEntry: ElementListenerMapEntry | void = listenerMap.get( + const listenerEntry = ((listenerMap.get( listenerMapKey, - ); + ): any): ElementListenerMapEntry | void); const shouldUpgrade = shouldUpgradeListener(listenerEntry, passive); // If the listener entry is empty or we should upgrade, then @@ -333,6 +332,9 @@ export function listenToTopLevelEvent( ((listenerEntry: any): ElementListenerMapEntry).listener, ); } + if (capture) { + eventSystemFlags |= IS_CAPTURE_PHASE; + } const listener = addTrappedEventListener( target, topLevelType, @@ -346,20 +348,31 @@ export function listenToTopLevelEvent( } } -export function listenToEvent( - registrationName: string, +export function listenToReactPropEvent( + reactPropEvent: string, rootContainerElement: Element, ): void { const listenerMap = getEventListenerMap(rootContainerElement); - const dependencies = registrationNameDependencies[registrationName]; + // For optimization, let's check if we have the registration name + // on the rootContainerElement. + if (listenerMap.has(reactPropEvent)) { + return; + } + // Add the registration name to the map, so we can avoid processing + // this React prop event again. + listenerMap.set(reactPropEvent, null); + const dependencies = registrationNameDependencies[reactPropEvent]; for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; + const capture = capturePhaseEvents.has(dependency); + listenToTopLevelEvent( dependency, rootContainerElement, listenerMap, PLUGIN_EVENT_SYSTEM, + capture, ); } } @@ -892,10 +905,11 @@ export function accumulateEnterLeaveListeners( } } -export function accumulateEventTargetListeners( +export function accumulateEventHandleTargetListeners( dispatchQueue: DispatchQueue, event: ReactSyntheticEvent, currentTarget: EventTarget, + inCapturePhase: boolean, ): void { const capturePhase: DispatchQueueItemPhase = []; const bubblePhase: DispatchQueueItemPhase = []; @@ -904,17 +918,16 @@ export function accumulateEventTargetListeners( if (eventListeners !== null) { const listenersArr = Array.from(eventListeners); const targetType = ((event.type: any): DOMTopLevelEventType); - const isCapturePhase = (event: any).eventPhase === 1; for (let i = 0; i < listenersArr.length; i++) { const listener = listenersArr[i]; const {callback, capture, type} = listener; if (type === targetType) { - if (isCapturePhase && capture) { + if (inCapturePhase && capture) { capturePhase.push( createDispatchQueueItemPhaseEntry(null, callback, currentTarget), ); - } else if (!isCapturePhase && !capture) { + } else if (!inCapturePhase && !capture) { bubblePhase.push( createDispatchQueueItemPhaseEntry(null, callback, currentTarget), ); diff --git a/packages/react-dom/src/events/EventSystemFlags.js b/packages/react-dom/src/events/EventSystemFlags.js index 69e39bf5fc92..35af32c3b2ce 100644 --- a/packages/react-dom/src/events/EventSystemFlags.js +++ b/packages/react-dom/src/events/EventSystemFlags.js @@ -12,8 +12,9 @@ export type EventSystemFlags = number; export const PLUGIN_EVENT_SYSTEM = 1; export const RESPONDER_EVENT_SYSTEM = 1 << 1; export const IS_TARGET_PHASE_ONLY = 1 << 2; -export const IS_PASSIVE = 1 << 3; -export const PASSIVE_NOT_SUPPORTED = 1 << 4; -export const IS_REPLAYED = 1 << 5; -export const IS_FIRST_ANCESTOR = 1 << 6; -export const LEGACY_FB_SUPPORT = 1 << 7; +export const IS_CAPTURE_PHASE = 1 << 3; +export const IS_PASSIVE = 1 << 4; +export const PASSIVE_NOT_SUPPORTED = 1 << 5; +export const IS_REPLAYED = 1 << 6; +export const IS_FIRST_ANCESTOR = 1 << 7; +export const LEGACY_FB_SUPPORT = 1 << 8; diff --git a/packages/react-dom/src/events/ReactDOMEventReplaying.js b/packages/react-dom/src/events/ReactDOMEventReplaying.js index d69d9684a76f..d14c7587f257 100644 --- a/packages/react-dom/src/events/ReactDOMEventReplaying.js +++ b/packages/react-dom/src/events/ReactDOMEventReplaying.js @@ -220,6 +220,7 @@ function trapReplayableEventForContainer( ((container: any): Element), listenerMap, PLUGIN_EVENT_SYSTEM, + false, ); } diff --git a/packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js b/packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js index fabf95598826..dc6cacce3779 100644 --- a/packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js +++ b/packages/react-dom/src/events/plugins/ModernSelectEventPlugin.js @@ -29,11 +29,7 @@ import { } from '../../client/ReactDOMComponentTree'; import {hasSelectionCapabilities} from '../../client/ReactInputSelection'; import {DOCUMENT_NODE} from '../../shared/HTMLNodeType'; -import { - accumulateTwoPhaseListeners, - getListenerMapKey, - capturePhaseEvents, -} from '../DOMModernPluginEventSystem'; +import {accumulateTwoPhaseListeners} from '../DOMModernPluginEventSystem'; const skipSelectionChangeEvent = canUseDOM && 'documentMode' in document && document.documentMode <= 11; @@ -148,32 +144,6 @@ function constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget) { } } -function isListeningToEvents( - events: Array, - mountAt: Document | Element, -): boolean { - const listenerMap = getEventListenerMap(mountAt); - for (let i = 0; i < events.length; i++) { - const event = events[i]; - const capture = capturePhaseEvents.has(event); - const listenerMapKey = getListenerMapKey(event, capture); - if (!listenerMap.has(listenerMapKey)) { - return false; - } - } - return true; -} - -function isListeningToEvent( - registrationName: string, - mountAt: Document | Element, -): boolean { - const listenerMap = getEventListenerMap(mountAt); - const capture = capturePhaseEvents.has(registrationName); - const listenerMapKey = getListenerMapKey(registrationName, capture); - return listenerMap.has(listenerMapKey); -} - /** * This plugin creates an `onSelect` event that normalizes select events * across form elements. @@ -197,19 +167,17 @@ function extractEvents( eventSystemFlags, targetContainer, ) { - const doc = getEventTargetDocument(nativeEventTarget); + const eventListenerMap = getEventListenerMap(targetContainer); // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. if ( - // We only listen to TOP_SELECTION_CHANGE on the document, never the - // root. - !isListeningToEvent(TOP_SELECTION_CHANGE, doc) || // If we are handling TOP_SELECTION_CHANGE, then we don't need to // check for the other dependencies, as TOP_SELECTION_CHANGE is only // event attached from the onChange plugin and we don't expose an // onSelectionChange event from React. - (topLevelType !== TOP_SELECTION_CHANGE && - !isListeningToEvents(rootTargetDependencies, targetContainer)) + topLevelType !== TOP_SELECTION_CHANGE && + !eventListenerMap.has('onSelect') && + !eventListenerMap.has('onSelectCapture') ) { return; } diff --git a/packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js b/packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js index d8021f825f4b..be6731082c10 100644 --- a/packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js +++ b/packages/react-dom/src/events/plugins/ModernSimpleEventPlugin.js @@ -24,10 +24,9 @@ import { } from '../DOMEventProperties'; import { accumulateTwoPhaseListeners, - accumulateEventTargetListeners, + accumulateEventHandleTargetListeners, } from '../DOMModernPluginEventSystem'; import {IS_TARGET_PHASE_ONLY} from '../EventSystemFlags'; - import SyntheticAnimationEvent from '../SyntheticAnimationEvent'; import SyntheticClipboardEvent from '../SyntheticClipboardEvent'; import SyntheticFocusEvent from '../SyntheticFocusEvent'; @@ -40,6 +39,7 @@ import SyntheticTransitionEvent from '../SyntheticTransitionEvent'; import SyntheticUIEvent from '../SyntheticUIEvent'; import SyntheticWheelEvent from '../SyntheticWheelEvent'; import getEventCharCode from '../getEventCharCode'; +import {IS_CAPTURE_PHASE} from '../EventSystemFlags'; import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags'; @@ -158,7 +158,13 @@ function extractEvents( eventSystemFlags & IS_TARGET_PHASE_ONLY && targetContainer != null ) { - accumulateEventTargetListeners(dispatchQueue, event, targetContainer); + const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0; + accumulateEventHandleTargetListeners( + dispatchQueue, + event, + targetContainer, + inCapturePhase, + ); } else { accumulateTwoPhaseListeners(targetInst, dispatchQueue, event, true); } 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 a98404154c39..fa9fcee0fb09 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 @@ -126,6 +126,7 @@ function handleGlobalFocusVisibleEvent( } const passiveObject = {passive: true}; +const passiveCaptureObject = {capture: true, passive: false}; function handleFocusVisibleTargetEvent( type: string, @@ -242,8 +243,8 @@ export function useFocus( ): void { // Setup controlled state for this useFocus hook const stateRef = useRef({isFocused: false, isFocusVisible: false}); - const focusHandle = useEvent('focus', passiveObject); - const blurHandle = useEvent('blur', passiveObject); + const focusHandle = useEvent('focus', passiveCaptureObject); + const blurHandle = useEvent('blur', passiveCaptureObject); const focusVisibleHandles = useFocusVisibleInputHandles(); useEffect(() => { @@ -329,8 +330,8 @@ export function useFocusWithin( ) { // Setup controlled state for this useFocus hook const stateRef = useRef({isFocused: false, isFocusVisible: false}); - const focusHandle = useEvent('focus', passiveObject); - const blurHandle = useEvent('blur', passiveObject); + const focusHandle = useEvent('focus', passiveCaptureObject); + const blurHandle = useEvent('blur', passiveCaptureObject); const afterBlurHandle = useEvent('afterblur', passiveObject); const beforeBlurHandle = useEvent('beforeblur', passiveObject); const focusVisibleHandles = useFocusVisibleInputHandles();