@samitouri / QOS-React-2 / commits / dbd85a08d9

ReactDOM.useEvent: support custom types (#18351)

* ReactDOM.useEvent: support custom types

Dominic Gannaway committed Mar 20, 2020 at 14:45 UTC dbd85a08d9274689973aa24e4c4e4d7ee669c913
6 files changed +147 -29
packages/legacy-events/ReactSyntheticEventType.js
+14 -6
@@ -13,19 +13,27 @@ import type {EventPriority} from 'shared/ReactTypes';
13 import type {TopLevelType} from './TopLevelEventTypes';
14
15 export type DispatchConfig = {|
16 - dependencies: Array<TopLevelType>,
17 - phasedRegistrationNames?: {|
18 - bubbled: string,
19 - captured: string,
16 + dependencies?: Array<TopLevelType>,
17 + phasedRegistrationNames: {|
18 + bubbled: null | string,
19 + captured: null | string,
20 |},
21 registrationName?: string,
22 eventPriority: EventPriority,
23 |};
24
25 +export type CustomDispatchConfig = {|
26 + phasedRegistrationNames: {|
27 + bubbled: null,
28 + captured: null,
29 + |},
30 + customEvent: true,
31 +|};
32 +
33 export type ReactSyntheticEvent = {|
26 - dispatchConfig: DispatchConfig,
34 + dispatchConfig: DispatchConfig | CustomDispatchConfig,
35 getPooled: (
28 - dispatchConfig: DispatchConfig,
36 + dispatchConfig: DispatchConfig | CustomDispatchConfig,
37 targetInst: Fiber,
38 nativeTarget: Event,
39 nativeEventTarget: EventTarget,
packages/react-dom/src/events/DOMEventProperties.js
+5 -2
@@ -12,7 +12,10 @@ import type {
12 TopLevelType,
13 DOMTopLevelEventType,
14 } from 'legacy-events/TopLevelEventTypes';
15 -import type {DispatchConfig} from 'legacy-events/ReactSyntheticEventType';
15 +import type {
16 + DispatchConfig,
17 + CustomDispatchConfig,
18 +} from 'legacy-events/ReactSyntheticEventType';
19
20 import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes';
21 import {
@@ -31,7 +34,7 @@ export const simpleEventPluginEventTypes = {};
34
35 export const topLevelEventsToDispatchConfig: Map<
36 TopLevelType,
34 - DispatchConfig,
37 + DispatchConfig | CustomDispatchConfig,
38 > = new Map();
39
40 const eventPriorities = new Map();
packages/react-dom/src/events/DOMModernPluginEventSystem.js
+27 -2
@@ -17,7 +17,10 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
17 import type {EventPriority} from 'shared/ReactTypes';
18 import type {Fiber} from 'react-reconciler/src/ReactFiber';
19 import type {PluginModule} from 'legacy-events/PluginModuleType';
20 -import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
20 +import type {
21 + ReactSyntheticEvent,
22 + CustomDispatchConfig,
23 +} from 'legacy-events/ReactSyntheticEventType';
24 import type {ReactDOMListener} from 'shared/ReactDOMTypes';
25
26 import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
@@ -79,6 +82,7 @@ import {
82 COMMENT_NODE,
83 ELEMENT_NODE,
84 } from '../shared/HTMLNodeType';
85 +import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
86
87 import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
88
@@ -118,6 +122,14 @@ const capturePhaseEvents = new Set([
122 TOP_WAITING,
123 ]);
124
125 +const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
126 + customEvent: true,
127 + phasedRegistrationNames: {
128 + bubbled: null,
129 + captured: null,
130 + },
131 +};
132 +
133 const isArray = Array.isArray;
134
135 function dispatchEventsForPlugins(
@@ -419,8 +431,21 @@ export function attachElementListener(listener: ReactDOMListener): void {
431 listeners = new Set();
432 initListenersSet(target, listeners);
433 }
422 - // Finally, add our listener to the listeners Set.
434 + // Add our listener to the listeners Set.
435 listeners.add(listener);
436 + // Finally, add the event to our known event types list.
437 + let dispatchConfig = topLevelEventsToDispatchConfig.get(type);
438 + // If we don't have a dispatchConfig, then we're dealing with
439 + // an event type that React does not know about (i.e. a custom event).
440 + // We need to register an event config for this or the SimpleEventPlugin
441 + // will not appropriately provide a SyntheticEvent, so we use out empty
442 + // dispatch config for custom events.
443 + if (dispatchConfig === undefined) {
444 + topLevelEventsToDispatchConfig.set(
445 + type,
446 + emptyDispatchConfigForCustomEvents,
447 + );
448 + }
449 }
450
451 export function detachElementListener(listener: ReactDOMListener): void {
packages/react-dom/src/events/SimpleEventPlugin.js
+4 -1
@@ -172,7 +172,10 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
172 break;
173 default:
174 if (__DEV__) {
175 - if (knownHTMLTopLevelTypes.indexOf(topLevelType) === -1) {
175 + if (
176 + knownHTMLTopLevelTypes.indexOf(topLevelType) === -1 &&
177 + dispatchConfig.customEvent !== true
178 + ) {
179 console.error(
180 'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' +
181 'is likely caused by a bug in React. Please file an issue.',
packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js
+80 -2
@@ -15,12 +15,16 @@ let ReactDOM;
15 let ReactDOMServer;
16 let Scheduler;
17
18 -function dispatchClickEvent(element) {
18 +function dispatchEvent(element, type) {
19 const event = document.createEvent('Event');
20 - event.initEvent('click', true, true);
20 + event.initEvent(type, true, true);
21 element.dispatchEvent(event);
22 }
23
24 +function dispatchClickEvent(element) {
25 + dispatchEvent(element, 'click');
26 +}
27 +
28 describe('DOMModernPluginEventSystem', () => {
29 let container;
30
@@ -1782,6 +1786,80 @@ describe('DOMModernPluginEventSystem', () => {
1786 dispatchClickEvent(button);
1787 expect(clickEvent).toHaveBeenCalledTimes(1);
1788 });
1789 +
1790 + it('handles propagation of custom user events', () => {
1791 + const buttonRef = React.createRef();
1792 + const divRef = React.createRef();
1793 + const log = [];
1794 + const onCustomEvent = jest.fn(e =>
1795 + log.push(['bubble', e.currentTarget]),
1796 + );
1797 + const onCustomEventCapture = jest.fn(e =>
1798 + log.push(['capture', e.currentTarget]),
1799 + );
1800 +
1801 + function Test() {
1802 + let customEventHandle;
1803 +
1804 + // Test that we get a warning when we don't provide an explicit priortiy
1805 + expect(() => {
1806 + customEventHandle = ReactDOM.unstable_useEvent('custom-event');
1807 + }).toWarnDev(
1808 + 'Warning: The event "type" provided to useEvent() does not have a known priority type. ' +
1809 + 'It is recommended to provide a "priority" option to specify a priority.',
1810 + );
1811 +
1812 + customEventHandle = ReactDOM.unstable_useEvent('custom-event', {
1813 + priority: 0, // Discrete
1814 + });
1815 +
1816 + const customCaptureHandle = ReactDOM.unstable_useEvent(
1817 + 'custom-event',
1818 + {
1819 + capture: true,
1820 + priority: 0, // Discrete
1821 + },
1822 + );
1823 +
1824 + React.useEffect(() => {
1825 + customEventHandle.setListener(buttonRef.current, onCustomEvent);
1826 + customCaptureHandle.setListener(
1827 + buttonRef.current,
1828 + onCustomEventCapture,
1829 + );
1830 + customEventHandle.setListener(divRef.current, onCustomEvent);
1831 + customCaptureHandle.setListener(
1832 + divRef.current,
1833 + onCustomEventCapture,
1834 + );
1835 + });
1836 +
1837 + return (
1838 + <button ref={buttonRef}>
1839 + <div ref={divRef}>Click me!</div>
1840 + </button>
1841 + );
1842 + }
1843 +
1844 + ReactDOM.render(<Test />, container);
1845 + Scheduler.unstable_flushAll();
1846 +
1847 + let buttonElement = buttonRef.current;
1848 + dispatchEvent(buttonElement, 'custom-event');
1849 + expect(onCustomEvent).toHaveBeenCalledTimes(1);
1850 + expect(onCustomEventCapture).toHaveBeenCalledTimes(1);
1851 + expect(log[0]).toEqual(['capture', buttonElement]);
1852 + expect(log[1]).toEqual(['bubble', buttonElement]);
1853 +
1854 + let divElement = divRef.current;
1855 + dispatchEvent(divElement, 'custom-event');
1856 + expect(onCustomEvent).toHaveBeenCalledTimes(3);
1857 + expect(onCustomEventCapture).toHaveBeenCalledTimes(3);
1858 + expect(log[2]).toEqual(['capture', buttonElement]);
1859 + expect(log[3]).toEqual(['capture', divElement]);
1860 + expect(log[4]).toEqual(['bubble', divElement]);
1861 + expect(log[5]).toEqual(['bubble', buttonElement]);
1862 + });
1863 });
1864 },
1865 );
packages/react-dom/src/events/accumulateTwoPhaseListeners.js
+17 -16
@@ -20,12 +20,9 @@ export default function accumulateTwoPhaseListeners(
20 accumulateUseEventListeners?: boolean,
21 ): void {
22 const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
23 - if (phasedRegistrationNames == null) {
24 - return;
25 - }
26 - const {bubbled, captured} = phasedRegistrationNames;
23 const dispatchListeners = [];
24 const dispatchInstances = [];
25 + const {bubbled, captured} = phasedRegistrationNames;
26 let node = event._targetInst;
27
28 // Accumulate all instances and listeners via the target -> root path.
@@ -60,19 +57,23 @@ export default function accumulateTwoPhaseListeners(
57 }
58 }
59 // Standard React on* listeners, i.e. onClick prop
63 - const captureListener = getListener(node, captured);
64 - if (captureListener != null) {
65 - // Capture listeners/instances should go at the start, so we
66 - // unshift them to the start of the array.
67 - dispatchListeners.unshift(captureListener);
68 - dispatchInstances.unshift(node);
60 + if (captured !== null) {
61 + const captureListener = getListener(node, captured);
62 + if (captureListener != null) {
63 + // Capture listeners/instances should go at the start, so we
64 + // unshift them to the start of the array.
65 + dispatchListeners.unshift(captureListener);
66 + dispatchInstances.unshift(node);
67 + }
68 }
70 - const bubbleListener = getListener(node, bubbled);
71 - if (bubbleListener != null) {
72 - // Bubble listeners/instances should go at the end, so we
73 - // push them to the end of the array.
74 - dispatchListeners.push(bubbleListener);
75 - dispatchInstances.push(node);
69 + if (bubbled !== null) {
70 + const bubbleListener = getListener(node, bubbled);
71 + if (bubbleListener != null) {
72 + // Bubble listeners/instances should go at the end, so we
73 + // push them to the end of the array.
74 + dispatchListeners.push(bubbleListener);
75 + dispatchInstances.push(node);
76 + }
77 }
78 }
79 node = node.return;