diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js index bf5508c72cbb3..962648161e95e 100644 --- a/packages/react-dom/src/client/ReactDOMComponent.js +++ b/packages/react-dom/src/client/ReactDOMComponent.js @@ -1302,7 +1302,7 @@ export function listenToEventResponderEventTypes( if (__DEV__) { warning( typeof targetEventType === 'object' && targetEventType !== null, - 'Event Responder: invalid entry in targetEventTypes array. ' + + 'Event Responder: invalid entry in event types array. ' + 'Entry must be string or an object. Instead, got %s.', targetEventType, ); diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 91657fbe67046..c829d0912f1cc 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -34,6 +34,7 @@ import { setEnabled as ReactBrowserEventEmitterSetEnabled, } from '../events/ReactBrowserEventEmitter'; import {Namespaces, getChildNamespace} from '../shared/DOMNamespaces'; +import {addRootEventTypesForComponentInstance} from '../events/DOMEventResponderSystem'; import { ELEMENT_NODE, TEXT_NODE, @@ -906,10 +907,18 @@ export function updateEventComponent( if (enableEventAPI) { const rootContainerInstance = ((eventComponentInstance.rootInstance: any): Container); const rootElement = rootContainerInstance.ownerDocument; - listenToEventResponderEventTypes( - eventComponentInstance.responder.targetEventTypes, - rootElement, - ); + const responder = eventComponentInstance.responder; + const {rootEventTypes, targetEventTypes} = responder; + if (targetEventTypes !== undefined) { + listenToEventResponderEventTypes(targetEventTypes, rootElement); + } + if (rootEventTypes !== undefined) { + addRootEventTypesForComponentInstance( + eventComponentInstance, + rootEventTypes, + ); + listenToEventResponderEventTypes(rootEventTypes, rootElement); + } } } diff --git a/packages/react-dom/src/events/DOMEventResponderSystem.js b/packages/react-dom/src/events/DOMEventResponderSystem.js index 685e227c7b1c7..2e3da99145f45 100644 --- a/packages/react-dom/src/events/DOMEventResponderSystem.js +++ b/packages/react-dom/src/events/DOMEventResponderSystem.js @@ -206,9 +206,20 @@ const eventResponderContext: ReactResponderContext = { rootEventComponentInstances, ); } - rootEventComponentInstances.add( - ((currentInstance: any): ReactEventComponentInstance), + 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); } }, removeRootEventTypes( @@ -222,6 +233,11 @@ const eventResponderContext: ReactResponderContext = { let rootEventComponents = rootEventTypesToEventComponentInstances.get( topLevelEventType, ); + let rootEventTypesSet = ((currentInstance: any): ReactEventComponentInstance) + .rootEventTypes; + if (rootEventTypesSet !== null) { + rootEventTypesSet.delete(topLevelEventType); + } if (rootEventComponents !== undefined) { rootEventComponents.delete( ((currentInstance: any): ReactEventComponentInstance), @@ -636,6 +652,20 @@ export function unmountEventResponder( if (responder.onOwnershipChange !== undefined) { ownershipChangeListeners.delete(eventComponentInstance); } + const rootEventTypesSet = eventComponentInstance.rootEventTypes; + if (rootEventTypesSet !== null) { + const rootEventTypes = Array.from(rootEventTypesSet); + + for (let i = 0; i < rootEventTypes.length; i++) { + const topLevelEventType = rootEventTypes[i]; + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get( + topLevelEventType, + ); + if (rootEventComponentInstances !== undefined) { + rootEventComponentInstances.delete(eventComponentInstance); + } + } + } } function validateResponderContext(): void { @@ -671,3 +701,32 @@ export function dispatchEventForResponderEventSystem( } } } + +export function addRootEventTypesForComponentInstance( + eventComponentInstance: ReactEventComponentInstance, + rootEventTypes: Array, +): 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, + ); + } + let rootEventTypesSet = eventComponentInstance.rootEventTypes; + if (rootEventTypesSet === null) { + rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set(); + } + rootEventTypesSet.add(topLevelEventType); + rootEventComponentInstances.add( + ((eventComponentInstance: any): ReactEventComponentInstance), + ); + } +} 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 98b7ef9f2cf0e..8d476b4568cd5 100644 --- a/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js @@ -16,6 +16,7 @@ let ReactSymbols; function createReactEventComponent( targetEventTypes, + rootEventTypes, createInitialState, onEvent, onEventCapture, @@ -26,6 +27,7 @@ function createReactEventComponent( ) { const testEventResponder = { targetEventTypes, + rootEventTypes, createInitialState, onEvent, onEventCapture, @@ -90,6 +92,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { eventResponderFiredCount++; eventLog.push({ @@ -163,6 +166,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { eventLog.push({ name: event.type, @@ -217,6 +221,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { eventResponderFiredCount++; eventLog.push({ @@ -288,6 +293,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponentA = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { eventLog.push(`A [bubble]`); }, @@ -299,6 +305,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponentB = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { eventLog.push(`B [bubble]`); }, @@ -336,6 +343,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { eventLog.push(`${props.name} [bubble]`); }, @@ -377,6 +385,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { eventLog.push(`${props.name} [bubble]`); }, @@ -413,6 +422,7 @@ describe('DOMEventResponderSystem', () => { const ClickEventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { if (props.onMagicClick) { const syntheticEvent = { @@ -505,6 +515,7 @@ describe('DOMEventResponderSystem', () => { const LongPressEventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props) => { handleEvent(event, context, props, 'bubble'); }, @@ -551,6 +562,7 @@ describe('DOMEventResponderSystem', () => { undefined, undefined, undefined, + undefined, (event, context, props, state) => {}, () => { onUnmountFired++; @@ -573,6 +585,7 @@ describe('DOMEventResponderSystem', () => { const EventComponent = createReactEventComponent( [], + undefined, () => ({ incrementAmount: 5, }), @@ -603,6 +616,7 @@ describe('DOMEventResponderSystem', () => { const EventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props, state) => { ownershipGained = context.requestOwnership(); }, @@ -641,6 +655,7 @@ describe('DOMEventResponderSystem', () => { const EventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props, state) => { queryResult = Array.from( context.getEventTargetsFromTarget(event.target), @@ -696,6 +711,7 @@ describe('DOMEventResponderSystem', () => { const EventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props, state) => { queryResult = context.getEventTargetsFromTarget( event.target, @@ -743,6 +759,7 @@ describe('DOMEventResponderSystem', () => { const EventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props, state) => { queryResult = context.getEventTargetsFromTarget( event.target, @@ -795,6 +812,7 @@ describe('DOMEventResponderSystem', () => { const EventComponent = createReactEventComponent( ['click'], undefined, + undefined, (event, context, props, state) => { queryResult = context.getEventTargetsFromTarget( event.target, @@ -855,4 +873,48 @@ describe('DOMEventResponderSystem', () => { ]); expect(queryResult3).toEqual([]); }); + + it('the event responder root listeners should fire on a root click event', () => { + let eventResponderFiredCount = 0; + let eventLog = []; + + const ClickEventComponent = createReactEventComponent( + undefined, + ['click'], + undefined, + undefined, + undefined, + event => { + eventResponderFiredCount++; + eventLog.push({ + name: event.type, + passive: event.passive, + passiveSupported: event.passiveSupported, + phase: 'root', + }); + }, + ); + + const Test = () => ( + + + + ); + + ReactDOM.render(, container); + expect(container.innerHTML).toBe(''); + + // Clicking the button should trigger the event responder onEvent() twice + dispatchClickEvent(document.body); + expect(eventResponderFiredCount).toBe(1); + expect(eventLog.length).toBe(1); + expect(eventLog).toEqual([ + { + name: 'click', + passive: false, + passiveSupported: false, + phase: 'root', + }, + ]); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index da5385eb3b059..da05076c53da8 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -816,6 +816,7 @@ function completeWork( context: null, props: newProps, responder, + rootEventTypes: null, rootInstance: rootContainerInstance, state: responderState, }; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 480fb271fcda0..d625733dfd335 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -86,7 +86,8 @@ export type ReactEventResponderEventType = | {name: string, passive?: boolean, capture?: boolean}; export type ReactEventResponder = { - targetEventTypes: Array, + targetEventTypes?: Array, + rootEventTypes?: Array, createInitialState?: (props: null | Object) => Object, stopLocalPropagation: boolean, onEvent?: ( @@ -123,6 +124,7 @@ export type ReactEventComponentInstance = {| context: null | Object, props: null | Object, responder: ReactEventResponder, + rootEventTypes: null | Set, rootInstance: mixed, state: null | Object, |};