diff --git a/packages/legacy-events/ReactSyntheticEventType.js b/packages/legacy-events/ReactSyntheticEventType.js index 80330aa5cdca4..5c98337e03957 100644 --- a/packages/legacy-events/ReactSyntheticEventType.js +++ b/packages/legacy-events/ReactSyntheticEventType.js @@ -13,19 +13,27 @@ import type {EventPriority} from 'shared/ReactTypes'; import type {TopLevelType} from './TopLevelEventTypes'; export type DispatchConfig = {| - dependencies: Array, - phasedRegistrationNames?: {| - bubbled: string, - captured: string, + dependencies?: Array, + phasedRegistrationNames: {| + bubbled: null | string, + captured: null | string, |}, registrationName?: string, eventPriority: EventPriority, |}; +export type CustomDispatchConfig = {| + phasedRegistrationNames: {| + bubbled: null, + captured: null, + |}, + customEvent: true, +|}; + export type ReactSyntheticEvent = {| - dispatchConfig: DispatchConfig, + dispatchConfig: DispatchConfig | CustomDispatchConfig, getPooled: ( - dispatchConfig: DispatchConfig, + dispatchConfig: DispatchConfig | CustomDispatchConfig, targetInst: Fiber, nativeTarget: Event, nativeEventTarget: EventTarget, diff --git a/packages/react-dom/src/events/DOMEventProperties.js b/packages/react-dom/src/events/DOMEventProperties.js index 94c3fb5b2f279..6a3b0e68fbf80 100644 --- a/packages/react-dom/src/events/DOMEventProperties.js +++ b/packages/react-dom/src/events/DOMEventProperties.js @@ -12,7 +12,10 @@ import type { TopLevelType, DOMTopLevelEventType, } from 'legacy-events/TopLevelEventTypes'; -import type {DispatchConfig} from 'legacy-events/ReactSyntheticEventType'; +import type { + DispatchConfig, + CustomDispatchConfig, +} from 'legacy-events/ReactSyntheticEventType'; import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes'; import { @@ -31,7 +34,7 @@ export const simpleEventPluginEventTypes = {}; export const topLevelEventsToDispatchConfig: Map< TopLevelType, - DispatchConfig, + DispatchConfig | CustomDispatchConfig, > = new Map(); const eventPriorities = new Map(); diff --git a/packages/react-dom/src/events/DOMModernPluginEventSystem.js b/packages/react-dom/src/events/DOMModernPluginEventSystem.js index f9f7fe0c128e8..1b09bb3cc22df 100644 --- a/packages/react-dom/src/events/DOMModernPluginEventSystem.js +++ b/packages/react-dom/src/events/DOMModernPluginEventSystem.js @@ -17,7 +17,10 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags'; import type {EventPriority} from 'shared/ReactTypes'; import type {Fiber} from 'react-reconciler/src/ReactFiber'; import type {PluginModule} from 'legacy-events/PluginModuleType'; -import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType'; +import type { + ReactSyntheticEvent, + CustomDispatchConfig, +} from 'legacy-events/ReactSyntheticEventType'; import type {ReactDOMListener} from 'shared/ReactDOMTypes'; import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry'; @@ -79,6 +82,7 @@ import { COMMENT_NODE, ELEMENT_NODE, } from '../shared/HTMLNodeType'; +import {topLevelEventsToDispatchConfig} from './DOMEventProperties'; import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags'; @@ -118,6 +122,14 @@ const capturePhaseEvents = new Set([ TOP_WAITING, ]); +const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = { + customEvent: true, + phasedRegistrationNames: { + bubbled: null, + captured: null, + }, +}; + const isArray = Array.isArray; function dispatchEventsForPlugins( @@ -419,8 +431,21 @@ export function attachElementListener(listener: ReactDOMListener): void { listeners = new Set(); initListenersSet(target, listeners); } - // Finally, add our listener to the listeners Set. + // Add our listener to the listeners Set. listeners.add(listener); + // Finally, add the event to our known event types list. + let dispatchConfig = topLevelEventsToDispatchConfig.get(type); + // If we don't have a dispatchConfig, 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 (dispatchConfig === undefined) { + topLevelEventsToDispatchConfig.set( + type, + emptyDispatchConfigForCustomEvents, + ); + } } export function detachElementListener(listener: ReactDOMListener): void { diff --git a/packages/react-dom/src/events/SimpleEventPlugin.js b/packages/react-dom/src/events/SimpleEventPlugin.js index 1c9cc0ecb0f1e..0f96651e3c056 100644 --- a/packages/react-dom/src/events/SimpleEventPlugin.js +++ b/packages/react-dom/src/events/SimpleEventPlugin.js @@ -172,7 +172,10 @@ const SimpleEventPlugin: PluginModule = { break; default: if (__DEV__) { - if (knownHTMLTopLevelTypes.indexOf(topLevelType) === -1) { + if ( + knownHTMLTopLevelTypes.indexOf(topLevelType) === -1 && + dispatchConfig.customEvent !== true + ) { console.error( 'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' + 'is likely caused by a bug in React. Please file an issue.', diff --git a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js index 17829a86761cc..e49236cacb4c8 100644 --- a/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js +++ b/packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js @@ -15,12 +15,16 @@ let ReactDOM; let ReactDOMServer; let Scheduler; -function dispatchClickEvent(element) { +function dispatchEvent(element, type) { const event = document.createEvent('Event'); - event.initEvent('click', true, true); + event.initEvent(type, true, true); element.dispatchEvent(event); } +function dispatchClickEvent(element) { + dispatchEvent(element, 'click'); +} + describe('DOMModernPluginEventSystem', () => { let container; @@ -1782,6 +1786,80 @@ describe('DOMModernPluginEventSystem', () => { dispatchClickEvent(button); expect(clickEvent).toHaveBeenCalledTimes(1); }); + + 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]), + ); + const onCustomEventCapture = jest.fn(e => + log.push(['capture', e.currentTarget]), + ); + + function Test() { + let customEventHandle; + + // Test that we get a warning when we don't provide an explicit priortiy + expect(() => { + customEventHandle = ReactDOM.unstable_useEvent('custom-event'); + }).toWarnDev( + 'Warning: The event "type" provided to useEvent() does not have a known priority type. ' + + 'It is recommended to provide a "priority" option to specify a priority.', + ); + + customEventHandle = ReactDOM.unstable_useEvent('custom-event', { + priority: 0, // Discrete + }); + + const customCaptureHandle = ReactDOM.unstable_useEvent( + 'custom-event', + { + capture: true, + priority: 0, // Discrete + }, + ); + + React.useEffect(() => { + customEventHandle.setListener(buttonRef.current, onCustomEvent); + customCaptureHandle.setListener( + buttonRef.current, + onCustomEventCapture, + ); + customEventHandle.setListener(divRef.current, onCustomEvent); + customCaptureHandle.setListener( + divRef.current, + onCustomEventCapture, + ); + }); + + return ( + + ); + } + + ReactDOM.render(, container); + Scheduler.unstable_flushAll(); + + let 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]); + + let 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]); + }); }); }, ); diff --git a/packages/react-dom/src/events/accumulateTwoPhaseListeners.js b/packages/react-dom/src/events/accumulateTwoPhaseListeners.js index ae10f68dc4d30..e43fff6ed3225 100644 --- a/packages/react-dom/src/events/accumulateTwoPhaseListeners.js +++ b/packages/react-dom/src/events/accumulateTwoPhaseListeners.js @@ -20,12 +20,9 @@ export default function accumulateTwoPhaseListeners( accumulateUseEventListeners?: boolean, ): void { const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames; - if (phasedRegistrationNames == null) { - return; - } - const {bubbled, captured} = phasedRegistrationNames; const dispatchListeners = []; const dispatchInstances = []; + const {bubbled, captured} = phasedRegistrationNames; let node = event._targetInst; // Accumulate all instances and listeners via the target -> root path. @@ -60,19 +57,23 @@ export default function accumulateTwoPhaseListeners( } } // Standard React on* listeners, i.e. onClick prop - const captureListener = getListener(node, captured); - if (captureListener != null) { - // Capture listeners/instances should go at the start, so we - // unshift them to the start of the array. - dispatchListeners.unshift(captureListener); - dispatchInstances.unshift(node); + if (captured !== null) { + const captureListener = getListener(node, captured); + if (captureListener != null) { + // Capture listeners/instances should go at the start, so we + // unshift them to the start of the array. + dispatchListeners.unshift(captureListener); + dispatchInstances.unshift(node); + } } - const bubbleListener = getListener(node, bubbled); - if (bubbleListener != null) { - // Bubble listeners/instances should go at the end, so we - // push them to the end of the array. - dispatchListeners.push(bubbleListener); - dispatchInstances.push(node); + if (bubbled !== null) { + const bubbleListener = getListener(node, bubbled); + if (bubbleListener != null) { + // Bubble listeners/instances should go at the end, so we + // push them to the end of the array. + dispatchListeners.push(bubbleListener); + dispatchInstances.push(node); + } } } node = node.return;