main
js 319 lines 9.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 import type {TextInstance, Instance} from '../../client/ReactFiberConfigDOM';
10 import type {AnyNativeEvent} from '../PluginModuleType';
11 import type {DOMEventName} from '../DOMEventNames';
12 import type {DispatchQueue} from '../DOMPluginEventSystem';
13 import type {EventSystemFlags} from '../EventSystemFlags';
14 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
15 import type {ReactSyntheticEvent} from '../ReactSyntheticEventType';
16
17 import {registerTwoPhaseEvent} from '../EventRegistry';
18 import {SyntheticEvent} from '../SyntheticEvent';
19 import isTextInputElement from '../isTextInputElement';
20 import {canUseDOM} from 'shared/ExecutionEnvironment';
21
22 import getEventTarget from '../getEventTarget';
23 import isEventSupported from '../isEventSupported';
24 import {getNodeFromInstance} from '../../client/ReactDOMComponentTree';
25 import {updateValueIfChanged} from '../../client/inputValueTracking';
26 import {enqueueStateRestore} from '../ReactDOMControlledComponent';
27
28 import {batchedUpdates} from '../ReactDOMUpdateBatching';
29 import {
30 processDispatchQueue,
31 accumulateTwoPhaseListeners,
32 } from '../DOMPluginEventSystem';
33 import isCustomElement from '../../shared/isCustomElement';
34
35 function registerEvents() {
36 registerTwoPhaseEvent('onChange', [
37 'change',
38 'click',
39 'focusin',
40 'focusout',
41 'input',
42 'keydown',
43 'keyup',
44 'selectionchange',
45 ]);
46 }
47
48 function createAndAccumulateChangeEvent(
49 dispatchQueue: DispatchQueue,
50 inst: null | Fiber,
51 nativeEvent: AnyNativeEvent,
52 target: null | EventTarget,
53 ) {
54 // Flag this event loop as needing state restore.
55 enqueueStateRestore(target as any as Node);
56 const listeners = accumulateTwoPhaseListeners(inst, 'onChange');
57 if (listeners.length > 0) {
58 const event: ReactSyntheticEvent = new SyntheticEvent(
59 'onChange',
60 'change',
61 null,
62 nativeEvent,
63 target,
64 );
65 dispatchQueue.push({event, listeners});
66 }
67 }
68 /**
69 * For IE shims
70 */
71 let activeElement = null;
72 let activeElementInst = null;
73
74 /**
75 * SECTION: handle `change` event
76 */
77 function shouldUseChangeEvent(elem: Instance | TextInstance) {
78 const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
79 return (
80 nodeName === 'select' ||
81 (nodeName === 'input' && (elem as any).type === 'file')
82 );
83 }
84
85 function manualDispatchChangeEvent(nativeEvent: AnyNativeEvent) {
86 const dispatchQueue: DispatchQueue = [];
87 createAndAccumulateChangeEvent(
88 dispatchQueue,
89 activeElementInst,
90 nativeEvent,
91 getEventTarget(nativeEvent),
92 );
93
94 // If change and propertychange bubbled, we'd just bind to it like all the
95 // other events and have it go through ReactBrowserEventEmitter. Since it
96 // doesn't, we manually listen for the events and so we have to enqueue and
97 // process the abstract event manually.
98 //
99 // Batching is necessary here in order to ensure that all event handlers run
100 // before the next rerender (including event handlers attached to ancestor
101 // elements instead of directly on the input). Without this, controlled
102 // components don't work properly in conjunction with event bubbling because
103 // the component is rerendered and the value reverted before all the event
104 // handlers can run. See https://github.com/facebook/react/issues/708.
105 batchedUpdates(runEventInBatch, dispatchQueue);
106 }
107
108 function runEventInBatch(dispatchQueue: DispatchQueue) {
109 processDispatchQueue(dispatchQueue, 0);
110 }
111
112 function getInstIfValueChanged(targetInst: Object) {
113 const targetNode = getNodeFromInstance(targetInst);
114 if (updateValueIfChanged(targetNode as any as HTMLInputElement)) {
115 return targetInst;
116 }
117 }
118
119 function getTargetInstForChangeEvent(
120 domEventName: DOMEventName,
121 targetInst: null | Fiber,
122 ) {
123 if (domEventName === 'change') {
124 return targetInst;
125 }
126 }
127
128 /**
129 * SECTION: handle `input` event
130 */
131 let isInputEventSupported = false;
132 if (canUseDOM) {
133 // IE9 claims to support the input event but fails to trigger it when
134 // deleting text, so we ignore its input events.
135 isInputEventSupported =
136 isEventSupported('input') &&
137 (!document.documentMode || document.documentMode > 9);
138 }
139
140 /**
141 * (For IE <=9) Starts tracking propertychange events on the passed-in element
142 * and override the value property so that we can distinguish user events from
143 * value changes in JS.
144 */
145 function startWatchingForValueChange(
146 target: Instance | TextInstance,
147 targetInst: null | Fiber,
148 ) {
149 activeElement = target;
150 activeElementInst = targetInst;
151 (activeElement as any).attachEvent('onpropertychange', handlePropertyChange);
152 }
153
154 /**
155 * (For IE <=9) Removes the event listeners from the currently-tracked element,
156 * if any exists.
157 */
158 function stopWatchingForValueChange() {
159 if (!activeElement) {
160 return;
161 }
162 (activeElement as any).detachEvent('onpropertychange', handlePropertyChange);
163 activeElement = null;
164 activeElementInst = null;
165 }
166
167 /**
168 * (For IE <=9) Handles a propertychange event, sending a `change` event if
169 * the value of the active element has changed.
170 */
171 // $FlowFixMe[missing-local-annot]
172 function handlePropertyChange(nativeEvent) {
173 if (nativeEvent.propertyName !== 'value') {
174 return;
175 }
176 if (getInstIfValueChanged(activeElementInst)) {
177 manualDispatchChangeEvent(nativeEvent);
178 }
179 }
180
181 function handleEventsForInputEventPolyfill(
182 domEventName: DOMEventName,
183 target: Instance | TextInstance,
184 targetInst: null | Fiber,
185 ) {
186 if (domEventName === 'focusin') {
187 // In IE9, propertychange fires for most input events but is buggy and
188 // doesn't fire when text is deleted, but conveniently, selectionchange
189 // appears to fire in all of the remaining cases so we catch those and
190 // forward the event if the value has changed
191 // In either case, we don't want to call the event handler if the value
192 // is changed from JS so we redefine a setter for `.value` that updates
193 // our activeElementValue variable, allowing us to ignore those changes
194 //
195 // stopWatching() should be a noop here but we call it just in case we
196 // missed a blur event somehow.
197 stopWatchingForValueChange();
198 startWatchingForValueChange(target, targetInst);
199 } else if (domEventName === 'focusout') {
200 stopWatchingForValueChange();
201 }
202 }
203
204 // For IE8 and IE9.
205 function getTargetInstForInputEventPolyfill(
206 domEventName: DOMEventName,
207 targetInst: null | Fiber,
208 ) {
209 if (
210 domEventName === 'selectionchange' ||
211 domEventName === 'keyup' ||
212 domEventName === 'keydown'
213 ) {
214 // On the selectionchange event, the target is just document which isn't
215 // helpful for us so just check activeElement instead.
216 //
217 // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire
218 // propertychange on the first input event after setting `value` from a
219 // script and fires only keydown, keypress, keyup. Catching keyup usually
220 // gets it and catching keydown lets us fire an event for the first
221 // keystroke if user does a key repeat (it'll be a little delayed: right
222 // before the second keystroke). Other input methods (e.g., paste) seem to
223 // fire selectionchange normally.
224 return getInstIfValueChanged(activeElementInst);
225 }
226 }
227
228 /**
229 * SECTION: handle `click` event
230 */
231 function shouldUseClickEvent(elem: any) {
232 // Use the `click` event to detect changes to checkbox and radio inputs.
233 // This approach works across all browsers, whereas `change` does not fire
234 // until `blur` in IE8.
235 const nodeName = elem.nodeName;
236 return (
237 nodeName &&
238 nodeName.toLowerCase() === 'input' &&
239 (elem.type === 'checkbox' || elem.type === 'radio')
240 );
241 }
242
243 function getTargetInstForClickEvent(
244 domEventName: DOMEventName,
245 targetInst: null | Fiber,
246 ) {
247 if (domEventName === 'click') {
248 return getInstIfValueChanged(targetInst);
249 }
250 }
251
252 function getTargetInstForInputOrChangeEvent(
253 domEventName: DOMEventName,
254 targetInst: null | Fiber,
255 ) {
256 if (domEventName === 'input' || domEventName === 'change') {
257 return getInstIfValueChanged(targetInst);
258 }
259 }
260
261 /**
262 * This plugin creates an `onChange` event that normalizes change events
263 * across form elements. This event fires at a time when it's possible to
264 * change the element's value without seeing a flicker.
265 *
266 * Supported elements are:
267 * - input (see `isTextInputElement`)
268 * - textarea
269 * - select
270 */
271 function extractEvents(
272 dispatchQueue: DispatchQueue,
273 domEventName: DOMEventName,
274 targetInst: null | Fiber,
275 nativeEvent: AnyNativeEvent,
276 nativeEventTarget: null | EventTarget,
277 eventSystemFlags: EventSystemFlags,
278 targetContainer: null | EventTarget,
279 ) {
280 const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
281
282 let getTargetInstFunc, handleEventFunc;
283 if (shouldUseChangeEvent(targetNode)) {
284 getTargetInstFunc = getTargetInstForChangeEvent;
285 } else if (isTextInputElement(targetNode as any as HTMLElement)) {
286 if (isInputEventSupported) {
287 getTargetInstFunc = getTargetInstForInputOrChangeEvent;
288 } else {
289 getTargetInstFunc = getTargetInstForInputEventPolyfill;
290 handleEventFunc = handleEventsForInputEventPolyfill;
291 }
292 } else if (shouldUseClickEvent(targetNode)) {
293 getTargetInstFunc = getTargetInstForClickEvent;
294 } else if (
295 targetInst &&
296 isCustomElement(targetInst.elementType, targetInst.memoizedProps)
297 ) {
298 getTargetInstFunc = getTargetInstForChangeEvent;
299 }
300
301 if (getTargetInstFunc) {
302 const inst = getTargetInstFunc(domEventName, targetInst);
303 if (inst) {
304 createAndAccumulateChangeEvent(
305 dispatchQueue,
306 inst,
307 nativeEvent,
308 nativeEventTarget,
309 );
310 return;
311 }
312 }
313
314 if (handleEventFunc) {
315 handleEventFunc(domEventName, targetNode, targetInst);
316 }
317 }
318
319 export {registerEvents, extractEvents};