mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
ReactDOM.useEvent: add EventTarget support (#18355)
* ReactDOM.useEvent: add support for all EventTarget types
This commit is contained in:
@@ -64,7 +64,8 @@ if (__DEV__) {
|
||||
*/
|
||||
export function executeDispatch(event, listener, inst) {
|
||||
const type = event.type || 'unknown-event';
|
||||
event.currentTarget = getNodeFromInstance(inst);
|
||||
event.currentTarget =
|
||||
inst.tag !== undefined ? getNodeFromInstance(inst) : inst;
|
||||
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
|
||||
event.currentTarget = null;
|
||||
}
|
||||
|
||||
@@ -16,4 +16,5 @@ export const IS_ACTIVE = 1 << 3;
|
||||
export const PASSIVE_NOT_SUPPORTED = 1 << 4;
|
||||
export const IS_REPLAYED = 1 << 5;
|
||||
export const IS_FIRST_ANCESTOR = 1 << 6;
|
||||
export const LEGACY_FB_SUPPORT = 1 << 7;
|
||||
export const IS_TARGET_EVENT_ONLY = 1 << 7;
|
||||
export const LEGACY_FB_SUPPORT = 1 << 8;
|
||||
|
||||
@@ -39,7 +39,7 @@ export type ReactSyntheticEvent = {|
|
||||
nativeEventTarget: EventTarget,
|
||||
) => ReactSyntheticEvent,
|
||||
isPersistent: () => boolean,
|
||||
_dispatchInstances: null | Array<Fiber> | Fiber,
|
||||
_dispatchInstances: null | Array<Fiber | EventTarget> | Fiber | EventTarget,
|
||||
_dispatchListeners: null | Array<Function> | Function,
|
||||
_targetInst: Fiber,
|
||||
type: string,
|
||||
|
||||
+17
-25
@@ -75,11 +75,13 @@ import {
|
||||
IS_PASSIVE,
|
||||
} from 'legacy-events/EventSystemFlags';
|
||||
import {
|
||||
attachElementListener,
|
||||
detachElementListener,
|
||||
isDOMDocument,
|
||||
isDOMElement,
|
||||
isManagedDOMElement,
|
||||
isValidEventTarget,
|
||||
listenToTopLevelEvent,
|
||||
detachListenerFromManagedDOMElement,
|
||||
attachListenerFromManagedDOMElement,
|
||||
detachTargetEventListener,
|
||||
attachTargetEventListener,
|
||||
} from '../events/DOMModernPluginEventSystem';
|
||||
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
|
||||
|
||||
@@ -1122,12 +1124,10 @@ export function registerEvent(
|
||||
export function mountEventListener(listener: ReactDOMListener): void {
|
||||
if (enableUseEventAPI) {
|
||||
const {target} = listener;
|
||||
if (target === window) {
|
||||
// TODO (useEvent)
|
||||
} else if (isDOMDocument(target)) {
|
||||
// TODO (useEvent)
|
||||
} else if (isDOMElement(target)) {
|
||||
attachElementListener(listener);
|
||||
if (isManagedDOMElement(target)) {
|
||||
attachListenerFromManagedDOMElement(listener);
|
||||
} else {
|
||||
attachTargetEventListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1135,12 +1135,10 @@ export function mountEventListener(listener: ReactDOMListener): void {
|
||||
export function unmountEventListener(listener: ReactDOMListener): void {
|
||||
if (enableUseEventAPI) {
|
||||
const {target} = listener;
|
||||
if (target === window) {
|
||||
// TODO (useEvent)
|
||||
} else if (isDOMDocument(target)) {
|
||||
// TODO (useEvent)
|
||||
} else if (isDOMElement(target)) {
|
||||
detachElementListener(listener);
|
||||
if (isManagedDOMElement(target)) {
|
||||
detachListenerFromManagedDOMElement(listener);
|
||||
} else {
|
||||
detachTargetEventListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1152,10 +1150,7 @@ export function validateEventListenerTarget(
|
||||
if (enableUseEventAPI) {
|
||||
if (
|
||||
target != null &&
|
||||
(target === window ||
|
||||
isDOMDocument(target) ||
|
||||
(isDOMElement(target) &&
|
||||
getClosestInstanceFromNode(((target: any): Element))))
|
||||
(isManagedDOMElement(target) || isValidEventTarget(target))
|
||||
) {
|
||||
if (listener == null || typeof listener === 'function') {
|
||||
return true;
|
||||
@@ -1169,11 +1164,8 @@ export function validateEventListenerTarget(
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Event listener method setListener() from useEvent() hook requires the first argument to be either:' +
|
||||
'\n\n' +
|
||||
'1. A valid DOM node that was rendered and managed by React\n' +
|
||||
'2. The "window" object\n' +
|
||||
'3. The "document" object',
|
||||
'Event listener method setListener() from useEvent() hook requires the first argument to be ' +
|
||||
'a valid DOM EventTarget. If using a ref, ensure the current value is not null.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -12,11 +12,11 @@ import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
|
||||
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
|
||||
|
||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||
// prettier-ignore
|
||||
const elementListenerMap:
|
||||
// $FlowFixMe Work around Flow bug
|
||||
| WeakMap
|
||||
| Map<EventTarget, ElementListenerMap> = new PossiblyWeakMap();
|
||||
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
|
||||
const elementListenerMap: WeakMap<
|
||||
EventTarget,
|
||||
ElementListenerMap,
|
||||
> = new PossiblyWeakMap();
|
||||
|
||||
export type ElementListenerMap = Map<
|
||||
DOMTopLevelEventType | string,
|
||||
|
||||
+186
-117
@@ -27,7 +27,11 @@ import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
|
||||
import {batchedEventUpdates} from 'legacy-events/ReactGenericBatching';
|
||||
import {executeDispatchesInOrder} from 'legacy-events/EventPluginUtils';
|
||||
import {plugins} from 'legacy-events/EventPluginRegistry';
|
||||
import {LEGACY_FB_SUPPORT, IS_REPLAYED} from 'legacy-events/EventSystemFlags';
|
||||
import {
|
||||
LEGACY_FB_SUPPORT,
|
||||
IS_REPLAYED,
|
||||
IS_TARGET_EVENT_ONLY,
|
||||
} from 'legacy-events/EventSystemFlags';
|
||||
|
||||
import {HostRoot, HostPortal} from 'shared/ReactWorkTags';
|
||||
|
||||
@@ -77,11 +81,7 @@ import {
|
||||
getListenersFromTarget,
|
||||
initListenersSet,
|
||||
} from '../client/ReactDOMComponentTree';
|
||||
import {
|
||||
DOCUMENT_NODE,
|
||||
COMMENT_NODE,
|
||||
ELEMENT_NODE,
|
||||
} from '../shared/HTMLNodeType';
|
||||
import {COMMENT_NODE} from '../shared/HTMLNodeType';
|
||||
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
|
||||
|
||||
import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
|
||||
@@ -132,6 +132,17 @@ const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
|
||||
|
||||
const isArray = Array.isArray;
|
||||
|
||||
// $FlowFixMe: Flow struggles with this pattern
|
||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
|
||||
export const eventTargetEventListenerStore: WeakMap<
|
||||
EventTarget,
|
||||
Map<
|
||||
DOMTopLevelEventType,
|
||||
{bubbled: Set<ReactDOMListener>, captured: Set<ReactDOMListener>},
|
||||
>,
|
||||
> = new PossiblyWeakMap();
|
||||
|
||||
function dispatchEventsForPlugins(
|
||||
topLevelType: DOMTopLevelEventType,
|
||||
eventSystemFlags: EventSystemFlags,
|
||||
@@ -198,11 +209,27 @@ export function listenToTopLevelEvent(
|
||||
listenerMap: ElementListenerMap,
|
||||
passive?: boolean,
|
||||
priority?: EventPriority,
|
||||
capture?: boolean,
|
||||
): void {
|
||||
const listenerEntry = listenerMap.get(topLevelType);
|
||||
// If we explicitly define capture, then these are for EventTarget objects,
|
||||
// rather than React managed DOM elements. So we need to ensure we separate
|
||||
// capture and non-capture events. For React managed DOM nodes we only use
|
||||
// one or the other, never both. Which one we use is determined by the the
|
||||
// capturePhaseEvents Set (in this module) that defines if the event listener
|
||||
// should use the capture phase – otherwise we always use the bubble phase.
|
||||
// Finally, when we get to dispatching and accumulating event listeners, we
|
||||
// check if the user wanted capture/bubble and emulate the behavior at that
|
||||
// point (we call this accumulating two phase listeners).
|
||||
const typeStr = ((topLevelType: any): string);
|
||||
const listenerMapKey =
|
||||
capture === undefined
|
||||
? topLevelType
|
||||
: `${typeStr}_${capture ? 'capture' : 'bubble'}`;
|
||||
const listenerEntry = listenerMap.get(listenerMapKey);
|
||||
const shouldUpgrade = shouldUpgradeListener(listenerEntry, passive);
|
||||
if (listenerEntry === undefined || shouldUpgrade) {
|
||||
const isCapturePhase = capturePhaseEvents.has(topLevelType);
|
||||
const isCapturePhase =
|
||||
capture === undefined ? capturePhaseEvents.has(topLevelType) : capture;
|
||||
// If we should upgrade, then we need to remove the existing trapped
|
||||
// event listener for the target container.
|
||||
if (shouldUpgrade) {
|
||||
@@ -221,7 +248,7 @@ export function listenToTopLevelEvent(
|
||||
passive,
|
||||
priority,
|
||||
);
|
||||
listenerMap.set(topLevelType, {passive, listener});
|
||||
listenerMap.set(listenerMapKey, {passive, listener});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,14 +296,12 @@ function isMatchingRootContainer(
|
||||
);
|
||||
}
|
||||
|
||||
export function isDOMElement(target: EventTarget): boolean {
|
||||
const nodeType = ((target: any): Node).nodeType;
|
||||
return (nodeType: any) && nodeType === ELEMENT_NODE;
|
||||
export function isManagedDOMElement(target: EventTarget): boolean {
|
||||
return getClosestInstanceFromNode(((target: any): Node)) !== null;
|
||||
}
|
||||
|
||||
export function isDOMDocument(target: EventTarget): boolean {
|
||||
const nodeType = ((target: any): Node).nodeType;
|
||||
return nodeType === DOCUMENT_NODE;
|
||||
export function isValidEventTarget(target: EventTarget): boolean {
|
||||
return typeof target.addEventListener === 'function';
|
||||
}
|
||||
|
||||
export function dispatchEventForPluginEventSystem(
|
||||
@@ -288,87 +313,85 @@ export function dispatchEventForPluginEventSystem(
|
||||
): void {
|
||||
let ancestorInst = targetInst;
|
||||
if (targetContainer !== null) {
|
||||
const possibleTargetContainerNode = ((targetContainer: any): Node);
|
||||
// Given the rootContainer can be any EventTarget, if the
|
||||
// target is not a valid DOM element then we'll skip this part.
|
||||
if (
|
||||
possibleTargetContainerNode === window ||
|
||||
!isDOMElement(possibleTargetContainerNode)
|
||||
) {
|
||||
// TODO: useEvent for document and window
|
||||
return;
|
||||
}
|
||||
// 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 &&
|
||||
// We do not want to defer if the event system has already been
|
||||
// set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
|
||||
// we call willDeferLaterForLegacyFBSupport, thus not bailing out
|
||||
// will result in endless cycles like an infinite loop.
|
||||
(eventSystemFlags & LEGACY_FB_SUPPORT) === 0 &&
|
||||
// We also don't want to defer during event replaying.
|
||||
(eventSystemFlags & IS_REPLAYED) === 0 &&
|
||||
willDeferLaterForLegacyFBSupport(topLevelType, 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 = targetInst;
|
||||
if (eventTargetEventListenerStore.has(targetContainer)) {
|
||||
// For TargetEvent nodes (i.e. document, window)
|
||||
ancestorInst = null;
|
||||
eventSystemFlags |= IS_TARGET_EVENT_ONLY;
|
||||
} else {
|
||||
const targetContainerNode = ((targetContainer: any): Node);
|
||||
|
||||
while (true) {
|
||||
if (node === null) {
|
||||
return;
|
||||
}
|
||||
if (node.tag === HostRoot || node.tag === HostPortal) {
|
||||
const container = node.stateNode.containerInfo;
|
||||
if (isMatchingRootContainer(container, possibleTargetContainerNode)) {
|
||||
break;
|
||||
}
|
||||
if (node.tag === 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) {
|
||||
if (grandNode.tag === HostRoot || grandNode.tag === HostPortal) {
|
||||
const grandContainer = grandNode.stateNode.containerInfo;
|
||||
if (
|
||||
isMatchingRootContainer(
|
||||
grandContainer,
|
||||
possibleTargetContainerNode,
|
||||
)
|
||||
) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
const parentSubtreeInst = getClosestInstanceFromNode(container);
|
||||
if (parentSubtreeInst === null) {
|
||||
// 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 &&
|
||||
// We do not want to defer if the event system has already been
|
||||
// set to LEGACY_FB_SUPPORT. LEGACY_FB_SUPPORT only gets set when
|
||||
// we call willDeferLaterForLegacyFBSupport, thus not bailing out
|
||||
// will result in endless cycles like an infinite loop.
|
||||
(eventSystemFlags & LEGACY_FB_SUPPORT) === 0 &&
|
||||
// We also don't want to defer during event replaying.
|
||||
(eventSystemFlags & IS_REPLAYED) === 0 &&
|
||||
willDeferLaterForLegacyFBSupport(topLevelType, 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 = targetInst;
|
||||
|
||||
while (true) {
|
||||
if (node === null) {
|
||||
return;
|
||||
}
|
||||
node = ancestorInst = parentSubtreeInst;
|
||||
continue;
|
||||
if (node.tag === HostRoot || node.tag === HostPortal) {
|
||||
const container = node.stateNode.containerInfo;
|
||||
if (isMatchingRootContainer(container, targetContainerNode)) {
|
||||
break;
|
||||
}
|
||||
if (node.tag === 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) {
|
||||
if (
|
||||
grandNode.tag === HostRoot ||
|
||||
grandNode.tag === 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;
|
||||
}
|
||||
}
|
||||
const parentSubtreeInst = getClosestInstanceFromNode(container);
|
||||
if (parentSubtreeInst === null) {
|
||||
return;
|
||||
}
|
||||
node = ancestorInst = parentSubtreeInst;
|
||||
continue;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -398,21 +421,32 @@ function getNearestRootOrPortalContainer(instance: Element): Element {
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function attachElementListener(listener: ReactDOMListener): void {
|
||||
function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void {
|
||||
let dispatchConfig = topLevelEventsToDispatchConfig.get(type);
|
||||
// If we don't have a dispatchConfig, then we're dealing with
|
||||
// an event type that React does not know about (i.e. a custom event).
|
||||
// We need to register an event config for this or the SimpleEventPlugin
|
||||
// will not appropriately provide a SyntheticEvent, so we use out empty
|
||||
// dispatch config for custom events.
|
||||
if (dispatchConfig === undefined) {
|
||||
topLevelEventsToDispatchConfig.set(
|
||||
type,
|
||||
emptyDispatchConfigForCustomEvents,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function attachListenerFromManagedDOMElement(
|
||||
listener: ReactDOMListener,
|
||||
): void {
|
||||
const {event, target} = listener;
|
||||
const {passive, priority, type} = event;
|
||||
const possibleManagedTarget = ((target: any): Element);
|
||||
let containerEventTarget = target;
|
||||
// If we the target is a managed React element, then we need to
|
||||
// find the nearest root/portal contained to attach the event listener
|
||||
// to. If it's not managed, i.e. the window, then we just attach
|
||||
// the listener to the target.
|
||||
if (isDOMElement(target)) {
|
||||
const possibleManagedTarget = ((target: any): Element);
|
||||
if (getClosestInstanceFromNode(possibleManagedTarget)) {
|
||||
containerEventTarget = getNearestRootOrPortalContainer(
|
||||
possibleManagedTarget,
|
||||
);
|
||||
}
|
||||
if (getClosestInstanceFromNode(possibleManagedTarget)) {
|
||||
containerEventTarget = getNearestRootOrPortalContainer(
|
||||
possibleManagedTarget,
|
||||
);
|
||||
}
|
||||
const listenerMap = getListenerMapForElement(containerEventTarget);
|
||||
// Add the event listener to the target container (falling back to
|
||||
@@ -434,21 +468,12 @@ export function attachElementListener(listener: ReactDOMListener): void {
|
||||
// Add our listener to the listeners Set.
|
||||
listeners.add(listener);
|
||||
// Finally, add the event to our known event types list.
|
||||
let dispatchConfig = topLevelEventsToDispatchConfig.get(type);
|
||||
// If we don't have a dispatchConfig, then we're dealing with
|
||||
// an event type that React does not know about (i.e. a custom event).
|
||||
// We need to register an event config for this or the SimpleEventPlugin
|
||||
// will not appropriately provide a SyntheticEvent, so we use out empty
|
||||
// dispatch config for custom events.
|
||||
if (dispatchConfig === undefined) {
|
||||
topLevelEventsToDispatchConfig.set(
|
||||
type,
|
||||
emptyDispatchConfigForCustomEvents,
|
||||
);
|
||||
}
|
||||
addEventTypeToDispatchConfig(type);
|
||||
}
|
||||
|
||||
export function detachElementListener(listener: ReactDOMListener): void {
|
||||
export function detachListenerFromManagedDOMElement(
|
||||
listener: ReactDOMListener,
|
||||
): void {
|
||||
const {target} = listener;
|
||||
// Get the internal listeners Set from the target instance.
|
||||
const listeners = getListenersFromTarget(target);
|
||||
@@ -457,3 +482,47 @@ export function detachElementListener(listener: ReactDOMListener): void {
|
||||
listeners.delete(listener);
|
||||
}
|
||||
}
|
||||
|
||||
export function attachTargetEventListener(listener: ReactDOMListener): void {
|
||||
const {event, target} = listener;
|
||||
const {capture, passive, priority, type} = event;
|
||||
const listenerMap = getListenerMapForElement(target);
|
||||
// Add the event listener to the TargetEvent object.
|
||||
listenToTopLevelEvent(type, target, listenerMap, passive, priority, capture);
|
||||
let eventTypeMap = eventTargetEventListenerStore.get(target);
|
||||
if (eventTypeMap === undefined) {
|
||||
eventTypeMap = new Map();
|
||||
eventTargetEventListenerStore.set(target, eventTypeMap);
|
||||
}
|
||||
// Get the listeners by the event type
|
||||
let listeners = eventTypeMap.get(type);
|
||||
if (listeners === undefined) {
|
||||
listeners = {captured: new Set(), bubbled: new Set()};
|
||||
eventTypeMap.set(type, listeners);
|
||||
}
|
||||
// Add our listener to the listeners Set.
|
||||
if (capture) {
|
||||
listeners.captured.add(listener);
|
||||
} else {
|
||||
listeners.bubbled.add(listener);
|
||||
}
|
||||
// Finally, add the event to our known event types list.
|
||||
addEventTypeToDispatchConfig(type);
|
||||
}
|
||||
|
||||
export function detachTargetEventListener(listener: ReactDOMListener): void {
|
||||
const {event, target} = listener;
|
||||
const {capture, type} = event;
|
||||
const eventTypeMap = eventTargetEventListenerStore.get(target);
|
||||
if (eventTypeMap !== undefined) {
|
||||
const listeners = eventTypeMap.get(type);
|
||||
if (listeners !== undefined) {
|
||||
// Remove out listener from the listeners Set.
|
||||
if (capture) {
|
||||
listeners.captured.delete(listener);
|
||||
} else {
|
||||
listeners.bubbled.delete(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -83,6 +83,7 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
|
||||
nativeEvent: MouseEvent,
|
||||
nativeEventTarget: null | EventTarget,
|
||||
eventSystemFlags: EventSystemFlags,
|
||||
targetContainer?: null | EventTarget,
|
||||
): null | ReactSyntheticEvent {
|
||||
const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);
|
||||
if (!dispatchConfig) {
|
||||
@@ -194,7 +195,7 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
|
||||
nativeEvent,
|
||||
nativeEventTarget,
|
||||
);
|
||||
accumulateTwoPhaseListeners(event, true);
|
||||
accumulateTwoPhaseListeners(event, true, eventSystemFlags, targetContainer);
|
||||
return event;
|
||||
},
|
||||
};
|
||||
|
||||
+285
-6
@@ -25,6 +25,27 @@ function dispatchClickEvent(element) {
|
||||
dispatchEvent(element, 'click');
|
||||
}
|
||||
|
||||
let eventListenersToClear = [];
|
||||
|
||||
function startNativeEventListenerClearDown() {
|
||||
const nativeWindowEventListener = window.addEventListener;
|
||||
window.addEventListener = function(...params) {
|
||||
eventListenersToClear.push({target: window, params});
|
||||
return nativeWindowEventListener.apply(this, params);
|
||||
};
|
||||
const nativeDocumentEventListener = document.addEventListener;
|
||||
document.addEventListener = function(...params) {
|
||||
eventListenersToClear.push({target: document, params});
|
||||
return nativeDocumentEventListener.apply(this, params);
|
||||
};
|
||||
}
|
||||
|
||||
function endNativeEventListenerClearDown() {
|
||||
eventListenersToClear.forEach(({target, params}) => {
|
||||
target.removeEventListener(...params);
|
||||
});
|
||||
}
|
||||
|
||||
describe('DOMModernPluginEventSystem', () => {
|
||||
let container;
|
||||
|
||||
@@ -45,11 +66,13 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
startNativeEventListenerClearDown();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
endNativeEventListenerClearDown();
|
||||
});
|
||||
|
||||
it('handle propagation of click events', () => {
|
||||
@@ -1328,14 +1351,18 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['bubble', buttonElement]);
|
||||
|
||||
log.length = 0;
|
||||
onClick.mockClear();
|
||||
onClickCapture.mockClear();
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[2]).toEqual(['capture', buttonElement]);
|
||||
expect(log[3]).toEqual(['capture', divElement]);
|
||||
expect(log[4]).toEqual(['bubble', divElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
expect(onClick).toHaveBeenCalledTimes(2);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(2);
|
||||
expect(log[0]).toEqual(['capture', buttonElement]);
|
||||
expect(log[1]).toEqual(['capture', divElement]);
|
||||
expect(log[2]).toEqual(['bubble', divElement]);
|
||||
expect(log[3]).toEqual(['bubble', buttonElement]);
|
||||
});
|
||||
|
||||
it('handle propagation of click events mixed with onClick events', () => {
|
||||
@@ -1787,6 +1814,258 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
expect(clickEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should correctly work for a basic "click" window listener', () => {
|
||||
const log = [];
|
||||
const clickEvent = jest.fn(event => {
|
||||
log.push({
|
||||
eventPhase: event.eventPhase,
|
||||
type: event.type,
|
||||
currentTarget: event.currentTarget,
|
||||
target: event.target,
|
||||
});
|
||||
});
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(window, clickEvent);
|
||||
});
|
||||
|
||||
return <button>Click anything!</button>;
|
||||
}
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
expect(container.innerHTML).toBe(
|
||||
'<button>Click anything!</button>',
|
||||
);
|
||||
|
||||
// Clicking outside the button should trigger the event callback
|
||||
dispatchClickEvent(document.body);
|
||||
expect(log[0]).toEqual({
|
||||
eventPhase: 3,
|
||||
type: 'click',
|
||||
currentTarget: window,
|
||||
target: document.body,
|
||||
});
|
||||
|
||||
// Unmounting the container and clicking should not work
|
||||
ReactDOM.render(null, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
dispatchClickEvent(document.body);
|
||||
expect(clickEvent).toBeCalledTimes(1);
|
||||
|
||||
// Re-rendering and clicking the body should work again
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
dispatchClickEvent(document.body);
|
||||
expect(clickEvent).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('handle propagation of click events on the window', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e =>
|
||||
log.push(['capture', e.currentTarget]),
|
||||
);
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
const clickCapture = ReactDOM.unstable_useEvent('click', {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(window, onClick);
|
||||
clickCapture.setListener(window, onClickCapture);
|
||||
click.setListener(buttonRef.current, onClick);
|
||||
clickCapture.setListener(buttonRef.current, onClickCapture);
|
||||
click.setListener(divRef.current, onClick);
|
||||
clickCapture.setListener(divRef.current, onClickCapture);
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={buttonRef}>
|
||||
<div ref={divRef}>Click me!</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(2);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(2);
|
||||
expect(log[0]).toEqual(['capture', window]);
|
||||
expect(log[1]).toEqual(['capture', buttonElement]);
|
||||
expect(log[2]).toEqual(['bubble', buttonElement]);
|
||||
expect(log[3]).toEqual(['bubble', window]);
|
||||
|
||||
log.length = 0;
|
||||
onClick.mockClear();
|
||||
onClickCapture.mockClear();
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[0]).toEqual(['capture', window]);
|
||||
expect(log[1]).toEqual(['capture', buttonElement]);
|
||||
expect(log[2]).toEqual(['capture', divElement]);
|
||||
expect(log[3]).toEqual(['bubble', divElement]);
|
||||
expect(log[4]).toEqual(['bubble', buttonElement]);
|
||||
expect(log[5]).toEqual(['bubble', window]);
|
||||
});
|
||||
|
||||
it('should correctly handle stopPropagation for mixed listeners', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const rootListerner1 = jest.fn(e => e.stopPropagation());
|
||||
const rootListerner2 = jest.fn();
|
||||
const targetListerner1 = jest.fn();
|
||||
const targetListerner2 = jest.fn();
|
||||
|
||||
function Test() {
|
||||
const click1 = ReactDOM.unstable_useEvent('click', {
|
||||
capture: true,
|
||||
});
|
||||
const click2 = ReactDOM.unstable_useEvent('click', {
|
||||
capture: true,
|
||||
});
|
||||
const click3 = ReactDOM.unstable_useEvent('click');
|
||||
const click4 = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click1.setListener(window, rootListerner1);
|
||||
click2.setListener(buttonRef.current, targetListerner1);
|
||||
click3.setListener(window, rootListerner2);
|
||||
click4.setListener(buttonRef.current, targetListerner2);
|
||||
});
|
||||
|
||||
return <button ref={buttonRef}>Click me!</button>;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(rootListerner1).toHaveBeenCalledTimes(1);
|
||||
expect(targetListerner1).toHaveBeenCalledTimes(0);
|
||||
expect(targetListerner2).toHaveBeenCalledTimes(0);
|
||||
expect(rootListerner2).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should correctly handle stopPropagation for delegated listeners', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const rootListerner1 = jest.fn(e => e.stopPropagation());
|
||||
const rootListerner2 = jest.fn();
|
||||
const rootListerner3 = jest.fn(e => e.stopPropagation());
|
||||
const rootListerner4 = jest.fn();
|
||||
|
||||
function Test() {
|
||||
const click1 = ReactDOM.unstable_useEvent('click', {
|
||||
capture: true,
|
||||
});
|
||||
const click2 = ReactDOM.unstable_useEvent('click', {
|
||||
capture: true,
|
||||
});
|
||||
const click3 = ReactDOM.unstable_useEvent('click');
|
||||
const click4 = ReactDOM.unstable_useEvent('click');
|
||||
|
||||
React.useEffect(() => {
|
||||
click1.setListener(window, rootListerner1);
|
||||
click2.setListener(window, rootListerner2);
|
||||
click3.setListener(window, rootListerner3);
|
||||
click4.setListener(window, rootListerner4);
|
||||
});
|
||||
|
||||
return <button ref={buttonRef}>Click me!</button>;
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(rootListerner1).toHaveBeenCalledTimes(1);
|
||||
expect(rootListerner2).toHaveBeenCalledTimes(1);
|
||||
expect(rootListerner3).toHaveBeenCalledTimes(0);
|
||||
expect(rootListerner4).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('handle propagation of click events on the window and document', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
const log = [];
|
||||
const onClick = jest.fn(e => log.push(['bubble', e.currentTarget]));
|
||||
const onClickCapture = jest.fn(e =>
|
||||
log.push(['capture', e.currentTarget]),
|
||||
);
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
const clickCapture = ReactDOM.unstable_useEvent('click', {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(window, onClick);
|
||||
clickCapture.setListener(window, onClickCapture);
|
||||
click.setListener(document, onClick);
|
||||
clickCapture.setListener(document, onClickCapture);
|
||||
click.setListener(buttonRef.current, onClick);
|
||||
clickCapture.setListener(buttonRef.current, onClickCapture);
|
||||
click.setListener(divRef.current, onClick);
|
||||
clickCapture.setListener(divRef.current, onClickCapture);
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={buttonRef}>
|
||||
<div ref={divRef}>Click me!</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
let buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(3);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(3);
|
||||
expect(log[0]).toEqual(['capture', window]);
|
||||
expect(log[1]).toEqual(['capture', document]);
|
||||
expect(log[2]).toEqual(['capture', buttonElement]);
|
||||
expect(log[3]).toEqual(['bubble', buttonElement]);
|
||||
expect(log[4]).toEqual(['bubble', document]);
|
||||
expect(log[5]).toEqual(['bubble', window]);
|
||||
|
||||
log.length = 0;
|
||||
onClick.mockClear();
|
||||
onClickCapture.mockClear();
|
||||
|
||||
let divElement = divRef.current;
|
||||
dispatchClickEvent(divElement);
|
||||
expect(onClick).toHaveBeenCalledTimes(4);
|
||||
expect(onClickCapture).toHaveBeenCalledTimes(4);
|
||||
expect(log[0]).toEqual(['capture', window]);
|
||||
expect(log[1]).toEqual(['capture', document]);
|
||||
expect(log[2]).toEqual(['capture', buttonElement]);
|
||||
expect(log[3]).toEqual(['capture', divElement]);
|
||||
expect(log[4]).toEqual(['bubble', divElement]);
|
||||
expect(log[5]).toEqual(['bubble', buttonElement]);
|
||||
expect(log[6]).toEqual(['bubble', document]);
|
||||
expect(log[7]).toEqual(['bubble', window]);
|
||||
});
|
||||
|
||||
it('handles propagation of custom user events', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const divRef = React.createRef();
|
||||
|
||||
+98
-45
@@ -7,6 +7,8 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
|
||||
import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
|
||||
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
|
||||
|
||||
import {HostComponent} from 'shared/ReactWorkTags';
|
||||
@@ -14,69 +16,120 @@ import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import getListener from 'legacy-events/getListener';
|
||||
import {getListenersFromTarget} from '../client/ReactDOMComponentTree';
|
||||
import {IS_TARGET_EVENT_ONLY} from 'legacy-events/EventSystemFlags';
|
||||
import {eventTargetEventListenerStore} from './DOMModernPluginEventSystem';
|
||||
|
||||
export default function accumulateTwoPhaseListeners(
|
||||
event: ReactSyntheticEvent,
|
||||
accumulateUseEventListeners?: boolean,
|
||||
eventSystemFlags?: EventSystemFlags,
|
||||
targetContainer?: null | EventTarget,
|
||||
): void {
|
||||
const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
|
||||
const dispatchListeners = [];
|
||||
const dispatchInstances = [];
|
||||
const {bubbled, captured} = phasedRegistrationNames;
|
||||
let node = event._targetInst;
|
||||
|
||||
// Accumulate all instances and listeners via the target -> root path.
|
||||
while (node !== null) {
|
||||
// We only care for listeners that are on HostComponents (i.e. <div>)
|
||||
if (node.tag === HostComponent) {
|
||||
// For useEvent listenrs
|
||||
if (enableUseEventAPI && accumulateUseEventListeners) {
|
||||
// useEvent event listeners
|
||||
const instance = node.stateNode;
|
||||
const targetType = event.type;
|
||||
const listeners = getListenersFromTarget(instance);
|
||||
// For TargetEvent only accumulation, we do not traverse through
|
||||
// the React tree looking for managed React DOM elements that have
|
||||
// events. Instead we only check the EventTarget Store Map to see
|
||||
// if the container has listeners for the particular phase we're
|
||||
// interested in. This is because we attach the native event listener
|
||||
// only in the given phase.
|
||||
if (
|
||||
enableUseEventAPI &&
|
||||
accumulateUseEventListeners &&
|
||||
eventSystemFlags !== undefined &&
|
||||
eventSystemFlags & IS_TARGET_EVENT_ONLY &&
|
||||
targetContainer != null
|
||||
) {
|
||||
const eventTypeMap = eventTargetEventListenerStore.get(targetContainer);
|
||||
if (eventTypeMap !== undefined) {
|
||||
const type = ((event.type: any): DOMTopLevelEventType);
|
||||
const listeners = eventTypeMap.get(type);
|
||||
if (listeners !== undefined) {
|
||||
const isCapturePhase = (event: any).eventPhase === 1;
|
||||
|
||||
if (listeners !== null) {
|
||||
const listenersArr = Array.from(listeners);
|
||||
for (let i = 0; i < listenersArr.length; i++) {
|
||||
const listener = listenersArr[i];
|
||||
const {
|
||||
callback,
|
||||
event: {capture, type},
|
||||
} = listener;
|
||||
if (type === targetType) {
|
||||
if (capture === true) {
|
||||
dispatchListeners.unshift(callback);
|
||||
dispatchInstances.unshift(node);
|
||||
} else {
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(node);
|
||||
if (isCapturePhase) {
|
||||
const captureListeners = Array.from(listeners.captured);
|
||||
|
||||
for (let i = captureListeners.length - 1; i >= 0; i--) {
|
||||
const listener = captureListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(targetContainer);
|
||||
}
|
||||
} else {
|
||||
const bubbleListeners = Array.from(listeners.bubbled);
|
||||
|
||||
for (let i = 0; i < bubbleListeners.length; i++) {
|
||||
const listener = bubbleListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(targetContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {bubbled, captured} = phasedRegistrationNames;
|
||||
// If we are not handling EventTarget only phase, then we're doing the
|
||||
// usual two phase accumulation using the React fiber tree to pick up
|
||||
// all relevant useEvent and on* prop events.
|
||||
let node = event._targetInst;
|
||||
|
||||
// Accumulate all instances and listeners via the target -> root path.
|
||||
while (node !== null) {
|
||||
// We only care for listeners that are on HostComponents (i.e. <div>)
|
||||
if (node.tag === HostComponent) {
|
||||
// For useEvent listenrs
|
||||
if (enableUseEventAPI && accumulateUseEventListeners) {
|
||||
// useEvent event listeners
|
||||
const instance = node.stateNode;
|
||||
const targetType = event.type;
|
||||
const listeners = getListenersFromTarget(instance);
|
||||
|
||||
if (listeners !== null) {
|
||||
const listenersArr = Array.from(listeners);
|
||||
for (let i = 0; i < listenersArr.length; i++) {
|
||||
const listener = listenersArr[i];
|
||||
const {
|
||||
callback,
|
||||
event: {capture, type},
|
||||
} = listener;
|
||||
if (type === targetType) {
|
||||
if (capture === true) {
|
||||
dispatchListeners.unshift(callback);
|
||||
dispatchInstances.unshift(node);
|
||||
} else {
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Standard React on* listeners, i.e. onClick prop
|
||||
if (captured !== null) {
|
||||
const captureListener = getListener(node, captured);
|
||||
if (captureListener != null) {
|
||||
// Capture listeners/instances should go at the start, so we
|
||||
// unshift them to the start of the array.
|
||||
dispatchListeners.unshift(captureListener);
|
||||
dispatchInstances.unshift(node);
|
||||
// Standard React on* listeners, i.e. onClick prop
|
||||
if (captured !== null) {
|
||||
const captureListener = getListener(node, captured);
|
||||
if (captureListener != null) {
|
||||
// Capture listeners/instances should go at the start, so we
|
||||
// unshift them to the start of the array.
|
||||
dispatchListeners.unshift(captureListener);
|
||||
dispatchInstances.unshift(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bubbled !== null) {
|
||||
const bubbleListener = getListener(node, bubbled);
|
||||
if (bubbleListener != null) {
|
||||
// Bubble listeners/instances should go at the end, so we
|
||||
// push them to the end of the array.
|
||||
dispatchListeners.push(bubbleListener);
|
||||
dispatchInstances.push(node);
|
||||
if (bubbled !== null) {
|
||||
const bubbleListener = getListener(node, bubbled);
|
||||
if (bubbleListener != null) {
|
||||
// Bubble listeners/instances should go at the end, so we
|
||||
// push them to the end of the array.
|
||||
dispatchListeners.push(bubbleListener);
|
||||
dispatchInstances.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
// To prevent allocation to the event unless we actually
|
||||
// have listeners we check the length of one of the arrays.
|
||||
|
||||
Reference in New Issue
Block a user