diff --git a/packages/events/EventSystemFlags.js b/packages/events/EventSystemFlags.js index be5e3544a2ea4..2d64878ce74b6 100644 --- a/packages/events/EventSystemFlags.js +++ b/packages/events/EventSystemFlags.js @@ -13,4 +13,5 @@ export const PLUGIN_EVENT_SYSTEM = 1; export const RESPONDER_EVENT_SYSTEM = 1 << 1; export const IS_PASSIVE = 1 << 2; export const IS_ACTIVE = 1 << 3; -export const PASSIVE_NOT_SUPPORTED = 1 << 4; +export const IS_CAPTURE = 1 << 4; +export const PASSIVE_NOT_SUPPORTED = 1 << 5; diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index 962648161e95e..00424efe2c97e 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -15,7 +15,10 @@ import {canUseDOM} from 'shared/ExecutionEnvironment'; import warningWithoutStack from 'shared/warningWithoutStack'; import type {ReactEventResponderEventType} from 'shared/ReactTypes'; import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes'; -import {setListenToResponderEventTypes} from '../events/DOMEventResponderSystem'; +import { + setListenToResponderEventTypes, + generateListeningKey, +} from '../events/DOMEventResponderSystem'; import { getValueForAttribute, @@ -1320,12 +1323,11 @@ export function listenToEventResponderEventTypes( capture = targetEventConfigObject.capture; } } - // Create a unique name for this event, plus its properties. We'll - // use this to ensure we don't listen to the same event with the same - // properties again. - const passiveKey = passive ? '_passive' : '_active'; - const captureKey = capture ? '_capture' : ''; - const listeningName = `${topLevelType}${passiveKey}${captureKey}`; + const listeningName = generateListeningKey( + topLevelType, + passive, + capture, + ); if (!listeningSet.has(listeningName)) { trapEventForResponderEventSystem( element, diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 40c4c703d32f3..23133a9ba6bdf 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -9,6 +9,7 @@ import { type EventSystemFlags, IS_PASSIVE, + IS_CAPTURE, PASSIVE_NOT_SUPPORTED, } from 'events/EventSystemFlags'; import type {AnyNativeEvent} from 'events/PluginModuleType'; @@ -73,7 +74,7 @@ const rootEventTypesToEventComponentInstances: Map< > = new Map(); const targetEventTypeCached: Map< Array, - Set, + Set, > = new Map(); const ownershipChangeListeners: Set = new Set(); const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; @@ -235,32 +236,8 @@ const eventResponderContext: ReactResponderContext = { listenToResponderEventTypesImpl(rootEventTypes, activeDocument); for (let i = 0; i < rootEventTypes.length; i++) { const rootEventType = rootEventTypes[i]; - const topLevelEventType = - typeof rootEventType === 'string' ? rootEventType : rootEventType.name; - let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( - topLevelEventType, - ); - if (rootEventComponentInstances === undefined) { - rootEventComponentInstances = new Set(); - rootEventTypesToEventComponentInstances.set( - topLevelEventType, - rootEventComponentInstances, - ); - } - const componentInstance = ((currentInstance: any): ReactEventComponentInstance); - let rootEventTypesSet = componentInstance.rootEventTypes; - if (rootEventTypesSet === null) { - rootEventTypesSet = componentInstance.rootEventTypes = new Set(); - } - invariant( - !rootEventTypesSet.has(topLevelEventType), - '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(topLevelEventType); - rootEventComponentInstances.add(componentInstance); + const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance); + registerRootEventType(rootEventType, eventComponentInstance); } }, removeRootEventTypes( @@ -269,15 +246,37 @@ const eventResponderContext: ReactResponderContext = { validateResponderContext(); for (let i = 0; i < rootEventTypes.length; i++) { const rootEventType = rootEventTypes[i]; - const topLevelEventType = - typeof rootEventType === 'string' ? rootEventType : rootEventType.name; + let name = rootEventType; + let capture = false; + let passive = true; + + if (typeof rootEventType !== 'string') { + const targetEventConfigObject = ((rootEventType: any): { + name: string, + passive?: boolean, + capture?: boolean, + }); + name = targetEventConfigObject.name; + if (targetEventConfigObject.passive !== undefined) { + passive = targetEventConfigObject.passive; + } + if (targetEventConfigObject.capture !== undefined) { + capture = targetEventConfigObject.capture; + } + } + + const listeningName = generateListeningKey( + ((name: any): string), + passive, + capture, + ); let rootEventComponents = rootEventTypesToEventComponentInstances.get( - topLevelEventType, + listeningName, ); let rootEventTypesSet = ((currentInstance: any): ReactEventComponentInstance) .rootEventTypes; if (rootEventTypesSet !== null) { - rootEventTypesSet.delete(topLevelEventType); + rootEventTypesSet.delete(listeningName); } if (rootEventComponents !== undefined) { rootEventComponents.delete( @@ -476,14 +475,15 @@ function createResponderEvent( topLevelType: string, nativeEvent: AnyNativeEvent, nativeEventTarget: Element | Document, - eventSystemFlags: EventSystemFlags, + passive: boolean, + passiveSupported: boolean, ): ReactResponderEvent { const responderEvent = { nativeEvent: nativeEvent, target: nativeEventTarget, type: topLevelType, - passive: (eventSystemFlags & IS_PASSIVE) !== 0, - passiveSupported: (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0, + passive, + passiveSupported, }; if (__DEV__) { Object.freeze(responderEvent); @@ -529,16 +529,37 @@ export function processEventQueue(): void { function getTargetEventTypesSet( eventTypes: Array, -): Set { +): Set { let cachedSet = targetEventTypeCached.get(eventTypes); if (cachedSet === undefined) { cachedSet = new Set(); for (let i = 0; i < eventTypes.length; i++) { const eventType = eventTypes[i]; - const topLevelEventType = - typeof eventType === 'string' ? eventType : eventType.name; - cachedSet.add(((topLevelEventType: any): DOMTopLevelEventType)); + let name = eventType; + let capture = false; + let passive = true; + + if (typeof eventType !== 'string') { + const targetEventConfigObject = ((eventType: any): { + name: string, + passive?: boolean, + capture?: boolean, + }); + name = targetEventConfigObject.name; + if (targetEventConfigObject.passive !== undefined) { + passive = targetEventConfigObject.passive; + } + if (targetEventConfigObject.capture !== undefined) { + capture = targetEventConfigObject.capture; + } + } + const listeningName = generateListeningKey( + ((name: any): string), + passive, + capture, + ); + cachedSet.add(listeningName); } targetEventTypeCached.set(eventTypes, cachedSet); } @@ -546,7 +567,7 @@ function getTargetEventTypesSet( } function getTargetEventResponderInstances( - topLevelType: DOMTopLevelEventType, + listeningName: string, targetFiber: null | Fiber, ): Array { const eventResponderInstances = []; @@ -560,7 +581,7 @@ function getTargetEventResponderInstances( // Validate the target event type exists on the responder if (targetEventTypes !== undefined) { const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes); - if (targetEventTypesSet.has(topLevelType)) { + if (targetEventTypesSet.has(listeningName)) { eventResponderInstances.push(eventComponentInstance); } } @@ -571,11 +592,11 @@ function getTargetEventResponderInstances( } function getRootEventResponderInstances( - topLevelType: DOMTopLevelEventType, + listeningName: string, ): Array { const eventResponderInstances = []; const rootEventInstances = rootEventTypesToEventComponentInstances.get( - topLevelType, + listeningName, ); if (rootEventInstances !== undefined) { const rootEventComponentInstances = Array.from(rootEventInstances); @@ -618,20 +639,30 @@ function traverseAndHandleEventResponderInstances( nativeEventTarget: EventTarget, eventSystemFlags: EventSystemFlags, ): void { + const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0; + const isCaptureEvent = (eventSystemFlags & IS_CAPTURE) !== 0; + const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0; + const listeningName = generateListeningKey( + ((topLevelType: any): string), + isPassiveEvent || !isPassiveSupported, + isCaptureEvent, + ); + // Trigger event responders in this order: // - Capture target phase // - Bubble target phase // - Root phase const targetEventResponderInstances = getTargetEventResponderInstances( - topLevelType, + listeningName, targetFiber, ); const responderEvent = createResponderEvent( ((topLevelType: any): string), nativeEvent, ((nativeEventTarget: any): Element | Document), - eventSystemFlags, + isPassiveEvent, + isPassiveSupported, ); const propagatedEventResponders: Set = new Set(); let length = targetEventResponderInstances.length; @@ -684,7 +715,7 @@ function traverseAndHandleEventResponderInstances( } // Root phase const rootEventResponderInstances = getRootEventResponderInstances( - topLevelType, + listeningName, ); length = rootEventResponderInstances.length; if (length > 0) { @@ -835,25 +866,74 @@ export function addRootEventTypesForComponentInstance( ): void { for (let i = 0; i < rootEventTypes.length; i++) { const rootEventType = rootEventTypes[i]; - const topLevelEventType = - typeof rootEventType === 'string' ? rootEventType : rootEventType.name; - let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( - topLevelEventType, - ); - if (rootEventComponentInstances === undefined) { - rootEventComponentInstances = new Set(); - rootEventTypesToEventComponentInstances.set( - topLevelEventType, - rootEventComponentInstances, - ); + registerRootEventType(rootEventType, eventComponentInstance); + } +} + +function registerRootEventType( + rootEventType: ReactEventResponderEventType, + eventComponentInstance: ReactEventComponentInstance, +): void { + let name = rootEventType; + let capture = false; + let passive = true; + + if (typeof rootEventType !== 'string') { + const targetEventConfigObject = ((rootEventType: any): { + name: string, + passive?: boolean, + capture?: boolean, + }); + name = targetEventConfigObject.name; + if (targetEventConfigObject.passive !== undefined) { + passive = targetEventConfigObject.passive; } - let rootEventTypesSet = eventComponentInstance.rootEventTypes; - if (rootEventTypesSet === null) { - rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set(); + if (targetEventConfigObject.capture !== undefined) { + capture = targetEventConfigObject.capture; } - rootEventTypesSet.add(topLevelEventType); - rootEventComponentInstances.add( - ((eventComponentInstance: any): ReactEventComponentInstance), + } + + const listeningName = generateListeningKey( + ((name: any): string), + passive, + capture, + ); + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( + listeningName, + ); + if (rootEventComponentInstances === undefined) { + rootEventComponentInstances = new Set(); + rootEventTypesToEventComponentInstances.set( + listeningName, + rootEventComponentInstances, ); } + let rootEventTypesSet = eventComponentInstance.rootEventTypes; + if (rootEventTypesSet === null) { + rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set(); + } + invariant( + !rootEventTypesSet.has(listeningName), + '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.', + name, + ); + rootEventTypesSet.add(listeningName); + rootEventComponentInstances.add( + ((eventComponentInstance: any): ReactEventComponentInstance), + ); +} + +export function generateListeningKey( + topLevelType: string, + passive: boolean, + capture: boolean, +): string { + // Create a unique name for this event, plus its properties. We'll + // use this to ensure we don't listen to the same event with the same + // properties again. + const passiveKey = passive ? '_passive' : '_active'; + const captureKey = capture ? '_capture' : ''; + return `${topLevelType}${passiveKey}${captureKey}`; } diff --git a/packages/react-dom/src/events/ReactDOMEventListener.js b/packages/react-dom/src/events/ReactDOMEventListener.js index b710b2728dfa0..f5fa9d3170629 100644 --- a/packages/react-dom/src/events/ReactDOMEventListener.js +++ b/packages/react-dom/src/events/ReactDOMEventListener.js @@ -22,6 +22,7 @@ import { RESPONDER_EVENT_SYSTEM, IS_PASSIVE, IS_ACTIVE, + IS_CAPTURE, PASSIVE_NOT_SUPPORTED, } from 'events/EventSystemFlags'; @@ -189,6 +190,9 @@ export function trapEventForResponderEventSystem( } else { eventFlags |= IS_ACTIVE; } + if (capture) { + eventFlags |= IS_CAPTURE; + } // Check if interactive and wrap in interactiveUpdates const listener = dispatchEvent.bind(null, topLevelType, eventFlags); addEventListener(element, rawEventName, listener, { diff --git a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js index 36aeb91d63894..18c2a9ba407bc 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -757,4 +757,134 @@ describe('DOMEventResponderSystem', () => { expect(log).toEqual([false, true, false]); }); + + it('the event responder target listeners should correctly fire for only their events', () => { + let clickEventComponent1Fired = 0; + let clickEventComponent2Fired = 0; + let eventLog = []; + const buttonRef = React.createRef(); + + const ClickEventComponent1 = createReactEventComponent( + [{name: 'click', passive: false, capture: false}], + undefined, + undefined, + event => { + clickEventComponent1Fired++; + eventLog.push({ + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, + }); + }, + ); + + const ClickEventComponent2 = createReactEventComponent( + [{name: 'click', passive: true, capture: false}], + undefined, + undefined, + event => { + clickEventComponent2Fired++; + eventLog.push({ + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, + }); + }, + ); + + const Test = () => ( + + + + + + ); + + ReactDOM.render(, container); + + let 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, + passiveSupported: false, + }, + { + name: 'click', + passive: false, + passiveSupported: true, + }, + ]); + }); + + it('the event responder root listeners should correctly fire for only their events', () => { + let clickEventComponent1Fired = 0; + let clickEventComponent2Fired = 0; + let eventLog = []; + + const ClickEventComponent1 = createReactEventComponent( + undefined, + [{name: 'click', passive: false, capture: false}], + undefined, + undefined, + undefined, + event => { + clickEventComponent1Fired++; + eventLog.push({ + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, + }); + }, + ); + + const ClickEventComponent2 = createReactEventComponent( + undefined, + [{name: 'click', passive: true, capture: false}], + undefined, + undefined, + undefined, + event => { + clickEventComponent2Fired++; + eventLog.push({ + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, + }); + }, + ); + + const Test = () => ( + + + + + + ); + + ReactDOM.render(, container); + + dispatchClickEvent(document.body); + + expect(clickEventComponent1Fired).toBe(1); + expect(clickEventComponent2Fired).toBe(1); + expect(eventLog.length).toBe(2); + expect(eventLog).toEqual([ + { + name: 'click', + passive: false, + passiveSupported: false, + }, + { + name: 'click', + passive: false, + passiveSupported: true, + }, + ]); + }); });