mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
ReactDOM.useEvent: fix scope propagation issue (#18464)
This commit is contained in:
@@ -64,8 +64,7 @@ if (__DEV__) {
|
||||
*/
|
||||
export function executeDispatch(event, listener, inst) {
|
||||
const type = event.type || 'unknown-event';
|
||||
event.currentTarget =
|
||||
inst.tag !== undefined ? getNodeFromInstance(inst) : inst;
|
||||
event.currentTarget = getNodeFromInstance(inst);
|
||||
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
|
||||
event.currentTarget = null;
|
||||
}
|
||||
@@ -80,20 +79,12 @@ export function executeDispatchesInOrder(event) {
|
||||
validateEventDispatches(event);
|
||||
}
|
||||
if (Array.isArray(dispatchListeners)) {
|
||||
let previousInstance;
|
||||
for (let i = 0; i < dispatchListeners.length; i++) {
|
||||
const instance = dispatchInstances[i];
|
||||
// We check if the instance was the same as the last one,
|
||||
// if it was, then we're still on the same instance thus
|
||||
// propagation should not stop. If we add support for
|
||||
// stopImmediatePropagation at some point, then we'll
|
||||
// need to handle that case here differently.
|
||||
if (instance !== previousInstance && event.isPropagationStopped()) {
|
||||
if (event.isPropagationStopped()) {
|
||||
break;
|
||||
}
|
||||
// Listeners and Instances are two parallel arrays that are always in sync.
|
||||
executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
|
||||
previousInstance = instance;
|
||||
}
|
||||
} else if (dispatchListeners) {
|
||||
executeDispatch(event, dispatchListeners, dispatchInstances);
|
||||
|
||||
@@ -40,8 +40,11 @@ export type ReactSyntheticEvent = {|
|
||||
nativeEventTarget: EventTarget,
|
||||
) => ReactSyntheticEvent,
|
||||
isPersistent: () => boolean,
|
||||
_dispatchInstances: null | Array<Fiber | EventTarget> | Fiber | EventTarget,
|
||||
isPropagationStopped: () => boolean,
|
||||
_dispatchInstances: null | Array<Fiber | null> | Fiber,
|
||||
_dispatchListeners: null | Array<Function> | Function,
|
||||
_dispatchCurrentTargets: null | Array<EventTarget>,
|
||||
_targetInst: Fiber,
|
||||
type: string,
|
||||
currentTarget: null | EventTarget,
|
||||
|};
|
||||
|
||||
@@ -78,6 +78,7 @@ function SyntheticEvent(
|
||||
this.nativeEvent = nativeEvent;
|
||||
this._dispatchListeners = null;
|
||||
this._dispatchInstances = null;
|
||||
this._dispatchCurrentTargets = null;
|
||||
|
||||
const Interface = this.constructor.Interface;
|
||||
for (const propName in Interface) {
|
||||
@@ -187,6 +188,7 @@ Object.assign(SyntheticEvent.prototype, {
|
||||
this.isPropagationStopped = functionThatReturnsFalse;
|
||||
this._dispatchListeners = null;
|
||||
this._dispatchInstances = null;
|
||||
this._dispatchCurrentTargets = null;
|
||||
if (__DEV__) {
|
||||
Object.defineProperty(
|
||||
this,
|
||||
|
||||
+47
-1
@@ -25,7 +25,6 @@ import type {ReactDOMListener} from '../shared/ReactDOMTypes';
|
||||
|
||||
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 {
|
||||
PLUGIN_EVENT_SYSTEM,
|
||||
@@ -92,6 +91,7 @@ import {
|
||||
enableLegacyFBSupport,
|
||||
enableUseEventAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
|
||||
|
||||
const capturePhaseEvents = new Set([
|
||||
TOP_FOCUS,
|
||||
@@ -167,6 +167,52 @@ export const reactScopeListenerStore: WeakMap<
|
||||
>,
|
||||
> = new PossiblyWeakMap();
|
||||
|
||||
function executeDispatch(
|
||||
event: ReactSyntheticEvent,
|
||||
listener: Function,
|
||||
currentTarget: EventTarget,
|
||||
): void {
|
||||
const type = event.type || 'unknown-event';
|
||||
event.currentTarget = currentTarget;
|
||||
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
|
||||
event.currentTarget = null;
|
||||
}
|
||||
|
||||
function executeDispatchesInOrder(event: ReactSyntheticEvent): void {
|
||||
// TODO we should remove _dispatchListeners and _dispatchInstances at some point.
|
||||
const dispatchListeners = event._dispatchListeners;
|
||||
const dispatchInstances = event._dispatchInstances;
|
||||
const dispatchCurrentTargets = event._dispatchCurrentTargets;
|
||||
let previousInstance;
|
||||
|
||||
if (
|
||||
dispatchListeners !== null &&
|
||||
dispatchInstances !== null &&
|
||||
dispatchCurrentTargets !== null
|
||||
) {
|
||||
for (let i = 0; i < dispatchListeners.length; i++) {
|
||||
const instance = dispatchInstances[i];
|
||||
const listener = dispatchListeners[i];
|
||||
const currentTarget = dispatchCurrentTargets[i];
|
||||
|
||||
// We check if the instance was the same as the last one,
|
||||
// if it was, then we're still on the same instance thus
|
||||
// propagation should not stop. If we add support for
|
||||
// stopImmediatePropagation at some point, then we'll
|
||||
// need to handle that case here differently.
|
||||
if (instance !== previousInstance && event.isPropagationStopped()) {
|
||||
break;
|
||||
}
|
||||
// Listeners and Instances are two parallel arrays that are always in sync.
|
||||
executeDispatch(event, listener, currentTarget);
|
||||
previousInstance = instance;
|
||||
}
|
||||
}
|
||||
event._dispatchListeners = null;
|
||||
event._dispatchInstances = null;
|
||||
event._dispatchCurrentTargets = null;
|
||||
}
|
||||
|
||||
function dispatchEventsForPlugins(
|
||||
topLevelType: DOMTopLevelEventType,
|
||||
eventSystemFlags: EventSystemFlags,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export type EventSystemFlags = number;
|
||||
|
||||
export const PLUGIN_EVENT_SYSTEM = 1;
|
||||
export const RESPONDER_EVENT_SYSTEM = 1 << 1;
|
||||
export const IS_PASSIVE = 1 << 2;
|
||||
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 IS_TARGET_EVENT_ONLY = 1 << 7;
|
||||
export const LEGACY_FB_SUPPORT = 1 << 8;
|
||||
@@ -2512,6 +2512,112 @@ describe('DOMModernPluginEventSystem', () => {
|
||||
// <button>, which is actually outside the scope.
|
||||
expect(clickEvent).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('handle stopPropagation (inner) correctly between scopes', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const outerOnClick = jest.fn();
|
||||
const innerOnClick = jest.fn(e => e.stopPropagation());
|
||||
const TestScope = React.unstable_createScope();
|
||||
const TestScope2 = React.unstable_createScope();
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
const scopeRef = React.useRef(null);
|
||||
const scope2Ref = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(scopeRef.current, outerOnClick);
|
||||
click.setListener(scope2Ref.current, innerOnClick);
|
||||
});
|
||||
|
||||
return (
|
||||
<TestScope ref={scopeRef}>
|
||||
<TestScope2 ref={scope2Ref}>
|
||||
<button ref={buttonRef} />
|
||||
</TestScope2>
|
||||
</TestScope>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
const buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
|
||||
expect(innerOnClick).toHaveBeenCalledTimes(1);
|
||||
expect(outerOnClick).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('handle stopPropagation (outer) correctly between scopes', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const outerOnClick = jest.fn(e => e.stopPropagation());
|
||||
const innerOnClick = jest.fn();
|
||||
const TestScope = React.unstable_createScope();
|
||||
const TestScope2 = React.unstable_createScope();
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
const scopeRef = React.useRef(null);
|
||||
const scope2Ref = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(scopeRef.current, outerOnClick);
|
||||
click.setListener(scope2Ref.current, innerOnClick);
|
||||
});
|
||||
|
||||
return (
|
||||
<TestScope ref={scopeRef}>
|
||||
<TestScope2 ref={scope2Ref}>
|
||||
<button ref={buttonRef} />
|
||||
</TestScope2>
|
||||
</TestScope>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
const buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
|
||||
expect(innerOnClick).toHaveBeenCalledTimes(1);
|
||||
expect(outerOnClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handle stopPropagation (inner and outer) correctly between scopes', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const onClick = jest.fn(e => e.stopPropagation());
|
||||
const TestScope = React.unstable_createScope();
|
||||
const TestScope2 = React.unstable_createScope();
|
||||
|
||||
function Test() {
|
||||
const click = ReactDOM.unstable_useEvent('click');
|
||||
const scopeRef = React.useRef(null);
|
||||
const scope2Ref = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
click.setListener(scopeRef.current, onClick);
|
||||
click.setListener(scope2Ref.current, onClick);
|
||||
});
|
||||
|
||||
return (
|
||||
<TestScope ref={scopeRef}>
|
||||
<TestScope2 ref={scope2Ref}>
|
||||
<button ref={buttonRef} />
|
||||
</TestScope2>
|
||||
</TestScope>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Test />, container);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
const buttonElement = buttonRef.current;
|
||||
dispatchClickEvent(buttonElement);
|
||||
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
+15
-10
@@ -83,42 +83,47 @@ function accumulateEnterLeaveListenersForEvent(
|
||||
}
|
||||
const dispatchListeners = [];
|
||||
const dispatchInstances = [];
|
||||
const dispatchCurrentTargets = [];
|
||||
|
||||
let node = target;
|
||||
while (node !== null) {
|
||||
if (node === common) {
|
||||
let instance = target;
|
||||
while (instance !== null) {
|
||||
if (instance === common) {
|
||||
break;
|
||||
}
|
||||
const alternate = node.alternate;
|
||||
const {alternate, stateNode, tag} = instance;
|
||||
if (alternate !== null && alternate === common) {
|
||||
break;
|
||||
}
|
||||
if (node.tag === HostComponent) {
|
||||
if (tag === HostComponent && stateNode !== null) {
|
||||
const currentTarget = stateNode;
|
||||
if (capture) {
|
||||
const captureListener = getListener(node, registrationName);
|
||||
const captureListener = getListener(instance, registrationName);
|
||||
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);
|
||||
dispatchInstances.unshift(instance);
|
||||
dispatchCurrentTargets.unshift(currentTarget);
|
||||
}
|
||||
} else {
|
||||
const bubbleListener = getListener(node, registrationName);
|
||||
const bubbleListener = getListener(instance, registrationName);
|
||||
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);
|
||||
dispatchInstances.push(instance);
|
||||
dispatchCurrentTargets.push(currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
node = node.return;
|
||||
instance = instance.return;
|
||||
}
|
||||
// To prevent allocation to the event unless we actually
|
||||
// have listeners we check the length of one of the arrays.
|
||||
if (dispatchListeners.length > 0) {
|
||||
event._dispatchListeners = dispatchListeners;
|
||||
event._dispatchInstances = dispatchInstances;
|
||||
event._dispatchCurrentTargets = dispatchCurrentTargets;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@ import {eventTargetEventListenerStore} from './DOMModernPluginEventSystem';
|
||||
|
||||
export default function accumulateEventTargetListeners(
|
||||
event: ReactSyntheticEvent,
|
||||
targetContainer: EventTarget,
|
||||
currentTarget: EventTarget,
|
||||
): void {
|
||||
const dispatchListeners = [];
|
||||
const dispatchInstances = [];
|
||||
const eventTypeMap = eventTargetEventListenerStore.get(targetContainer);
|
||||
const dispatchCurrentTargets = [];
|
||||
|
||||
const eventTypeMap = eventTargetEventListenerStore.get(currentTarget);
|
||||
if (eventTypeMap !== undefined) {
|
||||
const type = ((event.type: any): DOMTopLevelEventType);
|
||||
const listeners = eventTypeMap.get(type);
|
||||
@@ -32,7 +34,10 @@ export default function accumulateEventTargetListeners(
|
||||
const listener = captureListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(targetContainer);
|
||||
// EventTarget listeners do not have instances, as there
|
||||
// is no backing Fiber instance for them (window, document etc).
|
||||
dispatchInstances.push(null);
|
||||
dispatchCurrentTargets.push(currentTarget);
|
||||
}
|
||||
} else {
|
||||
const bubbleListeners = Array.from(listeners.bubbled);
|
||||
@@ -41,7 +46,10 @@ export default function accumulateEventTargetListeners(
|
||||
const listener = bubbleListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(targetContainer);
|
||||
// EventTarget listeners do not have instances, as there
|
||||
// is no backing Fiber instance for them (window, document etc).
|
||||
dispatchInstances.push(null);
|
||||
dispatchCurrentTargets.push(currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,5 +59,6 @@ export default function accumulateEventTargetListeners(
|
||||
if (dispatchListeners.length > 0) {
|
||||
event._dispatchListeners = dispatchListeners;
|
||||
event._dispatchInstances = dispatchInstances;
|
||||
event._dispatchCurrentTargets = dispatchCurrentTargets;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-16
@@ -31,20 +31,22 @@ export default function accumulateTwoPhaseListeners(
|
||||
const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
|
||||
const dispatchListeners = [];
|
||||
const dispatchInstances = [];
|
||||
const dispatchCurrentTargets = [];
|
||||
|
||||
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;
|
||||
let instance = event._targetInst;
|
||||
let lastHostComponent = null;
|
||||
|
||||
// Accumulate all instances and listeners via the target -> root path.
|
||||
while (node !== null) {
|
||||
const {stateNode: instance, tag} = node;
|
||||
while (instance !== null) {
|
||||
const {stateNode, tag} = instance;
|
||||
// Handle listeners that are on HostComponents (i.e. <div>)
|
||||
if (instance !== null && tag === HostComponent) {
|
||||
lastHostComponent = instance;
|
||||
if (tag === HostComponent && stateNode !== null) {
|
||||
const currentTarget = stateNode;
|
||||
lastHostComponent = currentTarget;
|
||||
// For useEvent listenrs
|
||||
if (
|
||||
enableModernEventSystem &&
|
||||
@@ -53,7 +55,7 @@ export default function accumulateTwoPhaseListeners(
|
||||
) {
|
||||
// useEvent event listeners
|
||||
const targetType = event.type;
|
||||
const listeners = getListenersFromTarget(instance);
|
||||
const listeners = getListenersFromTarget(currentTarget);
|
||||
|
||||
if (listeners !== null) {
|
||||
const listenersArr = Array.from(listeners);
|
||||
@@ -66,10 +68,12 @@ export default function accumulateTwoPhaseListeners(
|
||||
if (type === targetType) {
|
||||
if (capture === true) {
|
||||
dispatchListeners.unshift(callback);
|
||||
dispatchInstances.unshift(node);
|
||||
dispatchInstances.unshift(instance);
|
||||
dispatchCurrentTargets.unshift(currentTarget);
|
||||
} else {
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(node);
|
||||
dispatchInstances.push(instance);
|
||||
dispatchCurrentTargets.push(currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,21 +81,23 @@ export default function accumulateTwoPhaseListeners(
|
||||
}
|
||||
// Standard React on* listeners, i.e. onClick prop
|
||||
if (captured !== null) {
|
||||
const captureListener = getListener(node, captured);
|
||||
const captureListener = getListener(instance, 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);
|
||||
dispatchInstances.unshift(instance);
|
||||
dispatchCurrentTargets.unshift(currentTarget);
|
||||
}
|
||||
}
|
||||
if (bubbled !== null) {
|
||||
const bubbleListener = getListener(node, bubbled);
|
||||
const bubbleListener = getListener(instance, 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);
|
||||
dispatchInstances.push(instance);
|
||||
dispatchCurrentTargets.push(currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +109,7 @@ export default function accumulateTwoPhaseListeners(
|
||||
tag === ScopeComponent &&
|
||||
lastHostComponent !== null
|
||||
) {
|
||||
const reactScope = instance.methods;
|
||||
const reactScope = stateNode.methods;
|
||||
const eventTypeMap = reactScopeListenerStore.get(reactScope);
|
||||
if (eventTypeMap !== undefined) {
|
||||
const type = ((event.type: any): DOMTopLevelEventType);
|
||||
@@ -111,23 +117,26 @@ export default function accumulateTwoPhaseListeners(
|
||||
if (listeners !== undefined) {
|
||||
const captureListeners = Array.from(listeners.captured);
|
||||
const bubbleListeners = Array.from(listeners.bubbled);
|
||||
const lastCurrentTarget = ((lastHostComponent: any): Element);
|
||||
|
||||
for (let i = 0; i < captureListeners.length; i++) {
|
||||
const listener = captureListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.unshift(callback);
|
||||
dispatchInstances.unshift(((lastHostComponent: any): Element));
|
||||
dispatchInstances.unshift(instance);
|
||||
dispatchCurrentTargets.unshift(lastCurrentTarget);
|
||||
}
|
||||
for (let i = 0; i < bubbleListeners.length; i++) {
|
||||
const listener = bubbleListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(((lastHostComponent: any): Element));
|
||||
dispatchInstances.push(instance);
|
||||
dispatchCurrentTargets.push(lastCurrentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
node = node.return;
|
||||
instance = instance.return;
|
||||
}
|
||||
|
||||
// To prevent allocation to the event unless we actually
|
||||
@@ -135,5 +144,6 @@ export default function accumulateTwoPhaseListeners(
|
||||
if (dispatchListeners.length > 0) {
|
||||
event._dispatchListeners = dispatchListeners;
|
||||
event._dispatchInstances = dispatchInstances;
|
||||
event._dispatchCurrentTargets = dispatchCurrentTargets;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user