main
js 1,010 lines 30.6 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 './DOMEventNames';
11 import type {EventSystemFlags} from './EventSystemFlags';
12 import type {AnyNativeEvent} from './PluginModuleType';
13 import type {
14 KnownReactSyntheticEvent,
15 ReactSyntheticEvent,
16 } from './ReactSyntheticEventType';
17 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
18
19 import {allNativeEvents} from './EventRegistry';
20 import {
21 SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE,
22 IS_LEGACY_FB_SUPPORT_MODE,
23 SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS,
24 IS_CAPTURE_PHASE,
25 IS_EVENT_HANDLE_NON_MANAGED_NODE,
26 IS_NON_DELEGATED,
27 } from './EventSystemFlags';
28 import {isReplayingEvent} from './CurrentReplayingEvent';
29
30 import {
31 HostRoot,
32 HostPortal,
33 HostComponent,
34 HostHoistable,
35 HostSingleton,
36 HostText,
37 ScopeComponent,
38 } from 'react-reconciler/src/ReactWorkTags';
39 import {getLowestCommonAncestor} from 'react-reconciler/src/ReactFiberTreeReflection';
40
41 import getEventTarget from './getEventTarget';
42 import {
43 getClosestInstanceFromNode,
44 getEventListenerSet,
45 getEventHandlerListeners,
46 } from '../client/ReactDOMComponentTree';
47 import {COMMENT_NODE, DOCUMENT_NODE} from '../client/HTMLNodeType';
48 import {batchedUpdates} from './ReactDOMUpdateBatching';
49 import getListener from './getListener';
50 import {passiveBrowserEventsSupported} from './checkPassiveEvents';
51
52 import {
53 enableLegacyFBSupport,
54 enableCreateEventHandleAPI,
55 enableScopeAPI,
56 disableCommentsAsDOMContainers,
57 enableScrollEndPolyfill,
58 } from 'shared/ReactFeatureFlags';
59 import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
60 import {
61 removeEventListener,
62 addEventCaptureListener,
63 addEventBubbleListener,
64 addEventBubbleListenerWithPassiveFlag,
65 addEventCaptureListenerWithPassiveFlag,
66 } from './EventListener';
67 import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
68 import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
69 import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
70 import * as SelectEventPlugin from './plugins/SelectEventPlugin';
71 import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
72 import * as FormActionEventPlugin from './plugins/FormActionEventPlugin';
73 import * as ScrollEndEventPlugin from './plugins/ScrollEndEventPlugin';
74
75 import reportGlobalError from 'shared/reportGlobalError';
76
77 import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
78
79 type DispatchListener = {
80 instance: null | Fiber,
81 listener: Function,
82 currentTarget: EventTarget,
83 };
84
85 type DispatchEntry = {
86 event: ReactSyntheticEvent,
87 listeners: Array<DispatchListener>,
88 };
89
90 export type DispatchQueue = Array<DispatchEntry>;
91
92 // TODO: remove top-level side effect.
93 SimpleEventPlugin.registerEvents();
94 EnterLeaveEventPlugin.registerEvents();
95 ChangeEventPlugin.registerEvents();
96 SelectEventPlugin.registerEvents();
97 BeforeInputEventPlugin.registerEvents();
98 if (enableScrollEndPolyfill) {
99 ScrollEndEventPlugin.registerEvents();
100 }
101
102 function extractEvents(
103 dispatchQueue: DispatchQueue,
104 domEventName: DOMEventName,
105 targetInst: null | Fiber,
106 nativeEvent: AnyNativeEvent,
107 nativeEventTarget: null | EventTarget,
108 eventSystemFlags: EventSystemFlags,
109 targetContainer: EventTarget,
110 ) {
111 // TODO: we should remove the concept of a "SimpleEventPlugin".
112 // This is the basic functionality of the event system. All
113 // the other plugins are essentially polyfills. So the plugin
114 // should probably be inlined somewhere and have its logic
115 // be core the to event system. This would potentially allow
116 // us to ship builds of React without the polyfilled plugins below.
117 SimpleEventPlugin.extractEvents(
118 dispatchQueue,
119 domEventName,
120 targetInst,
121 nativeEvent,
122 nativeEventTarget,
123 eventSystemFlags,
124 targetContainer,
125 );
126 const shouldProcessPolyfillPlugins =
127 (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
128 // We don't process these events unless we are in the
129 // event's native "bubble" phase, which means that we're
130 // not in the capture phase. That's because we emulate
131 // the capture phase here still. This is a trade-off,
132 // because in an ideal world we would not emulate and use
133 // the phases properly, like we do with the SimpleEvent
134 // plugin. However, the plugins below either expect
135 // emulation (EnterLeave) or use state localized to that
136 // plugin (BeforeInput, Change, Select). The state in
137 // these modules complicates things, as you'll essentially
138 // get the case where the capture phase event might change
139 // state, only for the following bubble event to come in
140 // later and not trigger anything as the state now
141 // invalidates the heuristics of the event plugin. We
142 // could alter all these plugins to work in such ways, but
143 // that might cause other unknown side-effects that we
144 // can't foresee right now.
145 if (shouldProcessPolyfillPlugins) {
146 EnterLeaveEventPlugin.extractEvents(
147 dispatchQueue,
148 domEventName,
149 targetInst,
150 nativeEvent,
151 nativeEventTarget,
152 eventSystemFlags,
153 targetContainer,
154 );
155 ChangeEventPlugin.extractEvents(
156 dispatchQueue,
157 domEventName,
158 targetInst,
159 nativeEvent,
160 nativeEventTarget,
161 eventSystemFlags,
162 targetContainer,
163 );
164 SelectEventPlugin.extractEvents(
165 dispatchQueue,
166 domEventName,
167 targetInst,
168 nativeEvent,
169 nativeEventTarget,
170 eventSystemFlags,
171 targetContainer,
172 );
173 BeforeInputEventPlugin.extractEvents(
174 dispatchQueue,
175 domEventName,
176 targetInst,
177 nativeEvent,
178 nativeEventTarget,
179 eventSystemFlags,
180 targetContainer,
181 );
182 FormActionEventPlugin.extractEvents(
183 dispatchQueue,
184 domEventName,
185 targetInst,
186 nativeEvent,
187 nativeEventTarget,
188 eventSystemFlags,
189 targetContainer,
190 );
191 }
192 if (enableScrollEndPolyfill) {
193 ScrollEndEventPlugin.extractEvents(
194 dispatchQueue,
195 domEventName,
196 targetInst,
197 nativeEvent,
198 nativeEventTarget,
199 eventSystemFlags,
200 targetContainer,
201 );
202 }
203 }
204
205 // List of events that need to be individually attached to media elements.
206 export const mediaEventTypes: Array<DOMEventName> = [
207 'abort',
208 'canplay',
209 'canplaythrough',
210 'durationchange',
211 'emptied',
212 'encrypted',
213 'ended',
214 'error',
215 'loadeddata',
216 'loadedmetadata',
217 'loadstart',
218 'pause',
219 'play',
220 'playing',
221 'progress',
222 'ratechange',
223 'resize',
224 'seeked',
225 'seeking',
226 'stalled',
227 'suspend',
228 'timeupdate',
229 'volumechange',
230 'waiting',
231 ];
232
233 // We should not delegate these events to the container, but rather
234 // set them on the actual target element itself. This is primarily
235 // because these events do not consistently bubble in the DOM.
236 export const nonDelegatedEvents: Set<DOMEventName> = new Set([
237 'beforetoggle',
238 'cancel',
239 'close',
240 'invalid',
241 'load',
242 'scroll',
243 'scrollend',
244 'toggle',
245 // In order to reduce bytes, we insert the above array of media events
246 // into this Set. Note: the "error" event isn't an exclusive media event,
247 // and can occur on other elements too. Rather than duplicate that event,
248 // we just take it from the media events array.
249 ...mediaEventTypes,
250 ]);
251
252 function executeDispatch(
253 event: ReactSyntheticEvent,
254 listener: Function,
255 currentTarget: EventTarget,
256 ): void {
257 event.currentTarget = currentTarget;
258 try {
259 listener(event);
260 } catch (error) {
261 reportGlobalError(error);
262 }
263 event.currentTarget = null;
264 }
265
266 function processDispatchQueueItemsInOrder(
267 event: ReactSyntheticEvent,
268 dispatchListeners: Array<DispatchListener>,
269 inCapturePhase: boolean,
270 ): void {
271 let previousInstance;
272 if (inCapturePhase) {
273 for (let i = dispatchListeners.length - 1; i >= 0; i--) {
274 const {instance, currentTarget, listener} = dispatchListeners[i];
275 if (instance !== previousInstance && event.isPropagationStopped()) {
276 return;
277 }
278 if (__DEV__ && instance !== null) {
279 runWithFiberInDEV(
280 instance,
281 executeDispatch,
282 event,
283 listener,
284 currentTarget,
285 );
286 } else {
287 executeDispatch(event, listener, currentTarget);
288 }
289 previousInstance = instance;
290 }
291 } else {
292 for (let i = 0; i < dispatchListeners.length; i++) {
293 const {instance, currentTarget, listener} = dispatchListeners[i];
294 if (instance !== previousInstance && event.isPropagationStopped()) {
295 return;
296 }
297 if (__DEV__ && instance !== null) {
298 runWithFiberInDEV(
299 instance,
300 executeDispatch,
301 event,
302 listener,
303 currentTarget,
304 );
305 } else {
306 executeDispatch(event, listener, currentTarget);
307 }
308 previousInstance = instance;
309 }
310 }
311 }
312
313 export function processDispatchQueue(
314 dispatchQueue: DispatchQueue,
315 eventSystemFlags: EventSystemFlags,
316 ): void {
317 const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
318 for (let i = 0; i < dispatchQueue.length; i++) {
319 const {event, listeners} = dispatchQueue[i];
320 processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
321 // event system doesn't use pooling.
322 }
323 }
324
325 function dispatchEventsForPlugins(
326 domEventName: DOMEventName,
327 eventSystemFlags: EventSystemFlags,
328 nativeEvent: AnyNativeEvent,
329 targetInst: null | Fiber,
330 targetContainer: EventTarget,
331 ): void {
332 const nativeEventTarget = getEventTarget(nativeEvent);
333 const dispatchQueue: DispatchQueue = [];
334 extractEvents(
335 dispatchQueue,
336 domEventName,
337 targetInst,
338 nativeEvent,
339 nativeEventTarget,
340 eventSystemFlags,
341 targetContainer,
342 );
343 processDispatchQueue(dispatchQueue, eventSystemFlags);
344 }
345
346 export function listenToNonDelegatedEvent(
347 domEventName: DOMEventName,
348 targetElement: Element,
349 ): void {
350 if (__DEV__) {
351 if (!nonDelegatedEvents.has(domEventName)) {
352 console.error(
353 'Did not expect a listenToNonDelegatedEvent() call for "%s". ' +
354 'This is a bug in React. Please file an issue.',
355 domEventName,
356 );
357 }
358 }
359 const isCapturePhaseListener = false;
360 const listenerSet = getEventListenerSet(targetElement);
361 const listenerSetKey = getListenerSetKey(
362 domEventName,
363 isCapturePhaseListener,
364 );
365 if (!listenerSet.has(listenerSetKey)) {
366 addTrappedEventListener(
367 targetElement,
368 domEventName,
369 IS_NON_DELEGATED,
370 isCapturePhaseListener,
371 );
372 listenerSet.add(listenerSetKey);
373 }
374 }
375
376 export function listenToNativeEvent(
377 domEventName: DOMEventName,
378 isCapturePhaseListener: boolean,
379 target: EventTarget,
380 ): void {
381 if (__DEV__) {
382 if (nonDelegatedEvents.has(domEventName) && !isCapturePhaseListener) {
383 console.error(
384 'Did not expect a listenToNativeEvent() call for "%s" in the bubble phase. ' +
385 'This is a bug in React. Please file an issue.',
386 domEventName,
387 );
388 }
389 }
390
391 let eventSystemFlags = 0;
392 if (isCapturePhaseListener) {
393 eventSystemFlags |= IS_CAPTURE_PHASE;
394 }
395 addTrappedEventListener(
396 target,
397 domEventName,
398 eventSystemFlags,
399 isCapturePhaseListener,
400 );
401 }
402
403 // This is only used by createEventHandle when the
404 // target is not a DOM element. E.g. window.
405 export function listenToNativeEventForNonManagedEventTarget(
406 domEventName: DOMEventName,
407 isCapturePhaseListener: boolean,
408 target: EventTarget,
409 ): void {
410 let eventSystemFlags: number = IS_EVENT_HANDLE_NON_MANAGED_NODE;
411 const listenerSet = getEventListenerSet(target);
412 const listenerSetKey = getListenerSetKey(
413 domEventName,
414 isCapturePhaseListener,
415 );
416 if (!listenerSet.has(listenerSetKey)) {
417 if (isCapturePhaseListener) {
418 eventSystemFlags |= IS_CAPTURE_PHASE;
419 }
420 addTrappedEventListener(
421 target,
422 domEventName,
423 eventSystemFlags,
424 isCapturePhaseListener,
425 );
426 listenerSet.add(listenerSetKey);
427 }
428 }
429
430 const listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
431
432 export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
433 if (!(rootContainerElement as any)[listeningMarker]) {
434 (rootContainerElement as any)[listeningMarker] = true;
435 allNativeEvents.forEach(domEventName => {
436 // We handle selectionchange separately because it
437 // doesn't bubble and needs to be on the document.
438 if (domEventName !== 'selectionchange') {
439 if (!nonDelegatedEvents.has(domEventName)) {
440 listenToNativeEvent(domEventName, false, rootContainerElement);
441 }
442 listenToNativeEvent(domEventName, true, rootContainerElement);
443 }
444 });
445 const ownerDocument =
446 (rootContainerElement as any).nodeType === DOCUMENT_NODE
447 ? rootContainerElement
448 : (rootContainerElement as any).ownerDocument;
449 // $FlowFixMe[invalid-compare]
450 if (ownerDocument !== null) {
451 // The selectionchange event also needs deduplication
452 // but it is attached to the document.
453 if (!(ownerDocument as any)[listeningMarker]) {
454 (ownerDocument as any)[listeningMarker] = true;
455 listenToNativeEvent('selectionchange', false, ownerDocument);
456 }
457 }
458 }
459 }
460
461 function addTrappedEventListener(
462 targetContainer: EventTarget,
463 domEventName: DOMEventName,
464 eventSystemFlags: EventSystemFlags,
465 isCapturePhaseListener: boolean,
466 isDeferredListenerForLegacyFBSupport?: boolean,
467 ) {
468 let listener = createEventListenerWrapperWithPriority(
469 targetContainer,
470 domEventName,
471 eventSystemFlags,
472 );
473 // If passive option is not supported, then the event will be
474 // active and not passive.
475 let isPassiveListener: void | boolean = undefined;
476 if (passiveBrowserEventsSupported) {
477 // Browsers introduced an intervention, making these events
478 // passive by default on document. React doesn't bind them
479 // to document anymore, but changing this now would undo
480 // the performance wins from the change. So we emulate
481 // the existing behavior manually on the roots now.
482 // https://github.com/facebook/react/issues/19651
483 if (
484 domEventName === 'touchstart' ||
485 domEventName === 'touchmove' ||
486 domEventName === 'wheel'
487 ) {
488 isPassiveListener = true;
489 }
490 }
491
492 targetContainer =
493 enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
494 ? // A Document container's ownerDocument is null, so it must be used
495 // as the deferral target itself.
496 (targetContainer as any).nodeType === DOCUMENT_NODE
497 ? targetContainer
498 : (targetContainer as any).ownerDocument
499 : targetContainer;
500
501 let unsubscribeListener;
502 // When legacyFBSupport is enabled, it's for when we
503 // want to add a one time event listener to a container.
504 // This should only be used with enableLegacyFBSupport
505 // due to requirement to provide compatibility with
506 // internal FB www event tooling. This works by removing
507 // the event listener as soon as it is invoked. We could
508 // also attempt to use the {once: true} param on
509 // addEventListener, but that requires support and some
510 // browsers do not support this today, and given this is
511 // to support legacy code patterns, it's likely they'll
512 // need support for such browsers.
513 if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
514 const originalListener = listener;
515 // $FlowFixMe[missing-this-annot]
516 listener = function (...p) {
517 removeEventListener(
518 targetContainer,
519 domEventName,
520 unsubscribeListener,
521 isCapturePhaseListener,
522 );
523 return originalListener.apply(this, p);
524 };
525 }
526 // TODO: There are too many combinations here. Consolidate them.
527 if (isCapturePhaseListener) {
528 if (isPassiveListener !== undefined) {
529 unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
530 targetContainer,
531 domEventName,
532 listener,
533 isPassiveListener,
534 );
535 } else {
536 unsubscribeListener = addEventCaptureListener(
537 targetContainer,
538 domEventName,
539 listener,
540 );
541 }
542 } else {
543 if (isPassiveListener !== undefined) {
544 unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
545 targetContainer,
546 domEventName,
547 listener,
548 isPassiveListener,
549 );
550 } else {
551 unsubscribeListener = addEventBubbleListener(
552 targetContainer,
553 domEventName,
554 listener,
555 );
556 }
557 }
558 }
559
560 function deferClickToDocumentForLegacyFBSupport(
561 domEventName: DOMEventName,
562 targetContainer: EventTarget,
563 ): void {
564 // We defer all click events with legacy FB support mode on.
565 // This means we add a one time event listener to trigger
566 // after the FB delegated listeners fire.
567 const isDeferredListenerForLegacyFBSupport = true;
568 addTrappedEventListener(
569 targetContainer,
570 domEventName,
571 IS_LEGACY_FB_SUPPORT_MODE,
572 false,
573 isDeferredListenerForLegacyFBSupport,
574 );
575 }
576
577 function isMatchingRootContainer(
578 grandContainer: Element,
579 targetContainer: EventTarget,
580 ): boolean {
581 return (
582 grandContainer === targetContainer ||
583 (!disableCommentsAsDOMContainers &&
584 grandContainer.nodeType === COMMENT_NODE &&
585 grandContainer.parentNode === targetContainer)
586 );
587 }
588
589 export function dispatchEventForPluginEventSystem(
590 domEventName: DOMEventName,
591 eventSystemFlags: EventSystemFlags,
592 nativeEvent: AnyNativeEvent,
593 targetInst: null | Fiber,
594 targetContainer: EventTarget,
595 ): void {
596 let ancestorInst = targetInst;
597 if (
598 (eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
599 (eventSystemFlags & IS_NON_DELEGATED) === 0
600 ) {
601 const targetContainerNode = targetContainer as any as Node;
602
603 // If we are using the legacy FB support flag, we
604 // defer the event to the null with a one
605 // time event listener so we can defer the event.
606 if (
607 enableLegacyFBSupport &&
608 // If our event flags match the required flags for entering
609 // FB legacy mode and we are processing the "click" event,
610 // then we can defer the event to the "document", to allow
611 // for legacy FB support, where the expected behavior was to
612 // match React < 16 behavior of delegated clicks to the doc.
613 domEventName === 'click' &&
614 (eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
615 !isReplayingEvent(nativeEvent)
616 ) {
617 deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
618 return;
619 }
620 if (targetInst !== null) {
621 // The below logic attempts to work out if we need to change
622 // the target fiber to a different ancestor. We had similar logic
623 // in the legacy event system, except the big difference between
624 // systems is that the modern event system now has an event listener
625 // attached to each React Root and React Portal Root. Together,
626 // the DOM nodes representing these roots are the "rootContainer".
627 // To figure out which ancestor instance we should use, we traverse
628 // up the fiber tree from the target instance and attempt to find
629 // root boundaries that match that of our current "rootContainer".
630 // If we find that "rootContainer", we find the parent fiber
631 // sub-tree for that root and make that our ancestor instance.
632 let node: null | Fiber = targetInst;
633
634 mainLoop: while (true) {
635 if (node === null) {
636 return;
637 }
638 const nodeTag = node.tag;
639 if (nodeTag === HostRoot || nodeTag === HostPortal) {
640 let container = node.stateNode.containerInfo;
641 if (isMatchingRootContainer(container, targetContainerNode)) {
642 break;
643 }
644 if (nodeTag === HostPortal) {
645 // The target is a portal, but it's not the rootContainer we're looking for.
646 // Normally portals handle their own events all the way down to the root.
647 // So we should be able to stop now. However, we don't know if this portal
648 // was part of *our* root.
649 let grandNode = node.return;
650 while (grandNode !== null) {
651 const grandTag = grandNode.tag;
652 if (grandTag === HostRoot || grandTag === HostPortal) {
653 const grandContainer = grandNode.stateNode.containerInfo;
654 if (
655 isMatchingRootContainer(grandContainer, targetContainerNode)
656 ) {
657 // This is the rootContainer we're looking for and we found it as
658 // a parent of the Portal. That means we can ignore it because the
659 // Portal will bubble through to us.
660 return;
661 }
662 }
663 grandNode = grandNode.return;
664 }
665 }
666 // Now we need to find it's corresponding host fiber in the other
667 // tree. To do this we can use getClosestInstanceFromNode, but we
668 // need to validate that the fiber is a host instance, otherwise
669 // we need to traverse up through the DOM till we find the correct
670 // node that is from the other tree.
671 while (container !== null) {
672 const parentNode = getClosestInstanceFromNode(container);
673 if (parentNode === null) {
674 return;
675 }
676 const parentTag = parentNode.tag;
677 if (
678 parentTag === HostComponent ||
679 parentTag === HostText ||
680 parentTag === HostHoistable ||
681 parentTag === HostSingleton
682 ) {
683 node = ancestorInst = parentNode;
684 continue mainLoop;
685 }
686 container = container.parentNode;
687 }
688 }
689 node = node.return;
690 }
691 }
692 }
693
694 batchedUpdates(() =>
695 dispatchEventsForPlugins(
696 domEventName,
697 eventSystemFlags,
698 nativeEvent,
699 ancestorInst,
700 targetContainer,
701 ),
702 );
703 }
704
705 function createDispatchListener(
706 instance: null | Fiber,
707 listener: Function,
708 currentTarget: EventTarget,
709 ): DispatchListener {
710 return {
711 instance,
712 listener,
713 currentTarget,
714 };
715 }
716
717 export function accumulateSinglePhaseListeners(
718 targetFiber: Fiber | null,
719 reactName: string | null,
720 nativeEventType: string,
721 inCapturePhase: boolean,
722 accumulateTargetOnly: boolean,
723 nativeEvent: AnyNativeEvent,
724 ): Array<DispatchListener> {
725 const captureName = reactName !== null ? reactName + 'Capture' : null;
726 const reactEventName = inCapturePhase ? captureName : reactName;
727 let listeners: Array<DispatchListener> = [];
728
729 let instance = targetFiber;
730 let lastHostComponent = null;
731
732 // Accumulate all instances and listeners via the target -> root path.
733 while (instance !== null) {
734 const {stateNode, tag} = instance;
735 // Handle listeners that are on HostComponents (i.e. <div>)
736 if (
737 (tag === HostComponent ||
738 tag === HostHoistable ||
739 tag === HostSingleton) &&
740 stateNode !== null
741 ) {
742 lastHostComponent = stateNode;
743
744 // createEventHandle listeners
745 if (enableCreateEventHandleAPI) {
746 const eventHandlerListeners =
747 getEventHandlerListeners(lastHostComponent);
748 if (eventHandlerListeners !== null) {
749 eventHandlerListeners.forEach(entry => {
750 if (
751 entry.type === nativeEventType &&
752 entry.capture === inCapturePhase
753 ) {
754 listeners.push(
755 createDispatchListener(
756 instance,
757 entry.callback,
758 lastHostComponent as any,
759 ),
760 );
761 }
762 });
763 }
764 }
765
766 // Standard React on* listeners, i.e. onClick or onClickCapture
767 if (reactEventName !== null) {
768 const listener = getListener(instance, reactEventName);
769 if (listener != null) {
770 listeners.push(
771 createDispatchListener(instance, listener, lastHostComponent),
772 );
773 }
774 }
775 } else if (
776 enableCreateEventHandleAPI &&
777 enableScopeAPI &&
778 tag === ScopeComponent &&
779 lastHostComponent !== null &&
780 stateNode !== null
781 ) {
782 // Scopes
783 const reactScopeInstance = stateNode;
784 const eventHandlerListeners =
785 getEventHandlerListeners(reactScopeInstance);
786 if (eventHandlerListeners !== null) {
787 eventHandlerListeners.forEach(entry => {
788 if (
789 entry.type === nativeEventType &&
790 entry.capture === inCapturePhase
791 ) {
792 listeners.push(
793 createDispatchListener(
794 instance,
795 entry.callback,
796 lastHostComponent as any,
797 ),
798 );
799 }
800 });
801 }
802 }
803 // If we are only accumulating events for the target, then we don't
804 // continue to propagate through the React fiber tree to find other
805 // listeners.
806 if (accumulateTargetOnly) {
807 break;
808 }
809 // If we are processing the onBeforeBlur event, then we need to take
810 // into consideration that part of the React tree might have been hidden
811 // or deleted (as we're invoking this event during commit). We can find
812 // this out by checking if intercept fiber set on the event matches the
813 // current instance fiber. In which case, we should clear all existing
814 // listeners.
815 if (enableCreateEventHandleAPI && nativeEvent.type === 'beforeblur') {
816 // $FlowFixMe[prop-missing] internal field
817 const detachedInterceptFiber = nativeEvent._detachedInterceptFiber;
818 if (
819 detachedInterceptFiber !== null &&
820 (detachedInterceptFiber === instance ||
821 detachedInterceptFiber === instance.alternate)
822 ) {
823 listeners = [];
824 }
825 }
826 instance = instance.return;
827 }
828 return listeners;
829 }
830
831 // We should only use this function for:
832 // - BeforeInputEventPlugin
833 // - ChangeEventPlugin
834 // - SelectEventPlugin
835 // - ScrollEndEventPlugin
836 // This is because we only process these plugins
837 // in the bubble phase, so we need to accumulate two
838 // phase event listeners (via emulation).
839 export function accumulateTwoPhaseListeners(
840 targetFiber: Fiber | null,
841 reactName: string,
842 ): Array<DispatchListener> {
843 const captureName = reactName + 'Capture';
844 const listeners: Array<DispatchListener> = [];
845 let instance = targetFiber;
846
847 // Accumulate all instances and listeners via the target -> root path.
848 while (instance !== null) {
849 const {stateNode, tag} = instance;
850 // Handle listeners that are on HostComponents (i.e. <div>)
851 if (
852 (tag === HostComponent ||
853 tag === HostHoistable ||
854 tag === HostSingleton) &&
855 stateNode !== null
856 ) {
857 const currentTarget = stateNode;
858 const captureListener = getListener(instance, captureName);
859 if (captureListener != null) {
860 listeners.unshift(
861 createDispatchListener(instance, captureListener, currentTarget),
862 );
863 }
864 const bubbleListener = getListener(instance, reactName);
865 if (bubbleListener != null) {
866 listeners.push(
867 createDispatchListener(instance, bubbleListener, currentTarget),
868 );
869 }
870 }
871 if (instance.tag === HostRoot) {
872 return listeners;
873 }
874 instance = instance.return;
875 }
876 // If we didn't reach the root it means we're unmounted and shouldn't
877 // dispatch any events on the target.
878 return [];
879 }
880
881 function getParent(inst: Fiber | null): Fiber | null {
882 if (inst === null) {
883 return null;
884 }
885 do {
886 // $FlowFixMe[incompatible-use] found when upgrading Flow
887 inst = inst.return;
888 // TODO: If this is a HostRoot we might want to bail out.
889 // That is depending on if we want nested subtrees (layers) to bubble
890 // events to their parent. We could also go through parentNode on the
891 // host node but that wouldn't work for React Native and doesn't let us
892 // do the portal feature.
893 } while (inst && inst.tag !== HostComponent && inst.tag !== HostSingleton);
894 if (inst) {
895 return inst;
896 }
897 return null;
898 }
899
900 function accumulateEnterLeaveListenersForEvent(
901 dispatchQueue: DispatchQueue,
902 event: KnownReactSyntheticEvent,
903 target: Fiber,
904 common: Fiber | null,
905 inCapturePhase: boolean,
906 ): void {
907 const registrationName = event._reactName;
908 const listeners: Array<DispatchListener> = [];
909
910 let instance: null | Fiber = target;
911 while (instance !== null) {
912 if (instance === common) {
913 break;
914 }
915 const {alternate, stateNode, tag} = instance;
916 if (alternate !== null && alternate === common) {
917 break;
918 }
919 if (
920 (tag === HostComponent ||
921 tag === HostHoistable ||
922 tag === HostSingleton) &&
923 stateNode !== null
924 ) {
925 const currentTarget = stateNode;
926 if (inCapturePhase) {
927 const captureListener = getListener(instance, registrationName);
928 if (captureListener != null) {
929 listeners.unshift(
930 createDispatchListener(instance, captureListener, currentTarget),
931 );
932 }
933 // $FlowFixMe[constant-condition]
934 } else if (!inCapturePhase) {
935 const bubbleListener = getListener(instance, registrationName);
936 if (bubbleListener != null) {
937 listeners.push(
938 createDispatchListener(instance, bubbleListener, currentTarget),
939 );
940 }
941 }
942 }
943 instance = instance.return;
944 }
945 if (listeners.length !== 0) {
946 dispatchQueue.push({event, listeners});
947 }
948 }
949
950 // We should only use this function for:
951 // - EnterLeaveEventPlugin
952 // This is because we only process this plugin
953 // in the bubble phase, so we need to accumulate two
954 // phase event listeners.
955 export function accumulateEnterLeaveTwoPhaseListeners(
956 dispatchQueue: DispatchQueue,
957 leaveEvent: KnownReactSyntheticEvent,
958 enterEvent: null | KnownReactSyntheticEvent,
959 from: Fiber | null,
960 to: Fiber | null,
961 ): void {
962 const common =
963 from && to ? getLowestCommonAncestor(from, to, getParent) : null;
964
965 if (from !== null) {
966 accumulateEnterLeaveListenersForEvent(
967 dispatchQueue,
968 leaveEvent,
969 from,
970 common,
971 false,
972 );
973 }
974 if (to !== null && enterEvent !== null) {
975 accumulateEnterLeaveListenersForEvent(
976 dispatchQueue,
977 enterEvent,
978 to,
979 common,
980 true,
981 );
982 }
983 }
984
985 export function accumulateEventHandleNonManagedNodeListeners(
986 reactEventType: DOMEventName,
987 currentTarget: EventTarget,
988 inCapturePhase: boolean,
989 ): Array<DispatchListener> {
990 const listeners: Array<DispatchListener> = [];
991
992 const eventListeners = getEventHandlerListeners(currentTarget);
993 if (eventListeners !== null) {
994 eventListeners.forEach(entry => {
995 if (entry.type === reactEventType && entry.capture === inCapturePhase) {
996 listeners.push(
997 createDispatchListener(null, entry.callback, currentTarget),
998 );
999 }
1000 });
1001 }
1002 return listeners;
1003 }
1004
1005 export function getListenerSetKey(
1006 domEventName: DOMEventName,
1007 capture: boolean,
1008 ): string {
1009 return `${domEventName}__${capture ? 'capture' : 'bubble'}`;
1010 }