Skip to content

Commit 61f2a56

Browse files
authored
Add experimental ReactDOM.createEventHandle (facebook#18756)
1 parent 84fd4b8 commit 61f2a56

16 files changed

+3449
-13
lines changed

packages/react-dom/index.classic.fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ export {
3636
unstable_scheduleHydration,
3737
unstable_renderSubtreeIntoContainer,
3838
unstable_createPortal,
39+
unstable_createEventHandle,
3940
} from './src/client/ReactDOM';

packages/react-dom/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ export {
2727
unstable_scheduleHydration,
2828
unstable_renderSubtreeIntoContainer,
2929
unstable_createPortal,
30+
unstable_createEventHandle,
3031
} from './src/client/ReactDOM';

packages/react-dom/index.modern.fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ export {
1919
createBlockingRoot as unstable_createBlockingRoot,
2020
unstable_flushControlled,
2121
unstable_scheduleHydration,
22+
unstable_createEventHandle,
2223
} from './src/client/ReactDOM';

packages/react-dom/src/client/ReactDOM.js

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
unmountComponentAtNode,
2121
} from './ReactDOMLegacy';
2222
import {createRoot, createBlockingRoot, isValidContainer} from './ReactDOMRoot';
23+
import {createEventHandle} from './ReactDOMEventHandle';
2324

