Skip to content

Commit dbd85a0

Browse files
authored
ReactDOM.useEvent: support custom types (facebook#18351)
* ReactDOM.useEvent: support custom types
1 parent 7c14786 commit dbd85a0

File tree

6 files changed

+147
-29
lines changed

6 files changed

+147
-29
lines changed

packages/legacy-events/ReactSyntheticEventType.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,27 @@ import type {EventPriority} from 'shared/ReactTypes';
1313
import type {TopLevelType} from './TopLevelEventTypes';
1414

1515
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,
2020
|},
2121
registrationName?: string,
2222
eventPriority: EventPriority,
2323
|};
2424

25+
export type CustomDispatchConfig = {|
26+
phasedRegistrationNames: {|
27+
bubbled: null,
28+
captured: null,
29+
|},
30+
customEvent: true,
31+
|};
32+
2533
export type ReactSyntheticEvent = {|
26-
dispatchConfig: DispatchConfig,
34+
dispatchConfig: DispatchConfig | CustomDispatchConfig,
2735
getPooled: (
28-
dispatchConfig: DispatchConfig,
36+
dispatchConfig: DispatchConfig | CustomDispatchConfig,
2937
targetInst: Fiber,
3038
nativeTarget: Event,
3139
nativeEventTarget: EventTarget,

packages/react-dom/src/events/DOMEventProperties.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import type {
1212
TopLevelType,
1313
DOMTopLevelEventType,
1414
} from 'legacy-events/TopLevelEventTypes';
15-
import type {DispatchConfig} from 'legacy-events/ReactSyntheticEventType';
15+
import type {
16+
DispatchConfig,
17+
CustomDispatchConfig,
18+
} from 'legacy-events/ReactSyntheticEventType';
1619

1720
import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes';
1821
import {
@@ -31,7 +34,7 @@ export const simpleEventPluginEventTypes = {};
3134

3235
export const topLevelEventsToDispatchConfig: Map<
3336
TopLevelType,
34-
DispatchConfig,
37+
DispatchConfig | CustomDispatchConfig,
3538
> = new Map();
3639

3740
const eventPriorities = new Map();

packages/react-dom/src/events/DOMModernPluginEventSystem.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
1717
import type {EventPriority} from 'shared/ReactTypes';
1818
import type {Fiber} from 'react-reconciler/src/ReactFiber';
1919
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';
2124
import type {ReactDOMListener} from 'shared/ReactDOMTypes';
2225

2326
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
@@ -79,6 +82,7 @@ import {
7982
COMMENT_NODE,
8083
ELEMENT_NODE,
8184
} from '../shared/HTMLNodeType';
85+
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
8286

8387
import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
8488

@@ -118,6 +122,14 @@ const capturePhaseEvents = new Set([
118122
TOP_WAITING,
119123
]);
120124

125+
const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
126+
customEvent: true,
127+
phasedRegistrationNames: {
128+
bubbled: null,
129+
captured: null,
130+
},
131+
};
132+
121133
const isArray = Array.isArray;
122134

123135
function dispatchEventsForPlugins(
@@ -419,8 +431,21 @@ export function attachElementListener(listener: ReactDOMListener): void {
419431
listeners = new Set();
420432
initListenersSet(target, listeners);
421433
}
422-
// Finally, add our listener to the listeners Set.
434+
// Add our listener to the listeners Set.
423435
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+
}
424449
}
425450

426451
export function detachElementListener(listener: ReactDOMListener): void {

packages/react-dom/src/events/SimpleEventPlugin.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
172172
break;
173173
default:
174174
if (__DEV__) {
175-
if (knownHTMLTopLevelTypes.indexOf(topLevelType) === -1) {
175+
if (
176+
knownHTMLTopLevelTypes.indexOf(topLevelType) === -1 &&
177+
dispatchConfig.customEvent !== true
178+
) {
176179
console.error(
177180
'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' +
178181
'is likely caused by a bug in React. Please file an issue.',

packages/react-dom/src/events/__tests__/DOMModernPluginEventSystem-test.internal.js

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ let ReactDOM;
1515
let ReactDOMServer;
1616
let Scheduler;
1717

18-
function dispatchClickEvent(element) {
18+
function dispatchEvent(element, type) {
1919
const event = document.createEvent('Event');
20-
event.initEvent('click', true, true);
20+
event.initEvent(type, true, true);
2121
element.dispatchEvent(event);
2222
}
2323

24+
function dispatchClickEvent(element) {
25+
dispatchEvent(element, 'click');
26+
}
27+
2428
describe('DOMModernPluginEventSystem', () => {
2529
let container;
2630

@@ -1782,6 +1786,80 @@ describe('DOMModernPluginEventSystem', () => {
17821786
dispatchClickEvent(button);
17831787
expect(clickEvent).toHaveBeenCalledTimes(1);
17841788
});
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+
});
17851863
});
17861864
},
17871865
);

packages/react-dom/src/events/accumulateTwoPhaseListeners.js

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,9 @@ export default function accumulateTwoPhaseListeners(
2020
accumulateUseEventListeners?: boolean,
2121
): void {
2222
const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
23-
if (phasedRegistrationNames == null) {
24-
return;
25-
}
26-
const {bubbled, captured} = phasedRegistrationNames;
2723
const dispatchListeners = [];
2824
const dispatchInstances = [];
25+
const {bubbled, captured} = phasedRegistrationNames;
2926
let node = event._targetInst;
3027

3128
// Accumulate all instances and listeners via the target -> root path.
@@ -60,19 +57,23 @@ export default function accumulateTwoPhaseListeners(
6057
}
6158
}
6259
// 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+
}
6968
}
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+
}
7677
}
7778
}
7879
node = node.return;

0 commit comments

Comments
 (0)