main
js 615 lines 17.5 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 /* eslint valid-typeof: 0 */
11
12 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
13
14 import assign from 'shared/assign';
15 import getEventCharCode from './getEventCharCode';
16
17 type EventInterfaceType = {
18 [propName: string]: 0 | ((event: {[propName: string]: mixed, ...}) => mixed),
19 };
20
21 function functionThatReturnsTrue() {
22 return true;
23 }
24
25 function functionThatReturnsFalse() {
26 return false;
27 }
28
29 // This is intentionally a factory so that we have different returned constructors.
30 // If we had a single constructor, it would be megamorphic and engines would deopt.
31 function createSyntheticEvent(Interface: EventInterfaceType) {
32 /**
33 * Synthetic events are dispatched by event plugins, typically in response to a
34 * top-level event delegation handler.
35 *
36 * These systems should generally use pooling to reduce the frequency of garbage
37 * collection. The system should check `isPersistent` to determine whether the
38 * event should be released into the pool after being dispatched. Users that
39 * need a persisted event should invoke `persist`.
40 *
41 * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
42 * normalizing browser quirks. Subclasses do not necessarily have to implement a
43 * DOM interface; custom application-specific events can also subclass this.
44 */
45 // $FlowFixMe[missing-this-annot]
46 function SyntheticBaseEvent(
47 reactName: string | null,
48 reactEventType: string,
49 targetInst: Fiber | null,
50 nativeEvent: {[propName: string]: mixed, ...},
51 nativeEventTarget: null | EventTarget,
52 ) {
53 this._reactName = reactName;
54 this._targetInst = targetInst;
55 this.type = reactEventType;
56 this.nativeEvent = nativeEvent;
57 this.target = nativeEventTarget;
58 this.currentTarget = null;
59
60 for (const propName in Interface) {
61 if (!Interface.hasOwnProperty(propName)) {
62 continue;
63 }
64 const normalize = Interface[propName];
65 if (normalize) {
66 this[propName] = normalize(nativeEvent);
67 } else {
68 this[propName] = nativeEvent[propName];
69 }
70 }
71
72 const defaultPrevented =
73 nativeEvent.defaultPrevented != null
74 ? nativeEvent.defaultPrevented
75 : nativeEvent.returnValue === false;
76 if (defaultPrevented) {
77 this.isDefaultPrevented = functionThatReturnsTrue;
78 } else {
79 this.isDefaultPrevented = functionThatReturnsFalse;
80 }
81 this.isPropagationStopped = functionThatReturnsFalse;
82 return this;
83 }
84
85 // $FlowFixMe[prop-missing] found when upgrading Flow
86 assign(SyntheticBaseEvent.prototype, {
87 // $FlowFixMe[missing-this-annot]
88 preventDefault: function () {
89 this.defaultPrevented = true;
90 const event = this.nativeEvent;
91 if (!event) {
92 return;
93 }
94
95 if (event.preventDefault) {
96 event.preventDefault();
97 // $FlowFixMe[illegal-typeof] - flow is not aware of `unknown` in IE
98 } else if (typeof event.returnValue !== 'unknown') {
99 event.returnValue = false;
100 }
101 this.isDefaultPrevented = functionThatReturnsTrue;
102 },
103
104 // $FlowFixMe[missing-this-annot]
105 stopPropagation: function () {
106 const event = this.nativeEvent;
107 if (!event) {
108 return;
109 }
110
111 if (event.stopPropagation) {
112 event.stopPropagation();
113 // $FlowFixMe[illegal-typeof] - flow is not aware of `unknown` in IE
114 } else if (typeof event.cancelBubble !== 'unknown') {
115 // The ChangeEventPlugin registers a "propertychange" event for
116 // IE. This event does not support bubbling or cancelling, and
117 // any references to cancelBubble throw "Member not found". A
118 // typeof check of "unknown" circumvents this issue (and is also
119 // IE specific).
120 event.cancelBubble = true;
121 }
122
123 this.isPropagationStopped = functionThatReturnsTrue;
124 },
125
126 /**
127 * We release all dispatched `SyntheticEvent`s after each event loop, adding
128 * them back into the pool. This allows a way to hold onto a reference that
129 * won't be added back into the pool.
130 */
131 persist: function () {
132 // Modern event system doesn't use pooling.
133 },
134
135 /**
136 * Checks if this event should be released back into the pool.
137 *
138 * @return {boolean} True if this should not be released, false otherwise.
139 */
140 isPersistent: functionThatReturnsTrue,
141 });
142 return SyntheticBaseEvent;
143 }
144
145 /**
146 * @interface Event
147 * @see http://www.w3.org/TR/DOM-Level-3-Events/
148 */
149 const EventInterface: EventInterfaceType = {
150 eventPhase: 0,
151 bubbles: 0,
152 cancelable: 0,
153 timeStamp: function (event: {[propName: string]: mixed}) {
154 return event.timeStamp || Date.now();
155 },
156 defaultPrevented: 0,
157 isTrusted: 0,
158 };
159 export const SyntheticEvent: $FlowFixMe = createSyntheticEvent(EventInterface);
160
161 const UIEventInterface: EventInterfaceType = {
162 ...EventInterface,
163 view: 0,
164 detail: 0,
165 };
166 export const SyntheticUIEvent: $FlowFixMe =
167 createSyntheticEvent(UIEventInterface);
168
169 let lastMovementX;
170 let lastMovementY;
171 let lastMouseEvent: ?{[propName: string]: mixed};
172
173 function updateMouseMovementPolyfillState(event: {[propName: string]: mixed}) {
174 if (event !== lastMouseEvent) {
175 if (lastMouseEvent && event.type === 'mousemove') {
176 // $FlowFixMe[unsafe-arithmetic] assuming this is a number
177 lastMovementX = event.screenX - lastMouseEvent.screenX;
178 // $FlowFixMe[unsafe-arithmetic] assuming this is a number
179 lastMovementY = event.screenY - lastMouseEvent.screenY;
180 } else {
181 lastMovementX = 0;
182 lastMovementY = 0;
183 }
184 lastMouseEvent = event;
185 }
186 }
187
188 /**
189 * @interface MouseEvent
190 * @see http://www.w3.org/TR/DOM-Level-3-Events/
191 */
192 const MouseEventInterface: EventInterfaceType = {
193 ...UIEventInterface,
194 screenX: 0,
195 screenY: 0,
196 clientX: 0,
197 clientY: 0,
198 pageX: 0,
199 pageY: 0,
200 ctrlKey: 0,
201 shiftKey: 0,
202 altKey: 0,
203 metaKey: 0,
204 getModifierState: getEventModifierState,
205 button: 0,
206 buttons: 0,
207 relatedTarget: function (event) {
208 if (event.relatedTarget === undefined)
209 return event.fromElement === event.srcElement
210 ? event.toElement
211 : event.fromElement;
212
213 return event.relatedTarget;
214 },
215 movementX: function (event) {
216 if ('movementX' in event) {
217 return event.movementX;
218 }
219 updateMouseMovementPolyfillState(event);
220 return lastMovementX;
221 },
222 movementY: function (event) {
223 if ('movementY' in event) {
224 return event.movementY;
225 }
226 // Don't need to call updateMouseMovementPolyfillState() here
227 // because it's guaranteed to have already run when movementX
228 // was copied.
229 return lastMovementY;
230 },
231 };
232
233 export const SyntheticMouseEvent: $FlowFixMe =
234 createSyntheticEvent(MouseEventInterface);
235
236 /**
237 * @interface DragEvent
238 * @see http://www.w3.org/TR/DOM-Level-3-Events/
239 */
240 const DragEventInterface: EventInterfaceType = {
241 ...MouseEventInterface,
242 dataTransfer: 0,
243 };
244 export const SyntheticDragEvent: $FlowFixMe =
245 createSyntheticEvent(DragEventInterface);
246
247 /**
248 * @interface FocusEvent
249 * @see http://www.w3.org/TR/DOM-Level-3-Events/
250 */
251 const FocusEventInterface: EventInterfaceType = {
252 ...UIEventInterface,
253 relatedTarget: 0,
254 };
255 export const SyntheticFocusEvent: $FlowFixMe =
256 createSyntheticEvent(FocusEventInterface);
257
258 /**
259 * @interface Event
260 * @see http://www.w3.org/TR/css3-animations/#AnimationEvent-interface
261 * @see https://developer.mozilla.org/en-US/docs/Web/API/AnimationEvent
262 */
263 const AnimationEventInterface: EventInterfaceType = {
264 ...EventInterface,
265 animationName: 0,
266 elapsedTime: 0,
267 pseudoElement: 0,
268 };
269 export const SyntheticAnimationEvent: $FlowFixMe = createSyntheticEvent(
270 AnimationEventInterface,
271 );
272
273 /**
274 * @interface Event
275 * @see http://www.w3.org/TR/clipboard-apis/
276 */
277 const ClipboardEventInterface: EventInterfaceType = {
278 ...EventInterface,
279 clipboardData: function (event) {
280 return 'clipboardData' in event
281 ? event.clipboardData
282 : window.clipboardData;
283 },
284 };
285 export const SyntheticClipboardEvent: $FlowFixMe = createSyntheticEvent(
286 ClipboardEventInterface,
287 );
288
289 /**
290 * @interface Event
291 * @see http://www.w3.org/TR/DOM-Level-3-Events/#events-compositionevents
292 */
293 const CompositionEventInterface: EventInterfaceType = {
294 ...EventInterface,
295 data: 0,
296 };
297 export const SyntheticCompositionEvent: $FlowFixMe = createSyntheticEvent(
298 CompositionEventInterface,
299 );
300
301 /**
302 * @interface Event
303 * @see http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105
304 * /#events-inputevents
305 */
306 // Happens to share the same list for now.
307 export const SyntheticInputEvent = SyntheticCompositionEvent;
308
309 /**
310 * Normalization of deprecated HTML5 `key` values
311 * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
312 */
313 const normalizeKey = {
314 Esc: 'Escape',
315 Spacebar: ' ',
316 Left: 'ArrowLeft',
317 Up: 'ArrowUp',
318 Right: 'ArrowRight',
319 Down: 'ArrowDown',
320 Del: 'Delete',
321 Win: 'OS',
322 Menu: 'ContextMenu',
323 Apps: 'ContextMenu',
324 Scroll: 'ScrollLock',
325 MozPrintableKey: 'Unidentified',
326 };
327
328 /**
329 * Translation from legacy `keyCode` to HTML5 `key`
330 * Only special keys supported, all others depend on keyboard layout or browser
331 * @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Key_names
332 */
333 const translateToKey = {
334 '8': 'Backspace',
335 '9': 'Tab',
336 '12': 'Clear',
337 '13': 'Enter',
338 '16': 'Shift',
339 '17': 'Control',
340 '18': 'Alt',
341 '19': 'Pause',
342 '20': 'CapsLock',
343 '27': 'Escape',
344 '32': ' ',
345 '33': 'PageUp',
346 '34': 'PageDown',
347 '35': 'End',
348 '36': 'Home',
349 '37': 'ArrowLeft',
350 '38': 'ArrowUp',
351 '39': 'ArrowRight',
352 '40': 'ArrowDown',
353 '45': 'Insert',
354 '46': 'Delete',
355 '112': 'F1',
356 '113': 'F2',
357 '114': 'F3',
358 '115': 'F4',
359 '116': 'F5',
360 '117': 'F6',
361 '118': 'F7',
362 '119': 'F8',
363 '120': 'F9',
364 '121': 'F10',
365 '122': 'F11',
366 '123': 'F12',
367 '144': 'NumLock',
368 '145': 'ScrollLock',
369 '224': 'Meta',
370 };
371
372 /**
373 * @param {object} nativeEvent Native browser event.
374 * @return {string} Normalized `key` property.
375 */
376 function getEventKey(nativeEvent: {[propName: string]: mixed}) {
377 if (nativeEvent.key) {
378 // Normalize inconsistent values reported by browsers due to
379 // implementations of a working draft specification.
380
381 // FireFox implements `key` but returns `MozPrintableKey` for all
382 // printable characters (normalized to `Unidentified`), ignore it.
383 const key =
384 // $FlowFixMe[invalid-computed-prop] unable to index with a `mixed` value
385 normalizeKey[nativeEvent.key] || nativeEvent.key;
386 if (key !== 'Unidentified') {
387 return key;
388 }
389 }
390
391 // Browser does not implement `key`, polyfill as much of it as we can.
392 if (nativeEvent.type === 'keypress') {
393 const charCode = getEventCharCode(
394 // $FlowFixMe[incompatible-type] unable to narrow to `KeyboardEvent`
395 nativeEvent,
396 );
397
398 // The enter-key is technically both printable and non-printable and can
399 // thus be captured by `keypress`, no other non-printable key should.
400 return charCode === 13 ? 'Enter' : String.fromCharCode(charCode);
401 }
402 if (nativeEvent.type === 'keydown' || nativeEvent.type === 'keyup') {
403 // While user keyboard layout determines the actual meaning of each
404 // `keyCode` value, almost all function keys have a universal value.
405 // $FlowFixMe[invalid-computed-prop] unable to index with a `mixed` value
406 return translateToKey[nativeEvent.keyCode] || 'Unidentified';
407 }
408 return '';
409 }
410
411 /**
412 * Translation from modifier key to the associated property in the event.
413 * @see http://www.w3.org/TR/DOM-Level-3-Events/#keys-Modifiers
414 */
415 const modifierKeyToProp = {
416 Alt: 'altKey',
417 Control: 'ctrlKey',
418 Meta: 'metaKey',
419 Shift: 'shiftKey',
420 };
421
422 // Older browsers (Safari <= 10, iOS Safari <= 10.2) do not support
423 // getModifierState. If getModifierState is not supported, we map it to a set of
424 // modifier keys exposed by the event. In this case, Lock-keys are not supported.
425 // $FlowFixMe[missing-local-annot]
426 // $FlowFixMe[missing-this-annot]
427 function modifierStateGetter(keyArg) {
428 const syntheticEvent = this;
429 const nativeEvent = syntheticEvent.nativeEvent;
430 if (nativeEvent.getModifierState) {
431 return nativeEvent.getModifierState(keyArg);
432 }
433 const keyProp = modifierKeyToProp[keyArg];
434 return keyProp ? !!nativeEvent[keyProp] : false;
435 }
436
437 function getEventModifierState(nativeEvent: {[propName: string]: mixed}) {
438 return modifierStateGetter;
439 }
440
441 /**
442 * @interface KeyboardEvent
443 * @see http://www.w3.org/TR/DOM-Level-3-Events/
444 */
445 const KeyboardEventInterface: EventInterfaceType = {
446 ...UIEventInterface,
447 key: getEventKey,
448 code: 0,
449 location: 0,
450 ctrlKey: 0,
451 shiftKey: 0,
452 altKey: 0,
453 metaKey: 0,
454 repeat: 0,
455 locale: 0,
456 getModifierState: getEventModifierState,
457 // Legacy Interface
458 charCode: function (event: {[propName: string]: mixed}) {
459 // `charCode` is the result of a KeyPress event and represents the value of
460 // the actual printable character.
461
462 // KeyPress is deprecated, but its replacement is not yet final and not
463 // implemented in any major browser. Only KeyPress has charCode.
464 if (event.type === 'keypress') {
465 return getEventCharCode(
466 // $FlowFixMe[incompatible-type] unable to narrow to `KeyboardEvent`
467 event,
468 );
469 }
470 return 0;
471 },
472 keyCode: function (event: {[propName: string]: mixed}) {
473 // `keyCode` is the result of a KeyDown/Up event and represents the value of
474 // physical keyboard key.
475
476 // The actual meaning of the value depends on the users' keyboard layout
477 // which cannot be detected. Assuming that it is a US keyboard layout
478 // provides a surprisingly accurate mapping for US and European users.
479 // Due to this, it is left to the user to implement at this time.
480 if (event.type === 'keydown' || event.type === 'keyup') {
481 return event.keyCode;
482 }
483 return 0;
484 },
485 which: function (event: {[propName: string]: mixed}) {
486 // `which` is an alias for either `keyCode` or `charCode` depending on the
487 // type of the event.
488 if (event.type === 'keypress') {
489 return getEventCharCode(
490 // $FlowFixMe[incompatible-type] unable to narrow to `KeyboardEvent`
491 event,
492 );
493 }
494 if (event.type === 'keydown' || event.type === 'keyup') {
495 return event.keyCode;
496 }
497 return 0;
498 },
499 };
500 export const SyntheticKeyboardEvent: $FlowFixMe = createSyntheticEvent(
501 KeyboardEventInterface,
502 );
503
504 /**
505 * @interface PointerEvent
506 * @see http://www.w3.org/TR/pointerevents/
507 */
508 const PointerEventInterface: EventInterfaceType = {
509 ...MouseEventInterface,
510 pointerId: 0,
511 width: 0,
512 height: 0,
513 pressure: 0,
514 tangentialPressure: 0,
515 tiltX: 0,
516 tiltY: 0,
517 twist: 0,
518 pointerType: 0,
519 isPrimary: 0,
520 };
521 export const SyntheticPointerEvent: $FlowFixMe = createSyntheticEvent(
522 PointerEventInterface,
523 );
524
525 /**
526 * @interface SubmitEvent
527 * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#the-submitevent-interface
528 */
529 const SubmitEventInterface: EventInterfaceType = {
530 ...EventInterface,
531 submitter: 0,
532 };
533 export const SyntheticSubmitEvent: $FlowFixMe =
534 createSyntheticEvent(SubmitEventInterface);
535
536 /**
537 * @interface TouchEvent
538 * @see http://www.w3.org/TR/touch-events/
539 */
540 const TouchEventInterface: EventInterfaceType = {
541 ...UIEventInterface,
542 touches: 0,
543 targetTouches: 0,
544 changedTouches: 0,
545 altKey: 0,
546 metaKey: 0,
547 ctrlKey: 0,
548 shiftKey: 0,
549 getModifierState: getEventModifierState,
550 };
551 export const SyntheticTouchEvent: $FlowFixMe =
552 createSyntheticEvent(TouchEventInterface);
553
554 /**
555 * @interface Event
556 * @see http://www.w3.org/TR/2009/WD-css3-transitions-20090320/#transition-events-
557 * @see https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent
558 */
559 const TransitionEventInterface: EventInterfaceType = {
560 ...EventInterface,
561 propertyName: 0,
562 elapsedTime: 0,
563 pseudoElement: 0,
564 };
565 export const SyntheticTransitionEvent: $FlowFixMe = createSyntheticEvent(
566 TransitionEventInterface,
567 );
568
569 /**
570 * @interface WheelEvent
571 * @see http://www.w3.org/TR/DOM-Level-3-Events/
572 */
573 const WheelEventInterface: EventInterfaceType = {
574 ...MouseEventInterface,
575 deltaX(event: {[propName: string]: mixed}) {
576 return 'deltaX' in event
577 ? event.deltaX
578 : // Fallback to `wheelDeltaX` for Webkit and normalize (right is positive).
579 'wheelDeltaX' in event
580 ? // $FlowFixMe[unsafe-arithmetic] assuming this is a number
581 -event.wheelDeltaX
582 : 0;
583 },
584 deltaY(event: {[propName: string]: mixed}) {
585 return 'deltaY' in event
586 ? event.deltaY
587 : // Fallback to `wheelDeltaY` for Webkit and normalize (down is positive).
588 'wheelDeltaY' in event
589 ? // $FlowFixMe[unsafe-arithmetic] assuming this is a number
590 -event.wheelDeltaY
591 : // Fallback to `wheelDelta` for IE<9 and normalize (down is positive).
592 'wheelDelta' in event
593 ? // $FlowFixMe[unsafe-arithmetic] assuming this is a number
594 -event.wheelDelta
595 : 0;
596 },
597 deltaZ: 0,
598
599 // Browsers without "deltaMode" is reporting in raw wheel delta where one
600 // notch on the scroll is always +/- 120, roughly equivalent to pixels.
601 // A good approximation of DOM_DELTA_LINE (1) is 5% of viewport size or
602 // ~40 pixels, for DOM_DELTA_SCREEN (2) it is 87.5% of viewport size.
603 deltaMode: 0,
604 };
605 export const SyntheticWheelEvent: $FlowFixMe =
606 createSyntheticEvent(WheelEventInterface);
607
608 const ToggleEventInterface: EventInterfaceType = {
609 ...EventInterface,
610 newState: 0,
611 oldState: 0,
612 source: 0,
613 };
614 export const SyntheticToggleEvent: $FlowFixMe =
615 createSyntheticEvent(ToggleEventInterface);