2425
import {
2526
batchedEventUpdates,
@@ -208,6 +209,8 @@ export {
208209
// Temporary alias since we already shipped React 16 RC with it.
209210
// TODO: remove in React 17.
210211
unstable_createPortal,
212+
// enableCreateEventHandleAPI
213+
createEventHandle as unstable_createEventHandle,
211214
};
212215

213216
const foundDevTools = injectIntoDevTools({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
11+
import type {EventPriority, ReactScopeInstance} from 'shared/ReactTypes';
12+
import type {
13+
ReactDOMEventHandle,
14+
ReactDOMEventHandleListener,
15+
} from '../shared/ReactDOMTypes';
16+
17+
import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties';
18+
import {
19+
getClosestInstanceFromNode,
20+
getEventHandlerListeners,
21+
setEventHandlerListeners,
22+
getEventListenerMap,
23+
getFiberFromScopeInstance,
24+
} from './ReactDOMComponentTree';
25+
import {ELEMENT_NODE} from '../shared/HTMLNodeType';
26+
import {
27+
listenToTopLevelEvent,
28+
addEventTypeToDispatchConfig,
29+
} from '../events/DOMModernPluginEventSystem';
30+
31+
import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags';
32+
import {
33+
PLUGIN_EVENT_SYSTEM,
34+
IS_TARGET_PHASE_ONLY,
35+
} from '../events/EventSystemFlags';
36+
37+
import {
38+
enableScopeAPI,
39+
enableCreateEventHandleAPI,
40+
} from 'shared/ReactFeatureFlags';
41+
import invariant from 'shared/invariant';
42+
43+
type EventHandleOptions = {|
44+
capture?: boolean,
45+
passive?: boolean,
46+
priority?: EventPriority,
47+
|};
48+
49+
function getNearestRootOrPortalContainer(node: Fiber): null | Element {
50+
while (node !== null) {
51+
const tag = node.tag;
52+
// Once we encounter a host container or root container
53+
// we can return their DOM instance.
54+
if (tag === HostRoot || tag === HostPortal) {
55+
return node.stateNode.containerInfo;
56+
}
57+
node = node.return;
58+
}
59+
return null;
60+
}
61+
62+
function isValidEventTarget(target: EventTarget | ReactScopeInstance): boolean {
63+
return typeof (target: Object).addEventListener === 'function';
64+
}
65+
66+
function isReactScope(target: EventTarget | ReactScopeInstance): boolean {
67+
return typeof (target: Object).getChildContextValues === 'function';
68+
}
69+
70+
function createEventHandleListener(
71+
type: DOMTopLevelEventType,
72+
capture: boolean,
73+
callback: (SyntheticEvent<EventTarget>) => void,
74+
destroy: (target: EventTarget | ReactScopeInstance) => void,
75+
): ReactDOMEventHandleListener {
76+
return {
77+
callback,
78+
capture,
79+
destroy,
80+
type,
81+
};
82+
}
83+
84+
function registerEventOnNearestTargetContainer(
85+
targetFiber: Fiber,
86+
topLevelType: DOMTopLevelEventType,
87+
passive: boolean | void,
88+
priority: EventPriority | void,
89+
): void {
90+
// If it is, find the nearest root or portal and make it
91+
// our event handle target container.
92+
const targetContainer = getNearestRootOrPortalContainer(targetFiber);
93+
if (targetContainer === null) {
94+
invariant(
95+
false,
96+
'ReactDOM.createEventHandle: setListener called on an target ' +
97+
'that did not have a corresponding root. This is likely a bug in React.',
98+
);
99+
}
100+
const listenerMap = getEventListenerMap(targetContainer);
101+
listenToTopLevelEvent(
102+
topLevelType,
103+
targetContainer,
104+
listenerMap,
105+
PLUGIN_EVENT_SYSTEM,
106+
passive,
107+
priority,
108+
);
109+
}
110+
111+
export function createEventHandle(
112+
type: string,
113+
options?: EventHandleOptions,
114+
): ReactDOMEventHandle {
115+
if (enableCreateEventHandleAPI) {
116+
const topLevelType = ((type: any): DOMTopLevelEventType);
117+
let capture = false;
118+
let passive = undefined; // Undefined means to use the browser default
119+
let priority;
120+
121+
if (options != null) {
122+
const optionsCapture = options.capture;
123+
const optionsPassive = options.passive;
124+
const optionsPriority = options.priority;
125+
126+
if (typeof optionsCapture === 'boolean') {
127+
capture = optionsCapture;
128+
}
129+
if (typeof optionsPassive === 'boolean') {
130+
passive = optionsPassive;
131+
}
132+
if (typeof optionsPriority === 'number') {
133+
priority = optionsPriority;
134+
}
135+
}
136+
if (priority === undefined) {
137+
priority = getEventPriorityForListenerSystem(topLevelType);
138+
}
139+
140+
const listeners = new Map();
141+
142+
const destroy = (target: EventTarget | ReactScopeInstance): void => {
143+
const listener = listeners.get(target);
144+
if (listener !== undefined) {
145+
listeners.delete(target);
146+
const targetListeners = getEventHandlerListeners(target);
147+
if (targetListeners !== null) {
148+
targetListeners.delete(listener);
149+
}
150+
}
151+
};
152+
153+
const clear = (): void => {
154+
const eventTargetsArr = Array.from(listeners.keys());
155+
for (let i = 0; i < eventTargetsArr.length; i++) {
156+
destroy(eventTargetsArr[i]);
157+
}
158+
};
159+
160+
return {
161+
setListener(
162+
target: EventTarget | ReactScopeInstance,
163+
callback: null | ((SyntheticEvent<EventTarget>) => void),
164+
): void {
165+
// Check if the target is a DOM element.
166+
if ((target: any).nodeType === ELEMENT_NODE) {
167+
const targetElement = ((target: any): Element);
168+
// Check if the DOM element is managed by React.
169+
const targetFiber = getClosestInstanceFromNode(targetElement);
170+
if (targetFiber === null) {
171+
invariant(
172+
false,
173+
'ReactDOM.createEventHandle: setListener called on an element ' +
174+
'target that is not managed by React. Ensure React rendered the DOM element.',
175+
);
176+
}
177+
registerEventOnNearestTargetContainer(
178+
targetFiber,
179+
topLevelType,
180+
passive,
181+
priority,
182+
);
183+
} else if (enableScopeAPI && isReactScope(target)) {
184+
const scopeTarget = ((target: any): ReactScopeInstance);
185+
const targetFiber = getFiberFromScopeInstance(scopeTarget);
186+
if (targetFiber === null) {
187+
// Scope is unmounted, do not proceed.
188+
return;
189+
}
190+
registerEventOnNearestTargetContainer(
191+
targetFiber,
192+
topLevelType,
193+
passive,
194+
priority,
195+
);
196+
} else if (isValidEventTarget(target)) {
197+
const eventTarget = ((target: any): EventTarget);
198+
const listenerMap = getEventListenerMap(eventTarget);
199+
listenToTopLevelEvent(
200+
topLevelType,
201+
eventTarget,
202+
listenerMap,
203+
PLUGIN_EVENT_SYSTEM | IS_TARGET_PHASE_ONLY,
204+
passive,
205+
priority,
206+
capture,
207+
);
208+
} else {
209+
invariant(
210+
false,
211+
'ReactDOM.createEventHandle: setListener called on an invalid ' +
212+
'target. Provide a vaid EventTarget or an element managed by React.',
213+
);
214+
}
215+
let listener = listeners.get(target);
216+
if (listener === undefined) {
217+
if (callback === null) {
218+
return;
219+
}
220+
listener = createEventHandleListener(
221+
topLevelType,
222+
capture,
223+
callback,
224+
destroy,
225+
);
226+
listeners.set(target, listener);
227+
228+
let targetListeners = getEventHandlerListeners(target);
229+
if (targetListeners === null) {
230+
targetListeners = new Set();
231+
setEventHandlerListeners(target, targetListeners);
232+
}
233+
targetListeners.add(listener);
234+
// Finally, add the event to our known event types list.
235+
addEventTypeToDispatchConfig(topLevelType);
236+
} else if (callback !== null) {
237+
listener.callback = callback;
238+
} else {
239+
// Remove listener
240+
destroy(target);
241+
}
242+
},
243+
clear,
244+
};
245+
}
246+
return (null: any);
247+
}

packages/react-dom/src/client/ReactDOMHostConfig.js

+14-5
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ import {
7676
enableDeprecatedFlareAPI,
7777
enableFundamentalAPI,
7878
enableModernEventSystem,
79-
enableScopeAPI,
8079
enableCreateEventHandleAPI,
80+
enableScopeAPI,
8181
} from 'shared/ReactFeatureFlags';
8282
import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
8383
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
84-
import {listenToEvent} from '../events/DOMModernPluginEventSystem';
84+
import {
85+
listenToEvent,
86+
clearEventHandleListenersForTarget,
87+
} from '../events/DOMModernPluginEventSystem';
8588

8689
export type Type = string;
8790
export type Props = {
@@ -539,7 +542,9 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
539542
export function removeInstanceEventHandles(
540543
instance: Instance | TextInstance | SuspenseInstance,
541544
) {
542-
// TODO for ReactDOM.createEventInstance
545+
if (enableCreateEventHandleAPI) {
546+
clearEventHandleListenersForTarget(instance);
547+
}
543548
}
544549

545550
export function removeChild(
@@ -1136,8 +1141,12 @@ export function prepareScopeUpdate(
11361141
}
11371142
}
11381143

1139-
export function removeScopeEventHandles(scopeInstance: Object): void {
1140-
// TODO when we add createEventHandle
1144+
export function removeScopeEventHandles(
1145+
scopeInstance: ReactScopeInstance,
1146+
): void {
1147+
if (enableScopeAPI && enableCreateEventHandleAPI) {
1148+
clearEventHandleListenersForTarget(scopeInstance);
1149+
}
11411150
}
11421151

11431152
export function getInstanceFromScope(

0 commit comments

Comments
 (0)