main
js 149 lines 4.74 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and 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 {DOMEventName} from '../events/DOMEventNames';
11 import type {ReactScopeInstance} from 'shared/ReactTypes';
12 import type {
13 ReactDOMEventHandle,
14 ReactDOMEventHandleListener,
15 } from './ReactDOMEventHandleTypes';
16
17 import {allNativeEvents} from '../events/EventRegistry';
18 import {
19 getEventHandlerListeners,
20 setEventHandlerListeners,
21 doesTargetHaveEventHandle,
22 addEventHandleToTarget,
23 } from './ReactDOMComponentTree';
24 import {ELEMENT_NODE} from './HTMLNodeType';
25 import {listenToNativeEventForNonManagedEventTarget} from '../events/DOMPluginEventSystem';
26
27 import {
28 enableScopeAPI,
29 enableCreateEventHandleAPI,
30 } from 'shared/ReactFeatureFlags';
31 import typeof {SyntheticEvent} from '../events/SyntheticEvent';
32
33 type EventHandleOptions = {
34 capture?: boolean,
35 };
36
37 function isValidEventTarget(target: EventTarget | ReactScopeInstance): boolean {
38 return typeof (target as Object).addEventListener === 'function';
39 }
40
41 function isReactScope(target: EventTarget | ReactScopeInstance): boolean {
42 return typeof (target as Object).getChildContextValues === 'function';
43 }
44
45 function createEventHandleListener(
46 type: DOMEventName,
47 isCapturePhaseListener: boolean,
48 callback: SyntheticEvent => void,
49 ): ReactDOMEventHandleListener {
50 return {
51 callback,
52 capture: isCapturePhaseListener,
53 type,
54 };
55 }
56
57 function registerReactDOMEvent(
58 target: EventTarget | ReactScopeInstance,
59 domEventName: DOMEventName,
60 isCapturePhaseListener: boolean,
61 ): void {
62 if ((target as any).nodeType === ELEMENT_NODE) {
63 // Do nothing. We already attached all root listeners.
64 } else if (enableScopeAPI && isReactScope(target)) {
65 // Do nothing. We already attached all root listeners.
66 } else if (isValidEventTarget(target)) {
67 const eventTarget = target as any as EventTarget;
68 // These are valid event targets, but they are also
69 // non-managed React nodes.
70 listenToNativeEventForNonManagedEventTarget(
71 domEventName,
72 isCapturePhaseListener,
73 eventTarget,
74 );
75 } else {
76 throw new Error(
77 'ReactDOM.createEventHandle: setter called on an invalid ' +
78 'target. Provide a valid EventTarget or an element managed by React.',
79 );
80 }
81 }
82
83 export function createEventHandle(
84 type: string,
85 options?: EventHandleOptions,
86 ): ReactDOMEventHandle {
87 if (enableCreateEventHandleAPI) {
88 const domEventName = type as any as DOMEventName;
89
90 // We cannot support arbitrary native events with eager root listeners
91 // because the eager strategy relies on knowing the whole list ahead of time.
92 // If we wanted to support this, we'd have to add code to keep track
93 // (or search) for all portal and root containers, and lazily add listeners
94 // to them whenever we see a previously unknown event. This seems like a lot
95 // of complexity for something we don't even have a particular use case for.
96 // Unfortunately, the downside of this invariant is that *removing* a native
97 // event from the list of known events has now become a breaking change for
98 // any code relying on the createEventHandle API.
99 if (!allNativeEvents.has(domEventName)) {
100 throw new Error(
101 `Cannot call unstable_createEventHandle with "${domEventName}", as it is not an event known to React.`,
102 );
103 }
104
105 let isCapturePhaseListener = false;
106 if (options != null) {
107 const optionsCapture = options.capture;
108 if (typeof optionsCapture === 'boolean') {
109 isCapturePhaseListener = optionsCapture;
110 }
111 }
112
113 const eventHandle: ReactDOMEventHandle = (
114 target: EventTarget | ReactScopeInstance,
115 callback: SyntheticEvent => void,
116 ) => {
117 if (typeof callback !== 'function') {
118 throw new Error(
119 'ReactDOM.createEventHandle: setter called with an invalid ' +
120 'callback. The callback must be a function.',
121 );
122 }
123
124 if (!doesTargetHaveEventHandle(target, eventHandle)) {
125 addEventHandleToTarget(target, eventHandle);
126 registerReactDOMEvent(target, domEventName, isCapturePhaseListener);
127 }
128 const listener = createEventHandleListener(
129 domEventName,
130 isCapturePhaseListener,
131 callback,
132 );
133 let targetListeners = getEventHandlerListeners(target);
134 if (targetListeners === null) {
135 targetListeners = new Set();
136 setEventHandlerListeners(target, targetListeners);
137 }
138 targetListeners.add(listener);
139 return () => {
140 (targetListeners as any as Set<ReactDOMEventHandleListener>).delete(
141 listener,
142 );
143 };
144 };
145
146 return eventHandle;
147 }
148 return null as any;
149 }