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;