mirror of
https://github.com/facebook/react.git
synced 2026-02-22 03:42:05 +00:00
After an easy couple version with #34252, this version is less flexible (and safer) on inferring exported types mainly. We require to annotate some exported types to differentiate between `boolean` and literal `true` types, etc.
1005 lines
30 KiB
JavaScript
1005 lines
30 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {DOMEventName} from './DOMEventNames';
|
|
import type {EventSystemFlags} from './EventSystemFlags';
|
|
import type {AnyNativeEvent} from './PluginModuleType';
|
|
import type {
|
|
KnownReactSyntheticEvent,
|
|
ReactSyntheticEvent,
|
|
} from './ReactSyntheticEventType';
|
|
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
|
|
|
import {allNativeEvents} from './EventRegistry';
|
|
import {
|
|
SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE,
|
|
IS_LEGACY_FB_SUPPORT_MODE,
|
|
SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS,
|
|
IS_CAPTURE_PHASE,
|
|
IS_EVENT_HANDLE_NON_MANAGED_NODE,
|
|
IS_NON_DELEGATED,
|
|
} from './EventSystemFlags';
|
|
import {isReplayingEvent} from './CurrentReplayingEvent';
|
|
|
|
import {
|
|
HostRoot,
|
|
HostPortal,
|
|
HostComponent,
|
|
HostHoistable,
|
|
HostSingleton,
|
|
HostText,
|
|
ScopeComponent,
|
|
} from 'react-reconciler/src/ReactWorkTags';
|
|
import {getLowestCommonAncestor} from 'react-reconciler/src/ReactFiberTreeReflection';
|
|
|
|
import getEventTarget from './getEventTarget';
|
|
import {
|
|
getClosestInstanceFromNode,
|
|
getEventListenerSet,
|
|
getEventHandlerListeners,
|
|
} from '../client/ReactDOMComponentTree';
|
|
import {COMMENT_NODE, DOCUMENT_NODE} from '../client/HTMLNodeType';
|
|
import {batchedUpdates} from './ReactDOMUpdateBatching';
|
|
import getListener from './getListener';
|
|
import {passiveBrowserEventsSupported} from './checkPassiveEvents';
|
|
|
|
import {
|
|
enableLegacyFBSupport,
|
|
enableCreateEventHandleAPI,
|
|
enableScopeAPI,
|
|
disableCommentsAsDOMContainers,
|
|
enableScrollEndPolyfill,
|
|
} from 'shared/ReactFeatureFlags';
|
|
import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener';
|
|
import {
|
|
removeEventListener,
|
|
addEventCaptureListener,
|
|
addEventBubbleListener,
|
|
addEventBubbleListenerWithPassiveFlag,
|
|
addEventCaptureListenerWithPassiveFlag,
|
|
} from './EventListener';
|
|
import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
|
|
import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
|
|
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
|
|
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
|
|
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
|
|
import * as FormActionEventPlugin from './plugins/FormActionEventPlugin';
|
|
import * as ScrollEndEventPlugin from './plugins/ScrollEndEventPlugin';
|
|
|
|
import reportGlobalError from 'shared/reportGlobalError';
|
|
|
|
import {runWithFiberInDEV} from 'react-reconciler/src/ReactCurrentFiber';
|
|
|
|
type DispatchListener = {
|
|
instance: null | Fiber,
|
|
listener: Function,
|
|
currentTarget: EventTarget,
|
|
};
|
|
|
|
type DispatchEntry = {
|
|
event: ReactSyntheticEvent,
|
|
listeners: Array<DispatchListener>,
|
|
};
|
|
|
|
export type DispatchQueue = Array<DispatchEntry>;
|
|
|
|
// TODO: remove top-level side effect.
|
|
SimpleEventPlugin.registerEvents();
|
|
EnterLeaveEventPlugin.registerEvents();
|
|
ChangeEventPlugin.registerEvents();
|
|
SelectEventPlugin.registerEvents();
|
|
BeforeInputEventPlugin.registerEvents();
|
|
if (enableScrollEndPolyfill) {
|
|
ScrollEndEventPlugin.registerEvents();
|
|
}
|
|
|
|
function extractEvents(
|
|
dispatchQueue: DispatchQueue,
|
|
domEventName: DOMEventName,
|
|
targetInst: null | Fiber,
|
|
nativeEvent: AnyNativeEvent,
|
|
nativeEventTarget: null | EventTarget,
|
|
eventSystemFlags: EventSystemFlags,
|
|
targetContainer: EventTarget,
|
|
) {
|
|
// TODO: we should remove the concept of a "SimpleEventPlugin".
|
|
// This is the basic functionality of the event system. All
|
|
// the other plugins are essentially polyfills. So the plugin
|
|
// should probably be inlined somewhere and have its logic
|
|
// be core the to event system. This would potentially allow
|
|
// us to ship builds of React without the polyfilled plugins below.
|
|
SimpleEventPlugin.extractEvents(
|
|
dispatchQueue,
|
|
domEventName,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
targetContainer,
|
|
);
|
|
const shouldProcessPolyfillPlugins =
|
|
(eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
|
|
// We don't process these events unless we are in the
|
|
// event's native "bubble" phase, which means that we're
|
|
// not in the capture phase. That's because we emulate
|
|
// the capture phase here still. This is a trade-off,
|
|
// because in an ideal world we would not emulate and use
|
|
// the phases properly, like we do with the SimpleEvent
|
|
// plugin. However, the plugins below either expect
|
|
// emulation (EnterLeave) or use state localized to that
|
|
// plugin (BeforeInput, Change, Select). The state in
|
|
// these modules complicates things, as you'll essentially
|
|
// get the case where the capture phase event might change
|
|
// state, only for the following bubble event to come in
|
|
// later and not trigger anything as the state now
|
|
// invalidates the heuristics of the event plugin. We
|
|
// could alter all these plugins to work in such ways, but
|
|
// that might cause other unknown side-effects that we
|
|
// can't foresee right now.
|
|
if (shouldProcessPolyfillPlugins) {
|
|
EnterLeaveEventPlugin.extractEvents(
|
|
dispatchQueue,
|
|
domEventName,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
targetContainer,
|
|
);
|
|
ChangeEventPlugin.extractEvents(
|
|
dispatchQueue,
|
|
domEventName,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
targetContainer,
|
|
);
|
|
SelectEventPlugin.extractEvents(
|
|
dispatchQueue,
|
|
domEventName,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
targetContainer,
|
|
);
|
|
BeforeInputEventPlugin.extractEvents(
|
|
dispatchQueue,
|
|
domEventName,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
targetContainer,
|
|
);
|
|
FormActionEventPlugin.extractEvents(
|
|
dispatchQueue,
|
|
domEventName,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
targetContainer,
|
|
);
|
|
}
|
|
if (enableScrollEndPolyfill) {
|
|
ScrollEndEventPlugin.extractEvents(
|
|
dispatchQueue,
|
|
domEventName,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
targetContainer,
|
|
);
|
|
}
|
|
}
|
|
|
|
// List of events that need to be individually attached to media elements.
|
|
export const mediaEventTypes: Array<DOMEventName> = [
|
|
'abort',
|
|
'canplay',
|
|
'canplaythrough',
|
|
'durationchange',
|
|
'emptied',
|
|
'encrypted',
|
|
'ended',
|
|
'error',
|
|
'loadeddata',
|
|
'loadedmetadata',
|
|
'loadstart',
|
|
'pause',
|
|
'play',
|
|
'playing',
|
|
'progress',
|
|
'ratechange',
|
|
'resize',
|
|
'seeked',
|
|
'seeking',
|
|
'stalled',
|
|
'suspend',
|
|
'timeupdate',
|
|
'volumechange',
|
|
'waiting',
|
|
];
|
|
|
|
// We should not delegate these events to the container, but rather
|
|
// set them on the actual target element itself. This is primarily
|
|
// because these events do not consistently bubble in the DOM.
|
|
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
|
|
'beforetoggle',
|
|
'cancel',
|
|
'close',
|
|
'invalid',
|
|
'load',
|
|
'scroll',
|
|
'scrollend',
|
|
'toggle',
|
|
// In order to reduce bytes, we insert the above array of media events
|
|
// into this Set. Note: the "error" event isn't an exclusive media event,
|
|
// and can occur on other elements too. Rather than duplicate that event,
|
|
// we just take it from the media events array.
|
|
...mediaEventTypes,
|
|
]);
|
|
|
|
function executeDispatch(
|
|
event: ReactSyntheticEvent,
|
|
listener: Function,
|
|
currentTarget: EventTarget,
|
|
): void {
|
|
event.currentTarget = currentTarget;
|
|
try {
|
|
listener(event);
|
|
} catch (error) {
|
|
reportGlobalError(error);
|
|
}
|
|
event.currentTarget = null;
|
|
}
|
|
|
|
function processDispatchQueueItemsInOrder(
|
|
event: ReactSyntheticEvent,
|
|
dispatchListeners: Array<DispatchListener>,
|
|
inCapturePhase: boolean,
|
|
): void {
|
|
let previousInstance;
|
|
if (inCapturePhase) {
|
|
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
|
|
const {instance, currentTarget, listener} = dispatchListeners[i];
|
|
if (instance !== previousInstance && event.isPropagationStopped()) {
|
|
return;
|
|
}
|
|
if (__DEV__ && instance !== null) {
|
|
runWithFiberInDEV(
|
|
instance,
|
|
executeDispatch,
|
|
event,
|
|
listener,
|
|
currentTarget,
|
|
);
|
|
} else {
|
|
executeDispatch(event, listener, currentTarget);
|
|
}
|
|
previousInstance = instance;
|
|
}
|
|
} else {
|
|
for (let i = 0; i < dispatchListeners.length; i++) {
|
|
const {instance, currentTarget, listener} = dispatchListeners[i];
|
|
if (instance !== previousInstance && event.isPropagationStopped()) {
|
|
return;
|
|
}
|
|
if (__DEV__ && instance !== null) {
|
|
runWithFiberInDEV(
|
|
instance,
|
|
executeDispatch,
|
|
event,
|
|
listener,
|
|
currentTarget,
|
|
);
|
|
} else {
|
|
executeDispatch(event, listener, currentTarget);
|
|
}
|
|
previousInstance = instance;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function processDispatchQueue(
|
|
dispatchQueue: DispatchQueue,
|
|
eventSystemFlags: EventSystemFlags,
|
|
): void {
|
|
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
|
|
for (let i = 0; i < dispatchQueue.length; i++) {
|
|
const {event, listeners} = dispatchQueue[i];
|
|
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
|
|
// event system doesn't use pooling.
|
|
}
|
|
}
|
|
|
|
function dispatchEventsForPlugins(
|
|
domEventName: DOMEventName,
|
|
eventSystemFlags: EventSystemFlags,
|
|
nativeEvent: AnyNativeEvent,
|
|
targetInst: null | Fiber,
|
|
targetContainer: EventTarget,
|
|
): void {
|
|
const nativeEventTarget = getEventTarget(nativeEvent);
|
|
const dispatchQueue: DispatchQueue = [];
|
|
extractEvents(
|
|
dispatchQueue,
|
|
domEventName,
|
|
targetInst,
|
|
nativeEvent,
|
|
nativeEventTarget,
|
|
eventSystemFlags,
|
|
targetContainer,
|
|
);
|
|
processDispatchQueue(dispatchQueue, eventSystemFlags);
|
|
}
|
|
|
|
export function listenToNonDelegatedEvent(
|
|
domEventName: DOMEventName,
|
|
targetElement: Element,
|
|
): void {
|
|
if (__DEV__) {
|
|
if (!nonDelegatedEvents.has(domEventName)) {
|
|
console.error(
|
|
'Did not expect a listenToNonDelegatedEvent() call for "%s". ' +
|
|
'This is a bug in React. Please file an issue.',
|
|
domEventName,
|
|
);
|
|
}
|
|
}
|
|
const isCapturePhaseListener = false;
|
|
const listenerSet = getEventListenerSet(targetElement);
|
|
const listenerSetKey = getListenerSetKey(
|
|
domEventName,
|
|
isCapturePhaseListener,
|
|
);
|
|
if (!listenerSet.has(listenerSetKey)) {
|
|
addTrappedEventListener(
|
|
targetElement,
|
|
domEventName,
|
|
IS_NON_DELEGATED,
|
|
isCapturePhaseListener,
|
|
);
|
|
listenerSet.add(listenerSetKey);
|
|
}
|
|
}
|
|
|
|
export function listenToNativeEvent(
|
|
domEventName: DOMEventName,
|
|
isCapturePhaseListener: boolean,
|
|
target: EventTarget,
|
|
): void {
|
|
if (__DEV__) {
|
|
if (nonDelegatedEvents.has(domEventName) && !isCapturePhaseListener) {
|
|
console.error(
|
|
'Did not expect a listenToNativeEvent() call for "%s" in the bubble phase. ' +
|
|
'This is a bug in React. Please file an issue.',
|
|
domEventName,
|
|
);
|
|
}
|
|
}
|
|
|
|
let eventSystemFlags = 0;
|
|
if (isCapturePhaseListener) {
|
|
eventSystemFlags |= IS_CAPTURE_PHASE;
|
|
}
|
|
addTrappedEventListener(
|
|
target,
|
|
domEventName,
|
|
eventSystemFlags,
|
|
isCapturePhaseListener,
|
|
);
|
|
}
|
|
|
|
// This is only used by createEventHandle when the
|
|
// target is not a DOM element. E.g. window.
|
|
export function listenToNativeEventForNonManagedEventTarget(
|
|
domEventName: DOMEventName,
|
|
isCapturePhaseListener: boolean,
|
|
target: EventTarget,
|
|
): void {
|
|
let eventSystemFlags: number = IS_EVENT_HANDLE_NON_MANAGED_NODE;
|
|
const listenerSet = getEventListenerSet(target);
|
|
const listenerSetKey = getListenerSetKey(
|
|
domEventName,
|
|
isCapturePhaseListener,
|
|
);
|
|
if (!listenerSet.has(listenerSetKey)) {
|
|
if (isCapturePhaseListener) {
|
|
eventSystemFlags |= IS_CAPTURE_PHASE;
|
|
}
|
|
addTrappedEventListener(
|
|
target,
|
|
domEventName,
|
|
eventSystemFlags,
|
|
isCapturePhaseListener,
|
|
);
|
|
listenerSet.add(listenerSetKey);
|
|
}
|
|
}
|
|
|
|
const listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
|
|
|
|
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
|
|
if (!(rootContainerElement: any)[listeningMarker]) {
|
|
(rootContainerElement: any)[listeningMarker] = true;
|
|
allNativeEvents.forEach(domEventName => {
|
|
// We handle selectionchange separately because it
|
|
// doesn't bubble and needs to be on the document.
|
|
if (domEventName !== 'selectionchange') {
|
|
if (!nonDelegatedEvents.has(domEventName)) {
|
|
listenToNativeEvent(domEventName, false, rootContainerElement);
|
|
}
|
|
listenToNativeEvent(domEventName, true, rootContainerElement);
|
|
}
|
|
});
|
|
const ownerDocument =
|
|
(rootContainerElement: any).nodeType === DOCUMENT_NODE
|
|
? rootContainerElement
|
|
: (rootContainerElement: any).ownerDocument;
|
|
if (ownerDocument !== null) {
|
|
// The selectionchange event also needs deduplication
|
|
// but it is attached to the document.
|
|
if (!(ownerDocument: any)[listeningMarker]) {
|
|
(ownerDocument: any)[listeningMarker] = true;
|
|
listenToNativeEvent('selectionchange', false, ownerDocument);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function addTrappedEventListener(
|
|
targetContainer: EventTarget,
|
|
domEventName: DOMEventName,
|
|
eventSystemFlags: EventSystemFlags,
|
|
isCapturePhaseListener: boolean,
|
|
isDeferredListenerForLegacyFBSupport?: boolean,
|
|
) {
|
|
let listener = createEventListenerWrapperWithPriority(
|
|
targetContainer,
|
|
domEventName,
|
|
eventSystemFlags,
|
|
);
|
|
// If passive option is not supported, then the event will be
|
|
// active and not passive.
|
|
let isPassiveListener: void | boolean = undefined;
|
|
if (passiveBrowserEventsSupported) {
|
|
// Browsers introduced an intervention, making these events
|
|
// passive by default on document. React doesn't bind them
|
|
// to document anymore, but changing this now would undo
|
|
// the performance wins from the change. So we emulate
|
|
// the existing behavior manually on the roots now.
|
|
// https://github.com/facebook/react/issues/19651
|
|
if (
|
|
domEventName === 'touchstart' ||
|
|
domEventName === 'touchmove' ||
|
|
domEventName === 'wheel'
|
|
) {
|
|
isPassiveListener = true;
|
|
}
|
|
}
|
|
|
|
targetContainer =
|
|
enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
|
|
? (targetContainer: any).ownerDocument
|
|
: targetContainer;
|
|
|
|
let unsubscribeListener;
|
|
// When legacyFBSupport is enabled, it's for when we
|
|
// want to add a one time event listener to a container.
|
|
// This should only be used with enableLegacyFBSupport
|
|
// due to requirement to provide compatibility with
|
|
// internal FB www event tooling. This works by removing
|
|
// the event listener as soon as it is invoked. We could
|
|
// also attempt to use the {once: true} param on
|
|
// addEventListener, but that requires support and some
|
|
// browsers do not support this today, and given this is
|
|
// to support legacy code patterns, it's likely they'll
|
|
// need support for such browsers.
|
|
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
|
|
const originalListener = listener;
|
|
// $FlowFixMe[missing-this-annot]
|
|
listener = function (...p) {
|
|
removeEventListener(
|
|
targetContainer,
|
|
domEventName,
|
|
unsubscribeListener,
|
|
isCapturePhaseListener,
|
|
);
|
|
return originalListener.apply(this, p);
|
|
};
|
|
}
|
|
// TODO: There are too many combinations here. Consolidate them.
|
|
if (isCapturePhaseListener) {
|
|
if (isPassiveListener !== undefined) {
|
|
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
|
|
targetContainer,
|
|
domEventName,
|
|
listener,
|
|
isPassiveListener,
|
|
);
|
|
} else {
|
|
unsubscribeListener = addEventCaptureListener(
|
|
targetContainer,
|
|
domEventName,
|
|
listener,
|
|
);
|
|
}
|
|
} else {
|
|
if (isPassiveListener !== undefined) {
|
|
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
|
|
targetContainer,
|
|
domEventName,
|
|
listener,
|
|
isPassiveListener,
|
|
);
|
|
} else {
|
|
unsubscribeListener = addEventBubbleListener(
|
|
targetContainer,
|
|
domEventName,
|
|
listener,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function deferClickToDocumentForLegacyFBSupport(
|
|
domEventName: DOMEventName,
|
|
targetContainer: EventTarget,
|
|
): void {
|
|
// We defer all click events with legacy FB support mode on.
|
|
// This means we add a one time event listener to trigger
|
|
// after the FB delegated listeners fire.
|
|
const isDeferredListenerForLegacyFBSupport = true;
|
|
addTrappedEventListener(
|
|
targetContainer,
|
|
domEventName,
|
|
IS_LEGACY_FB_SUPPORT_MODE,
|
|
false,
|
|
isDeferredListenerForLegacyFBSupport,
|
|
);
|
|
}
|
|
|
|
function isMatchingRootContainer(
|
|
grandContainer: Element,
|
|
targetContainer: EventTarget,
|
|
): boolean {
|
|
return (
|
|
grandContainer === targetContainer ||
|
|
(!disableCommentsAsDOMContainers &&
|
|
grandContainer.nodeType === COMMENT_NODE &&
|
|
grandContainer.parentNode === targetContainer)
|
|
);
|
|
}
|
|
|
|
export function dispatchEventForPluginEventSystem(
|
|
domEventName: DOMEventName,
|
|
eventSystemFlags: EventSystemFlags,
|
|
nativeEvent: AnyNativeEvent,
|
|
targetInst: null | Fiber,
|
|
targetContainer: EventTarget,
|
|
): void {
|
|
let ancestorInst = targetInst;
|
|
if (
|
|
(eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE) === 0 &&
|
|
(eventSystemFlags & IS_NON_DELEGATED) === 0
|
|
) {
|
|
const targetContainerNode = ((targetContainer: any): Node);
|
|
|
|
// If we are using the legacy FB support flag, we
|
|
// defer the event to the null with a one
|
|
// time event listener so we can defer the event.
|
|
if (
|
|
enableLegacyFBSupport &&
|
|
// If our event flags match the required flags for entering
|
|
// FB legacy mode and we are processing the "click" event,
|
|
// then we can defer the event to the "document", to allow
|
|
// for legacy FB support, where the expected behavior was to
|
|
// match React < 16 behavior of delegated clicks to the doc.
|
|
domEventName === 'click' &&
|
|
(eventSystemFlags & SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE) === 0 &&
|
|
!isReplayingEvent(nativeEvent)
|
|
) {
|
|
deferClickToDocumentForLegacyFBSupport(domEventName, targetContainer);
|
|
return;
|
|
}
|
|
if (targetInst !== null) {
|
|
// The below logic attempts to work out if we need to change
|
|
// the target fiber to a different ancestor. We had similar logic
|
|
// in the legacy event system, except the big difference between
|
|
// systems is that the modern event system now has an event listener
|
|
// attached to each React Root and React Portal Root. Together,
|
|
// the DOM nodes representing these roots are the "rootContainer".
|
|
// To figure out which ancestor instance we should use, we traverse
|
|
// up the fiber tree from the target instance and attempt to find
|
|
// root boundaries that match that of our current "rootContainer".
|
|
// If we find that "rootContainer", we find the parent fiber
|
|
// sub-tree for that root and make that our ancestor instance.
|
|
let node: null | Fiber = targetInst;
|
|
|
|
mainLoop: while (true) {
|
|
if (node === null) {
|
|
return;
|
|
}
|
|
const nodeTag = node.tag;
|
|
if (nodeTag === HostRoot || nodeTag === HostPortal) {
|
|
let container = node.stateNode.containerInfo;
|
|
if (isMatchingRootContainer(container, targetContainerNode)) {
|
|
break;
|
|
}
|
|
if (nodeTag === HostPortal) {
|
|
// The target is a portal, but it's not the rootContainer we're looking for.
|
|
// Normally portals handle their own events all the way down to the root.
|
|
// So we should be able to stop now. However, we don't know if this portal
|
|
// was part of *our* root.
|
|
let grandNode = node.return;
|
|
while (grandNode !== null) {
|
|
const grandTag = grandNode.tag;
|
|
if (grandTag === HostRoot || grandTag === HostPortal) {
|
|
const grandContainer = grandNode.stateNode.containerInfo;
|
|
if (
|
|
isMatchingRootContainer(grandContainer, targetContainerNode)
|
|
) {
|
|
// This is the rootContainer we're looking for and we found it as
|
|
// a parent of the Portal. That means we can ignore it because the
|
|
// Portal will bubble through to us.
|
|
return;
|
|
}
|
|
}
|
|
grandNode = grandNode.return;
|
|
}
|
|
}
|
|
// Now we need to find it's corresponding host fiber in the other
|
|
// tree. To do this we can use getClosestInstanceFromNode, but we
|
|
// need to validate that the fiber is a host instance, otherwise
|
|
// we need to traverse up through the DOM till we find the correct
|
|
// node that is from the other tree.
|
|
while (container !== null) {
|
|
const parentNode = getClosestInstanceFromNode(container);
|
|
if (parentNode === null) {
|
|
return;
|
|
}
|
|
const parentTag = parentNode.tag;
|
|
if (
|
|
parentTag === HostComponent ||
|
|
parentTag === HostText ||
|
|
parentTag === HostHoistable ||
|
|
parentTag === HostSingleton
|
|
) {
|
|
node = ancestorInst = parentNode;
|
|
continue mainLoop;
|
|
}
|
|
container = container.parentNode;
|
|
}
|
|
}
|
|
node = node.return;
|
|
}
|
|
}
|
|
}
|
|
|
|
batchedUpdates(() =>
|
|
dispatchEventsForPlugins(
|
|
domEventName,
|
|
eventSystemFlags,
|
|
nativeEvent,
|
|
ancestorInst,
|
|
targetContainer,
|
|
),
|
|
);
|
|
}
|
|
|
|
function createDispatchListener(
|
|
instance: null | Fiber,
|
|
listener: Function,
|
|
currentTarget: EventTarget,
|
|
): DispatchListener {
|
|
return {
|
|
instance,
|
|
listener,
|
|
currentTarget,
|
|
};
|
|
}
|
|
|
|
export function accumulateSinglePhaseListeners(
|
|
targetFiber: Fiber | null,
|
|
reactName: string | null,
|
|
nativeEventType: string,
|
|
inCapturePhase: boolean,
|
|
accumulateTargetOnly: boolean,
|
|
nativeEvent: AnyNativeEvent,
|
|
): Array<DispatchListener> {
|
|
const captureName = reactName !== null ? reactName + 'Capture' : null;
|
|
const reactEventName = inCapturePhase ? captureName : reactName;
|
|
let listeners: Array<DispatchListener> = [];
|
|
|
|
let instance = targetFiber;
|
|
let lastHostComponent = null;
|
|
|
|
// Accumulate all instances and listeners via the target -> root path.
|
|
while (instance !== null) {
|
|
const {stateNode, tag} = instance;
|
|
// Handle listeners that are on HostComponents (i.e. <div>)
|
|
if (
|
|
(tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton) &&
|
|
stateNode !== null
|
|
) {
|
|
lastHostComponent = stateNode;
|
|
|
|
// createEventHandle listeners
|
|
if (enableCreateEventHandleAPI) {
|
|
const eventHandlerListeners =
|
|
getEventHandlerListeners(lastHostComponent);
|
|
if (eventHandlerListeners !== null) {
|
|
eventHandlerListeners.forEach(entry => {
|
|
if (
|
|
entry.type === nativeEventType &&
|
|
entry.capture === inCapturePhase
|
|
) {
|
|
listeners.push(
|
|
createDispatchListener(
|
|
instance,
|
|
entry.callback,
|
|
(lastHostComponent: any),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Standard React on* listeners, i.e. onClick or onClickCapture
|
|
if (reactEventName !== null) {
|
|
const listener = getListener(instance, reactEventName);
|
|
if (listener != null) {
|
|
listeners.push(
|
|
createDispatchListener(instance, listener, lastHostComponent),
|
|
);
|
|
}
|
|
}
|
|
} else if (
|
|
enableCreateEventHandleAPI &&
|
|
enableScopeAPI &&
|
|
tag === ScopeComponent &&
|
|
lastHostComponent !== null &&
|
|
stateNode !== null
|
|
) {
|
|
// Scopes
|
|
const reactScopeInstance = stateNode;
|
|
const eventHandlerListeners =
|
|
getEventHandlerListeners(reactScopeInstance);
|
|
if (eventHandlerListeners !== null) {
|
|
eventHandlerListeners.forEach(entry => {
|
|
if (
|
|
entry.type === nativeEventType &&
|
|
entry.capture === inCapturePhase
|
|
) {
|
|
listeners.push(
|
|
createDispatchListener(
|
|
instance,
|
|
entry.callback,
|
|
(lastHostComponent: any),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// If we are only accumulating events for the target, then we don't
|
|
// continue to propagate through the React fiber tree to find other
|
|
// listeners.
|
|
if (accumulateTargetOnly) {
|
|
break;
|
|
}
|
|
// If we are processing the onBeforeBlur event, then we need to take
|
|
// into consideration that part of the React tree might have been hidden
|
|
// or deleted (as we're invoking this event during commit). We can find
|
|
// this out by checking if intercept fiber set on the event matches the
|
|
// current instance fiber. In which case, we should clear all existing
|
|
// listeners.
|
|
if (enableCreateEventHandleAPI && nativeEvent.type === 'beforeblur') {
|
|
// $FlowFixMe[prop-missing] internal field
|
|
const detachedInterceptFiber = nativeEvent._detachedInterceptFiber;
|
|
if (
|
|
detachedInterceptFiber !== null &&
|
|
(detachedInterceptFiber === instance ||
|
|
detachedInterceptFiber === instance.alternate)
|
|
) {
|
|
listeners = [];
|
|
}
|
|
}
|
|
instance = instance.return;
|
|
}
|
|
return listeners;
|
|
}
|
|
|
|
// We should only use this function for:
|
|
// - BeforeInputEventPlugin
|
|
// - ChangeEventPlugin
|
|
// - SelectEventPlugin
|
|
// - ScrollEndEventPlugin
|
|
// This is because we only process these plugins
|
|
// in the bubble phase, so we need to accumulate two
|
|
// phase event listeners (via emulation).
|
|
export function accumulateTwoPhaseListeners(
|
|
targetFiber: Fiber | null,
|
|
reactName: string,
|
|
): Array<DispatchListener> {
|
|
const captureName = reactName + 'Capture';
|
|
const listeners: Array<DispatchListener> = [];
|
|
let instance = targetFiber;
|
|
|
|
// Accumulate all instances and listeners via the target -> root path.
|
|
while (instance !== null) {
|
|
const {stateNode, tag} = instance;
|
|
// Handle listeners that are on HostComponents (i.e. <div>)
|
|
if (
|
|
(tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton) &&
|
|
stateNode !== null
|
|
) {
|
|
const currentTarget = stateNode;
|
|
const captureListener = getListener(instance, captureName);
|
|
if (captureListener != null) {
|
|
listeners.unshift(
|
|
createDispatchListener(instance, captureListener, currentTarget),
|
|
);
|
|
}
|
|
const bubbleListener = getListener(instance, reactName);
|
|
if (bubbleListener != null) {
|
|
listeners.push(
|
|
createDispatchListener(instance, bubbleListener, currentTarget),
|
|
);
|
|
}
|
|
}
|
|
if (instance.tag === HostRoot) {
|
|
return listeners;
|
|
}
|
|
instance = instance.return;
|
|
}
|
|
// If we didn't reach the root it means we're unmounted and shouldn't
|
|
// dispatch any events on the target.
|
|
return [];
|
|
}
|
|
|
|
function getParent(inst: Fiber | null): Fiber | null {
|
|
if (inst === null) {
|
|
return null;
|
|
}
|
|
do {
|
|
// $FlowFixMe[incompatible-use] found when upgrading Flow
|
|
inst = inst.return;
|
|
// TODO: If this is a HostRoot we might want to bail out.
|
|
// That is depending on if we want nested subtrees (layers) to bubble
|
|
// events to their parent. We could also go through parentNode on the
|
|
// host node but that wouldn't work for React Native and doesn't let us
|
|
// do the portal feature.
|
|
} while (inst && inst.tag !== HostComponent && inst.tag !== HostSingleton);
|
|
if (inst) {
|
|
return inst;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function accumulateEnterLeaveListenersForEvent(
|
|
dispatchQueue: DispatchQueue,
|
|
event: KnownReactSyntheticEvent,
|
|
target: Fiber,
|
|
common: Fiber | null,
|
|
inCapturePhase: boolean,
|
|
): void {
|
|
const registrationName = event._reactName;
|
|
const listeners: Array<DispatchListener> = [];
|
|
|
|
let instance: null | Fiber = target;
|
|
while (instance !== null) {
|
|
if (instance === common) {
|
|
break;
|
|
}
|
|
const {alternate, stateNode, tag} = instance;
|
|
if (alternate !== null && alternate === common) {
|
|
break;
|
|
}
|
|
if (
|
|
(tag === HostComponent ||
|
|
tag === HostHoistable ||
|
|
tag === HostSingleton) &&
|
|
stateNode !== null
|
|
) {
|
|
const currentTarget = stateNode;
|
|
if (inCapturePhase) {
|
|
const captureListener = getListener(instance, registrationName);
|
|
if (captureListener != null) {
|
|
listeners.unshift(
|
|
createDispatchListener(instance, captureListener, currentTarget),
|
|
);
|
|
}
|
|
} else if (!inCapturePhase) {
|
|
const bubbleListener = getListener(instance, registrationName);
|
|
if (bubbleListener != null) {
|
|
listeners.push(
|
|
createDispatchListener(instance, bubbleListener, currentTarget),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
instance = instance.return;
|
|
}
|
|
if (listeners.length !== 0) {
|
|
dispatchQueue.push({event, listeners});
|
|
}
|
|
}
|
|
|
|
// We should only use this function for:
|
|
// - EnterLeaveEventPlugin
|
|
// This is because we only process this plugin
|
|
// in the bubble phase, so we need to accumulate two
|
|
// phase event listeners.
|
|
export function accumulateEnterLeaveTwoPhaseListeners(
|
|
dispatchQueue: DispatchQueue,
|
|
leaveEvent: KnownReactSyntheticEvent,
|
|
enterEvent: null | KnownReactSyntheticEvent,
|
|
from: Fiber | null,
|
|
to: Fiber | null,
|
|
): void {
|
|
const common =
|
|
from && to ? getLowestCommonAncestor(from, to, getParent) : null;
|
|
|
|
if (from !== null) {
|
|
accumulateEnterLeaveListenersForEvent(
|
|
dispatchQueue,
|
|
leaveEvent,
|
|
from,
|
|
common,
|
|
false,
|
|
);
|
|
}
|
|
if (to !== null && enterEvent !== null) {
|
|
accumulateEnterLeaveListenersForEvent(
|
|
dispatchQueue,
|
|
enterEvent,
|
|
to,
|
|
common,
|
|
true,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function accumulateEventHandleNonManagedNodeListeners(
|
|
reactEventType: DOMEventName,
|
|
currentTarget: EventTarget,
|
|
inCapturePhase: boolean,
|
|
): Array<DispatchListener> {
|
|
const listeners: Array<DispatchListener> = [];
|
|
|
|
const eventListeners = getEventHandlerListeners(currentTarget);
|
|
if (eventListeners !== null) {
|
|
eventListeners.forEach(entry => {
|
|
if (entry.type === reactEventType && entry.capture === inCapturePhase) {
|
|
listeners.push(
|
|
createDispatchListener(null, entry.callback, currentTarget),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
return listeners;
|
|
}
|
|
|
|
export function getListenerSetKey(
|
|
domEventName: DOMEventName,
|
|
capture: boolean,
|
|
): string {
|
|
return `${domEventName}__${capture ? 'capture' : 'bubble'}`;
|
|
}
|