Files
react/packages/react-dom/src/events/DOMEventResponderSystem.js
T

1040 lines
31 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its 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 EventSystemFlags,
IS_PASSIVE,
PASSIVE_NOT_SUPPORTED,
} from 'events/EventSystemFlags';
import type {AnyNativeEvent} from 'events/PluginModuleType';
import {
EventComponent,
EventTarget as EventTargetWorkTag,
HostComponent,
} from 'shared/ReactWorkTags';
import type {
ReactEventResponder,
ReactEventResponderEventType,
ReactEventComponentInstance,
ReactResponderContext,
ReactResponderEvent,
} from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import {
batchedEventUpdates,
discreteUpdates,
flushDiscreteUpdates,
} from 'events/ReactGenericBatching';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import warning from 'shared/warning';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
import invariant from 'shared/invariant';
import {
isFiberSuspenseAndTimedOut,
getSuspenseFallbackChild,
} from 'react-reconciler/src/ReactFiberEvents';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
export let listenToResponderEventTypesImpl;
export function setListenToResponderEventTypes(
_listenToResponderEventTypesImpl: Function,
) {
listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl;
}
type EventObjectType = $Shape<PartialEventObject>;
type EventQueue = {
events: Array<EventObjectType>,
discrete: boolean,
};
type PartialEventObject = {
target: Element | Document,
type: string,
};
type ResponderTimeout = {|
id: TimeoutID,
timers: Map<Symbol, ResponderTimer>,
|};
type ResponderTimer = {|
instance: ReactEventComponentInstance,
func: () => void,
id: Symbol,
timeStamp: number,
|};
const activeTimeouts: Map<Symbol, ResponderTimeout> = new Map();
const rootEventTypesToEventComponentInstances: Map<
DOMTopLevelEventType | string,
Set<ReactEventComponentInstance>,
> = new Map();
const targetEventTypeCached: Map<
Array<ReactEventResponderEventType>,
Set<string>,
> = new Map();
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
const eventListeners:
| WeakMap
| Map<
$Shape<PartialEventObject>,
($Shape<PartialEventObject>) => void,
> = new PossiblyWeakMap();
const responderOwners: Map<
ReactEventResponder,
ReactEventComponentInstance,
> = new Map();
let globalOwner = null;
let currentTimeStamp = 0;
let currentTimers = new Map();
let currentInstance: null | ReactEventComponentInstance = null;
let currentEventQueue: null | EventQueue = null;
const eventResponderContext: ReactResponderContext = {
dispatchEvent(
possibleEventObject: Object,
listener: ($Shape<PartialEventObject>) => void,
discrete: boolean,
): void {
validateResponderContext();
const {target, type, timeStamp} = possibleEventObject;
if (target == null || type == null || timeStamp == null) {
throw new Error(
'context.dispatchEvent: "target", "timeStamp", and "type" fields on event object are required.',
);
}
const showWarning = name => {
if (__DEV__) {
warning(
false,
'%s is not available on event objects created from event responder modules (React Flare). ' +
'Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.%s }`',
name,
name,
);
}
};
possibleEventObject.preventDefault = () => {
if (__DEV__) {
showWarning('preventDefault()');
}
};
possibleEventObject.stopPropagation = () => {
if (__DEV__) {
showWarning('stopPropagation()');
}
};
possibleEventObject.isDefaultPrevented = () => {
if (__DEV__) {
showWarning('isDefaultPrevented()');
}
};
possibleEventObject.isPropagationStopped = () => {
if (__DEV__) {
showWarning('isPropagationStopped()');
}
};
// $FlowFixMe: we don't need value, Flow thinks we do
Object.defineProperty(possibleEventObject, 'nativeEvent', {
get() {
if (__DEV__) {
showWarning('nativeEvent');
}
},
});
// $FlowFixMe: we don't need value, Flow thinks we do
Object.defineProperty(possibleEventObject, 'defaultPrevented', {
get() {
if (__DEV__) {
showWarning('defaultPrevented');
}
},
});
const eventObject = ((possibleEventObject: any): $Shape<
PartialEventObject,
>);
const eventQueue = ((currentEventQueue: any): EventQueue);
if (discrete) {
eventQueue.discrete = true;
}
eventListeners.set(eventObject, listener);
eventQueue.events.push(eventObject);
},
isPositionWithinTouchHitTarget(x: number, y: number): boolean {
validateResponderContext();
const doc = getActiveDocument();
// This isn't available in some environments (JSDOM)
if (typeof doc.elementFromPoint !== 'function') {
return false;
}
const target = doc.elementFromPoint(x, y);
if (target === null) {
return false;
}
const childFiber = getClosestInstanceFromNode(target);
if (childFiber === null) {
return false;
}
const parentFiber = childFiber.return;
if (parentFiber !== null && parentFiber.tag === EventTargetWorkTag) {
const parentNode = ((target.parentNode: any): Element);
// TODO find another way to do this without using the
// expensive getBoundingClientRect.
const {left, top, right, bottom} = parentNode.getBoundingClientRect();
// Check if the co-ords intersect with the target element's rect.
if (x > left && y > top && x < right && y < bottom) {
return false;
}
return true;
}
return false;
},
isTargetWithinEventComponent,
isTargetWithinEventResponderScope(target: Element | Document): boolean {
validateResponderContext();
const responder = ((currentInstance: any): ReactEventComponentInstance)
.responder;
if (target != null) {
let fiber = getClosestInstanceFromNode(target);
while (fiber !== null) {
if (fiber.stateNode === currentInstance) {
return true;
}
if (
fiber.tag === EventComponent &&
(fiber.stateNode === null || fiber.stateNode.responder === responder)
) {
return false;
}
fiber = fiber.return;
}
}
return false;
},
isTargetWithinElement(
childTarget: Element | Document,
parentTarget: Element | Document,
): boolean {
validateResponderContext();
const childFiber = getClosestInstanceFromNode(childTarget);
const parentFiber = getClosestInstanceFromNode(parentTarget);
let node = childFiber;
while (node !== null) {
if (node === parentFiber) {
return true;
}
node = node.return;
}
return false;
},
addRootEventTypes(rootEventTypes: Array<ReactEventResponderEventType>): void {
validateResponderContext();
const activeDocument = getActiveDocument();
listenToResponderEventTypesImpl(rootEventTypes, activeDocument);
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
registerRootEventType(rootEventType, eventComponentInstance);
}
},
removeRootEventTypes(
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
validateResponderContext();
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
let name = rootEventType;
let passive = true;
if (typeof rootEventType !== 'string') {
const targetEventConfigObject = ((rootEventType: any): {
name: string,
passive?: boolean,
});
name = targetEventConfigObject.name;
if (targetEventConfigObject.passive !== undefined) {
passive = targetEventConfigObject.passive;
}
}
const listeningName = generateListeningKey(
((name: any): string),
passive,
);
let rootEventComponents = rootEventTypesToEventComponentInstances.get(
listeningName,
);
let rootEventTypesSet = ((currentInstance: any): ReactEventComponentInstance)
.rootEventTypes;
if (rootEventTypesSet !== null) {
rootEventTypesSet.delete(listeningName);
}
if (rootEventComponents !== undefined) {
rootEventComponents.delete(
((currentInstance: any): ReactEventComponentInstance),
);
}
}
},
hasOwnership(): boolean {
validateResponderContext();
const responder = ((currentInstance: any): ReactEventComponentInstance)
.responder;
return (
globalOwner === currentInstance ||
responderOwners.get(responder) === currentInstance
);
},
requestGlobalOwnership(): boolean {
validateResponderContext();
if (globalOwner !== null) {
return false;
}
globalOwner = currentInstance;
triggerOwnershipListeners(null);
return true;
},
requestResponderOwnership(): boolean {
validateResponderContext();
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
const responder = eventComponentInstance.responder;
if (responderOwners.has(responder)) {
return false;
}
responderOwners.set(responder, eventComponentInstance);
triggerOwnershipListeners(responder);
return true;
},
releaseOwnership(): boolean {
validateResponderContext();
return releaseOwnershipForEventComponentInstance(
((currentInstance: any): ReactEventComponentInstance),
);
},
setTimeout(func: () => void, delay): Symbol {
validateResponderContext();
if (currentTimers === null) {
currentTimers = new Map();
}
let timeout = currentTimers.get(delay);
const timerId = Symbol();
if (timeout === undefined) {
const timers = new Map();
const id = setTimeout(() => {
processTimers(timers, delay);
}, delay);
timeout = {
id,
timers,
};
currentTimers.set(delay, timeout);
}
timeout.timers.set(timerId, {
instance: ((currentInstance: any): ReactEventComponentInstance),
func,
id: timerId,
timeStamp: currentTimeStamp,
});
activeTimeouts.set(timerId, timeout);
return timerId;
},
clearTimeout(timerId: Symbol): void {
validateResponderContext();
const timeout = activeTimeouts.get(timerId);
if (timeout !== undefined) {
const timers = timeout.timers;
timers.delete(timerId);
if (timers.size === 0) {
clearTimeout(timeout.id);
}
}
},
getFocusableElementsInScope(): Array<HTMLElement> {
validateResponderContext();
const focusableElements = [];
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
const child = ((eventComponentInstance.currentFiber: any): Fiber).child;
if (child !== null) {
collectFocusableElements(child, focusableElements);
}
return focusableElements;
},
getActiveDocument,
objectAssign: Object.assign,
getEventPointerType(
event: ReactResponderEvent,
): '' | 'mouse' | 'keyboard' | 'pen' | 'touch' {
validateResponderContext();
const nativeEvent: any = event.nativeEvent;
const {type, pointerType} = nativeEvent;
if (pointerType != null) {
return pointerType;
}
if (type.indexOf('mouse') === 0) {
return 'mouse';
}
if (type.indexOf('touch') === 0) {
return 'touch';
}
if (type.indexOf('key') === 0) {
return 'keyboard';
}
return '';
},
getEventCurrentTarget(event: ReactResponderEvent): Element {
validateResponderContext();
const target = event.target;
let fiber = getClosestInstanceFromNode(target);
let hostComponent = target;
while (fiber !== null) {
if (fiber.stateNode === currentInstance) {
break;
}
if (fiber.tag === HostComponent) {
hostComponent = fiber.stateNode;
}
fiber = fiber.return;
}
return ((hostComponent: any): Element);
},
getTimeStamp(): number {
validateResponderContext();
return currentTimeStamp;
},
isTargetWithinHostComponent(
target: Element | Document,
elementType: string,
): boolean {
validateResponderContext();
let fiber = getClosestInstanceFromNode(target);
while (fiber !== null) {
if (fiber.stateNode === currentInstance) {
return false;
}
if (fiber.tag === HostComponent && fiber.type === elementType) {
return true;
}
fiber = fiber.return;
}
return false;
},
};
function collectFocusableElements(
node: Fiber,
focusableElements: Array<HTMLElement>,
): void {
if (isFiberSuspenseAndTimedOut(node)) {
const fallbackChild = getSuspenseFallbackChild(node);
if (fallbackChild !== null) {
collectFocusableElements(fallbackChild, focusableElements);
}
} else {
if (isFiberHostComponentFocusable(node)) {
focusableElements.push(node.stateNode);
} else {
const child = node.child;
if (child !== null) {
collectFocusableElements(child, focusableElements);
}
}
}
const sibling = node.sibling;
if (sibling !== null) {
collectFocusableElements(sibling, focusableElements);
}
}
function isTargetWithinEventComponent(target: Element | Document): boolean {
validateResponderContext();
if (target != null) {
let fiber = getClosestInstanceFromNode(target);
while (fiber !== null) {
if (fiber.stateNode === currentInstance) {
return true;
}
fiber = fiber.return;
}
}
return false;
}
function getActiveDocument(): Document {
const eventComponentInstance = ((currentInstance: any): ReactEventComponentInstance);
const rootElement = ((eventComponentInstance.rootInstance: any): Element);
return rootElement.ownerDocument;
}
function releaseOwnershipForEventComponentInstance(
eventComponentInstance: ReactEventComponentInstance,
): boolean {
const responder = eventComponentInstance.responder;
let triggerOwnershipListenersWith;
if (responderOwners.get(responder) === eventComponentInstance) {
responderOwners.delete(responder);
triggerOwnershipListenersWith = responder;
}
if (globalOwner === eventComponentInstance) {
globalOwner = null;
triggerOwnershipListenersWith = null;
}
if (triggerOwnershipListenersWith !== undefined) {
triggerOwnershipListeners(triggerOwnershipListenersWith);
return true;
} else {
return false;
}
}
function isFiberHostComponentFocusable(fiber: Fiber): boolean {
if (fiber.tag !== HostComponent) {
return false;
}
const {type, memoizedProps} = fiber;
if (memoizedProps.tabIndex === -1 || memoizedProps.disabled) {
return false;
}
if (memoizedProps.tabIndex === 0 || memoizedProps.contentEditable === true) {
return true;
}
if (type === 'a' || type === 'area') {
return !!memoizedProps.href && memoizedProps.rel !== 'ignore';
}
if (type === 'input') {
return memoizedProps.type !== 'hidden' && memoizedProps.type !== 'file';
}
return (
type === 'button' ||
type === 'textarea' ||
type === 'object' ||
type === 'select' ||
type === 'iframe' ||
type === 'embed'
);
}
function processTimers(
timers: Map<Symbol, ResponderTimer>,
delay: number,
): void {
const timersArr = Array.from(timers.values());
currentEventQueue = createEventQueue();
try {
for (let i = 0; i < timersArr.length; i++) {
const {instance, func, id, timeStamp} = timersArr[i];
currentInstance = instance;
currentTimeStamp = timeStamp + delay;
try {
func();
} finally {
activeTimeouts.delete(id);
}
}
processEventQueue();
} finally {
currentTimers = null;
currentInstance = null;
currentEventQueue = null;
currentTimeStamp = 0;
}
}
function createResponderEvent(
topLevelType: string,
nativeEvent: AnyNativeEvent,
nativeEventTarget: Element | Document,
passive: boolean,
passiveSupported: boolean,
): ReactResponderEvent {
const responderEvent = {
nativeEvent: nativeEvent,
target: nativeEventTarget,
type: topLevelType,
passive,
passiveSupported,
};
if (__DEV__) {
Object.freeze(responderEvent);
}
return responderEvent;
}
function createEventQueue(): EventQueue {
return {
events: [],
discrete: false,
};
}
function processEvent(event: $Shape<PartialEventObject>): void {
const type = event.type;
const listener = ((eventListeners.get(event): any): (
$Shape<PartialEventObject>,
) => void);
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
}
function processEvents(events: Array<EventObjectType>): void {
for (let i = 0, length = events.length; i < length; i++) {
processEvent(events[i]);
}
}
export function processEventQueue(): void {
const {events, discrete} = ((currentEventQueue: any): EventQueue);
if (events.length === 0) {
return;
}
if (discrete) {
if (shouldflushDiscreteUpdates(currentTimeStamp)) {
flushDiscreteUpdates();
}
discreteUpdates(() => {
batchedEventUpdates(processEvents, events);
});
} else {
batchedEventUpdates(processEvents, events);
}
}
function getTargetEventTypesSet(
eventTypes: Array<ReactEventResponderEventType>,
): Set<string> {
let cachedSet = targetEventTypeCached.get(eventTypes);
if (cachedSet === undefined) {
cachedSet = new Set();
for (let i = 0; i < eventTypes.length; i++) {
const eventType = eventTypes[i];
let name = eventType;
let passive = true;
if (typeof eventType !== 'string') {
const targetEventConfigObject = ((eventType: any): {
name: string,
passive?: boolean,
});
name = targetEventConfigObject.name;
if (targetEventConfigObject.passive !== undefined) {
passive = targetEventConfigObject.passive;
}
}
const listeningName = generateListeningKey(
((name: any): string),
passive,
);
cachedSet.add(listeningName);
}
targetEventTypeCached.set(eventTypes, cachedSet);
}
return cachedSet;
}
function getTargetEventResponderInstances(
listeningName: string,
targetFiber: null | Fiber,
): Array<ReactEventComponentInstance> {
const eventResponderInstances = [];
let node = targetFiber;
while (node !== null) {
// Traverse up the fiber tree till we find event component fibers.
if (node.tag === EventComponent) {
const eventComponentInstance = node.stateNode;
const responder = eventComponentInstance.responder;
const targetEventTypes = responder.targetEventTypes;
// Validate the target event type exists on the responder
if (targetEventTypes !== undefined) {
const targetEventTypesSet = getTargetEventTypesSet(targetEventTypes);
if (targetEventTypesSet.has(listeningName)) {
eventResponderInstances.push(eventComponentInstance);
}
}
}
node = node.return;
}
return eventResponderInstances;
}
function getRootEventResponderInstances(
listeningName: string,
): Array<ReactEventComponentInstance> {
const eventResponderInstances = [];
const rootEventInstances = rootEventTypesToEventComponentInstances.get(
listeningName,
);
if (rootEventInstances !== undefined) {
const rootEventComponentInstances = Array.from(rootEventInstances);
for (let i = 0; i < rootEventComponentInstances.length; i++) {
const rootEventComponentInstance = rootEventComponentInstances[i];
eventResponderInstances.push(rootEventComponentInstance);
}
}
return eventResponderInstances;
}
function shouldSkipEventComponent(
eventResponderInstance: ReactEventComponentInstance,
propagatedEventResponders: null | Set<ReactEventResponder>,
): boolean {
const responder = eventResponderInstance.responder;
if (propagatedEventResponders !== null && responder.stopLocalPropagation) {
if (propagatedEventResponders.has(responder)) {
return true;
}
propagatedEventResponders.add(responder);
}
if (globalOwner && globalOwner !== eventResponderInstance) {
return true;
}
if (
responderOwners.has(responder) &&
responderOwners.get(responder) !== eventResponderInstance
) {
return true;
}
return false;
}
function traverseAndHandleEventResponderInstances(
topLevelType: DOMTopLevelEventType,
targetFiber: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
eventSystemFlags: EventSystemFlags,
): void {
const isPassiveEvent = (eventSystemFlags & IS_PASSIVE) !== 0;
const isPassiveSupported = (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0;
const listeningName = generateListeningKey(
((topLevelType: any): string),
isPassiveEvent || !isPassiveSupported,
);
// Trigger event responders in this order:
// - Capture target phase
// - Bubble target phase
// - Root phase
const targetEventResponderInstances = getTargetEventResponderInstances(
listeningName,
targetFiber,
);
const responderEvent = createResponderEvent(
((topLevelType: any): string),
nativeEvent,
((nativeEventTarget: any): Element | Document),
isPassiveEvent,
isPassiveSupported,
);
const propagatedEventResponders: Set<ReactEventResponder> = new Set();
let length = targetEventResponderInstances.length;
let i;
// Captured and bubbled event phases have the notion of local propagation.
// This means that the propgation chain can be stopped part of the the way
// through processing event component instances. The major difference to other
// events systems is that the stopping of propgation is localized to a single
// phase, rather than both phases.
if (length > 0) {
// Capture target phase
for (i = length; i-- > 0; ) {
const targetEventResponderInstance = targetEventResponderInstances[i];
const {responder, props, state} = targetEventResponderInstance;
const eventListener = responder.onEventCapture;
if (eventListener !== undefined) {
if (
shouldSkipEventComponent(
targetEventResponderInstance,
propagatedEventResponders,
)
) {
continue;
}
currentInstance = targetEventResponderInstance;
eventListener(responderEvent, eventResponderContext, props, state);
}
}
// We clean propagated event responders between phases.
propagatedEventResponders.clear();
// Bubble target phase
for (i = 0; i < length; i++) {
const targetEventResponderInstance = targetEventResponderInstances[i];
const {responder, props, state} = targetEventResponderInstance;
const eventListener = responder.onEvent;
if (eventListener !== undefined) {
if (
shouldSkipEventComponent(
targetEventResponderInstance,
propagatedEventResponders,
)
) {
continue;
}
currentInstance = targetEventResponderInstance;
eventListener(responderEvent, eventResponderContext, props, state);
}
}
}
// Root phase
const rootEventResponderInstances = getRootEventResponderInstances(
listeningName,
);
length = rootEventResponderInstances.length;
if (length > 0) {
for (i = 0; i < length; i++) {
const rootEventResponderInstance = rootEventResponderInstances[i];
const {responder, props, state} = rootEventResponderInstance;
const eventListener = responder.onRootEvent;
if (eventListener !== undefined) {
if (shouldSkipEventComponent(rootEventResponderInstance, null)) {
continue;
}
currentInstance = rootEventResponderInstance;
eventListener(responderEvent, eventResponderContext, props, state);
}
}
}
}
function triggerOwnershipListeners(
limitByResponder: null | ReactEventResponder,
): void {
const listeningInstances = Array.from(ownershipChangeListeners);
const previousInstance = currentInstance;
try {
for (let i = 0; i < listeningInstances.length; i++) {
const instance = listeningInstances[i];
const {props, responder, state} = instance;
if (limitByResponder !== null && limitByResponder !== responder) {
continue;
}
currentInstance = instance;
const onOwnershipChange = responder.onOwnershipChange;
if (onOwnershipChange !== undefined) {
onOwnershipChange(eventResponderContext, props, state);
}
}
} finally {
currentInstance = previousInstance;
}
}
export function mountEventResponder(
eventComponentInstance: ReactEventComponentInstance,
) {
const responder = eventComponentInstance.responder;
if (responder.onOwnershipChange !== undefined) {
ownershipChangeListeners.add(eventComponentInstance);
}
const onMount = responder.onMount;
if (onMount !== undefined) {
let {props, state} = eventComponentInstance;
currentEventQueue = createEventQueue();
currentInstance = eventComponentInstance;
try {
onMount(eventResponderContext, props, state);
} finally {
currentEventQueue = null;
currentInstance = null;
currentTimers = null;
}
}
}
export function unmountEventResponder(
eventComponentInstance: ReactEventComponentInstance,
): void {
const responder = eventComponentInstance.responder;
const onUnmount = responder.onUnmount;
if (onUnmount !== undefined) {
let {props, state} = eventComponentInstance;
currentEventQueue = createEventQueue();
currentInstance = eventComponentInstance;
try {
onUnmount(eventResponderContext, props, state);
} finally {
currentEventQueue = null;
currentInstance = null;
currentTimers = null;
}
}
try {
currentEventQueue = createEventQueue();
releaseOwnershipForEventComponentInstance(eventComponentInstance);
processEventQueue();
} finally {
currentEventQueue = null;
}
if (responder.onOwnershipChange !== undefined) {
ownershipChangeListeners.delete(eventComponentInstance);
}
const rootEventTypesSet = eventComponentInstance.rootEventTypes;
if (rootEventTypesSet !== null) {
const rootEventTypes = Array.from(rootEventTypesSet);
for (let i = 0; i < rootEventTypes.length; i++) {
const topLevelEventType = rootEventTypes[i];
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
topLevelEventType,
);
if (rootEventComponentInstances !== undefined) {
rootEventComponentInstances.delete(eventComponentInstance);
}
}
}
}
function validateResponderContext(): void {
invariant(
currentEventQueue && currentInstance,
'An event responder context was used outside of an event cycle. ' +
'Use context.setTimeout() to use asynchronous responder context outside of event cycle .',
);
}
export function dispatchEventForResponderEventSystem(
topLevelType: DOMTopLevelEventType,
targetFiber: null | Fiber,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
eventSystemFlags: EventSystemFlags,
): void {
if (enableEventAPI) {
const previousEventQueue = currentEventQueue;
const previousInstance = currentInstance;
const previousTimers = currentTimers;
const previousTimeStamp = currentTimeStamp;
currentTimers = null;
currentEventQueue = createEventQueue();
// We might want to control timeStamp another way here
currentTimeStamp = (nativeEvent: any).timeStamp;
try {
traverseAndHandleEventResponderInstances(
topLevelType,
targetFiber,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
);
processEventQueue();
} finally {
currentTimers = previousTimers;
currentInstance = previousInstance;
currentEventQueue = previousEventQueue;
currentTimeStamp = previousTimeStamp;
}
}
}
export function addRootEventTypesForComponentInstance(
eventComponentInstance: ReactEventComponentInstance,
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
registerRootEventType(rootEventType, eventComponentInstance);
}
}
function registerRootEventType(
rootEventType: ReactEventResponderEventType,
eventComponentInstance: ReactEventComponentInstance,
): void {
let name = rootEventType;
let passive = true;
if (typeof rootEventType !== 'string') {
const targetEventConfigObject = ((rootEventType: any): {
name: string,
passive?: boolean,
});
name = targetEventConfigObject.name;
if (targetEventConfigObject.passive !== undefined) {
passive = targetEventConfigObject.passive;
}
}
const listeningName = generateListeningKey(((name: any): string), passive);
let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
listeningName,
);
if (rootEventComponentInstances === undefined) {
rootEventComponentInstances = new Set();
rootEventTypesToEventComponentInstances.set(
listeningName,
rootEventComponentInstances,
);
}
let rootEventTypesSet = eventComponentInstance.rootEventTypes;
if (rootEventTypesSet === null) {
rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
}
invariant(
!rootEventTypesSet.has(listeningName),
'addRootEventTypes() found a duplicate root event ' +
'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
'array or because of a previous addRootEventTypes() using this root event type.',
name,
);
rootEventTypesSet.add(listeningName);
rootEventComponentInstances.add(
((eventComponentInstance: any): ReactEventComponentInstance),
);
}
export function generateListeningKey(
topLevelType: string,
passive: boolean,
): string {
// Create a unique name for this event, plus its properties. We'll
// use this to ensure we don't listen to the same event with the same
// properties again.
const passiveKey = passive ? '_passive' : '_active';
return `${topLevelType}${passiveKey}`;
}
let lastDiscreteEventTimeStamp = 0;
export function shouldflushDiscreteUpdates(timeStamp: number): boolean {
// event.timeStamp isn't overly reliable due to inconsistencies in
// how different browsers have historically provided the time stamp.
// Some browsers provide high-resolution time stamps for all events,
// some provide low-resoltion time stamps for all events. FF < 52
// even mixes both time stamps together. Some browsers even report
// negative time stamps or time stamps that are 0 (iOS9) in some cases.
// Given we are only comparing two time stamps with equality (!==),
// we are safe from the resolution differences. If the time stamp is 0
// we bail-out of preventing the flush, which can affect semantics,
// such as if an earlier flush removes or adds event listeners that
// are fired in the subsequent flush. However, this is the same
// behaviour as we had before this change, so the risks are low.
if (timeStamp === 0 || lastDiscreteEventTimeStamp !== timeStamp) {
lastDiscreteEventTimeStamp = timeStamp;
return true;
}
return false;
}