diff --git a/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js index 4ab66536dd..bf46b68474 100644 --- a/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js +++ b/packages/react-dom/src/events/DOMLegacyEventPluginSystem.js @@ -15,6 +15,7 @@ import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {PluginModule} from 'legacy-events/PluginModuleType'; import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType'; import type {TopLevelType} from 'legacy-events/TopLevelEventTypes'; +import forEachAccumulated from 'legacy-events/forEachAccumulated'; import { HostRoot, @@ -45,6 +46,7 @@ import { } from './DOMTopLevelEventTypes'; import {addTrappedEventListener} from './ReactDOMEventListener'; import {batchedEventUpdates} from './ReactDOMUpdateBatching'; +import getListener from './getListener'; /** * Summary of `DOMEventPluginSystem` event handling: @@ -396,3 +398,203 @@ export function legacyTrapCapturedEvent( ); listenerMap.set(topLevelType, {passive: undefined, listener}); } + +function getParent(inst: Object | null): Object | null { + if (!inst) { + return null; + } + do { + 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); + if (inst) { + return inst; + } + return null; +} + +/** + * Simulates the traversal of a two-phase, capture/bubble event dispatch. + */ +function traverseTwoPhase( + inst: Object, + fn: Function, + arg: ReactSyntheticEvent, +) { + const path = []; + while (inst) { + path.push(inst); + inst = getParent(inst); + } + let i; + for (i = path.length; i-- > 0; ) { + fn(path[i], 'captured', arg); + } + for (i = 0; i < path.length; i++) { + fn(path[i], 'bubbled', arg); + } +} + +function listenerAtPhase(inst, event, propagationPhase) { + const registrationName = + event.dispatchConfig.phasedRegistrationNames[propagationPhase]; + return getListener(inst, registrationName); +} + +/** + * Return the lowest common ancestor of A and B, or null if they are in + * different trees. + */ +export function getLowestCommonAncestor( + instA: Object, + instB: Object, +): Object | null { + let depthA = 0; + for (let tempA = instA; tempA; tempA = getParent(tempA)) { + depthA++; + } + let depthB = 0; + for (let tempB = instB; tempB; tempB = getParent(tempB)) { + depthB++; + } + + // If A is deeper, crawl up. + while (depthA - depthB > 0) { + instA = getParent(instA); + depthA--; + } + + // If B is deeper, crawl up. + while (depthB - depthA > 0) { + instB = getParent(instB); + depthB--; + } + + // Walk in lockstep until we find a match. + let depth = depthA; + while (depth--) { + if (instA === instB || instA === instB.alternate) { + return instA; + } + instA = getParent(instA); + instB = getParent(instB); + } + return null; +} + +/** + * Traverses the ID hierarchy and invokes the supplied `cb` on any IDs that + * should would receive a `mouseEnter` or `mouseLeave` event. + * + * Does not invoke the callback on the nearest common ancestor because nothing + * "entered" or "left" that element. + */ +export function traverseEnterLeave( + from: Object, + to: Object, + fn: Function, + argFrom: ReactSyntheticEvent, + argTo: ReactSyntheticEvent, +) { + const common = from && to ? getLowestCommonAncestor(from, to) : null; + const pathFrom = []; + while (true) { + if (!from) { + break; + } + if (from === common) { + break; + } + const alternate = from.alternate; + if (alternate !== null && alternate === common) { + break; + } + pathFrom.push(from); + from = getParent(from); + } + const pathTo = []; + while (true) { + if (!to) { + break; + } + if (to === common) { + break; + } + const alternate = to.alternate; + if (alternate !== null && alternate === common) { + break; + } + pathTo.push(to); + to = getParent(to); + } + for (let i = 0; i < pathFrom.length; i++) { + fn(pathFrom[i], 'bubbled', argFrom); + } + for (let i = pathTo.length; i-- > 0; ) { + fn(pathTo[i], 'captured', argTo); + } +} + +function accumulateDirectionalDispatches(inst, phase, event) { + if (__DEV__) { + if (!inst) { + console.error('Dispatching inst must not be null'); + } + } + const listener = listenerAtPhase(inst, event, phase); + if (listener) { + event._dispatchListeners = accumulateInto( + event._dispatchListeners, + listener, + ); + event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); + } +} + +function accumulateTwoPhaseDispatchesSingle(event) { + if (event && event.dispatchConfig.phasedRegistrationNames) { + traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event); + } +} + +/** + * Accumulates without regard to direction, does not look for phased + * registration names. Same as `accumulateDirectDispatchesSingle` but without + * requiring that the `dispatchMarker` be the same as the dispatched ID. + */ +function accumulateDispatches( + inst: Object, + ignoredDirection: ?boolean, + event: Object, +): void { + if (inst && event && event.dispatchConfig.registrationName) { + const registrationName = event.dispatchConfig.registrationName; + const listener = getListener(inst, registrationName); + if (listener) { + event._dispatchListeners = accumulateInto( + event._dispatchListeners, + listener, + ); + event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); + } + } +} + +export function accumulateTwoPhaseDispatches( + events: ReactSyntheticEvent | Array, +): void { + forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); +} + +export function accumulateEnterLeaveDispatches( + leave: ReactSyntheticEvent, + enter: ReactSyntheticEvent, + from: Fiber, + to: Fiber, +) { + traverseEnterLeave(from, to, accumulateDispatches, leave, enter); +} diff --git a/packages/react-dom/src/events/plugins/LegacyBeforeInputEventPlugin.js b/packages/react-dom/src/events/plugins/LegacyBeforeInputEventPlugin.js index 96685f7c81..3c866c6749 100644 --- a/packages/react-dom/src/events/plugins/LegacyBeforeInputEventPlugin.js +++ b/packages/react-dom/src/events/plugins/LegacyBeforeInputEventPlugin.js @@ -28,7 +28,7 @@ import { } from '../FallbackCompositionState'; import SyntheticCompositionEvent from '../SyntheticCompositionEvent'; import SyntheticInputEvent from '../SyntheticInputEvent'; -import accumulateTwoPhaseListeners from '../accumulateTwoPhaseListeners'; +import {accumulateTwoPhaseDispatches} from '../DOMLegacyEventPluginSystem'; const END_KEYCODES = [9, 13, 27, 32]; // Tab, Return, Esc, Space const START_KEYCODE = 229; @@ -276,7 +276,7 @@ function extractCompositionEvent( } } - accumulateTwoPhaseListeners(event); + accumulateTwoPhaseDispatches(event); return event; } @@ -437,7 +437,7 @@ function extractBeforeInputEvent( ); event.data = chars; - accumulateTwoPhaseListeners(event); + accumulateTwoPhaseDispatches(event); return event; } diff --git a/packages/react-dom/src/events/plugins/LegacyChangeEventPlugin.js b/packages/react-dom/src/events/plugins/LegacyChangeEventPlugin.js index dbe3685f36..e9518bb1a2 100644 --- a/packages/react-dom/src/events/plugins/LegacyChangeEventPlugin.js +++ b/packages/react-dom/src/events/plugins/LegacyChangeEventPlugin.js @@ -27,13 +27,9 @@ import {updateValueIfChanged} from '../../client/inputValueTracking'; import {setDefaultValue} from '../../client/ReactDOMInput'; import {enqueueStateRestore} from '../ReactDOMControlledComponent'; -import { - disableInputAttributeSyncing, - enableModernEventSystem, -} from 'shared/ReactFeatureFlags'; -import accumulateTwoPhaseListeners from '../accumulateTwoPhaseListeners'; +import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags'; import {batchedUpdates} from '../ReactDOMUpdateBatching'; -import {dispatchEventsInBatch} from '../DOMModernPluginEventSystem'; +import {accumulateTwoPhaseDispatches} from '../DOMLegacyEventPluginSystem'; const eventTypes = { change: { @@ -64,7 +60,7 @@ function createAndAccumulateChangeEvent(inst, nativeEvent, target) { event.type = 'change'; // Flag this event loop as needing state restore. enqueueStateRestore(target); - accumulateTwoPhaseListeners(event); + accumulateTwoPhaseDispatches(event); return event; } /** @@ -105,11 +101,7 @@ function manualDispatchChangeEvent(nativeEvent) { } function runEventInBatch(event) { - if (enableModernEventSystem) { - dispatchEventsInBatch([event]); - } else { - runEventsInBatch(event); - } + runEventsInBatch(event); } function getInstIfValueChanged(targetInst) { diff --git a/packages/react-dom/src/events/plugins/LegacyEnterLeaveEventPlugin.js b/packages/react-dom/src/events/plugins/LegacyEnterLeaveEventPlugin.js index cb51ed4546..9b23efd888 100644 --- a/packages/react-dom/src/events/plugins/LegacyEnterLeaveEventPlugin.js +++ b/packages/react-dom/src/events/plugins/LegacyEnterLeaveEventPlugin.js @@ -23,8 +23,7 @@ import { } from '../../client/ReactDOMComponentTree'; import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags'; import {getNearestMountedFiber} from 'react-reconciler/src/ReactFiberTreeReflection'; -import {enableModernEventSystem} from 'shared/ReactFeatureFlags'; -import accumulateEnterLeaveListeners from '../accumulateEnterLeaveListeners'; +import {accumulateEnterLeaveDispatches} from '../DOMLegacyEventPluginSystem'; const eventTypes = { mouseEnter: { @@ -67,26 +66,16 @@ const EnterLeaveEventPlugin = { const isOutEvent = topLevelType === TOP_MOUSE_OUT || topLevelType === TOP_POINTER_OUT; - if (isOverEvent && (eventSystemFlags & IS_REPLAYED) === 0) { - const related = nativeEvent.relatedTarget || nativeEvent.fromElement; - if (related) { - if (enableModernEventSystem) { - // Due to the fact we don't add listeners to the document with the - // modern event system and instead attach listeners to roots, we - // need to handle the over event case. To ensure this, we just need to - // make sure the node that we're coming from is managed by React. - const inst = getClosestInstanceFromNode(related); - if (inst !== null) { - return null; - } - } else { - // If this is an over event with a target, then we've already dispatched - // the event in the out event of the other target. If this is replayed, - // then it's because we couldn't dispatch against this target previously - // so we have to do it now instead. - return null; - } - } + if ( + isOverEvent && + (eventSystemFlags & IS_REPLAYED) === 0 && + (nativeEvent.relatedTarget || nativeEvent.fromElement) + ) { + // If this is an over event with a target, then we've already dispatched + // the event in the out event of the other target. If this is replayed, + // then it's because we couldn't dispatch against this target previously + // so we have to do it now instead. + return null; } if (!isOutEvent && !isOverEvent) { @@ -174,15 +163,13 @@ const EnterLeaveEventPlugin = { enter.target = toNode; enter.relatedTarget = fromNode; - accumulateEnterLeaveListeners(leave, enter, from, to); + accumulateEnterLeaveDispatches(leave, enter, from, to); - if (!enableModernEventSystem) { - // If we are not processing the first ancestor, then we - // should not process the same nativeEvent again, as we - // will have already processed it in the first ancestor. - if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) { - return [leave]; - } + // If we are not processing the first ancestor, then we + // should not process the same nativeEvent again, as we + // will have already processed it in the first ancestor. + if ((eventSystemFlags & IS_FIRST_ANCESTOR) === 0) { + return [leave]; } return [leave, enter]; diff --git a/packages/react-dom/src/events/plugins/LegacySelectEventPlugin.js b/packages/react-dom/src/events/plugins/LegacySelectEventPlugin.js index 0d1266b0bc..74d1d9fb74 100644 --- a/packages/react-dom/src/events/plugins/LegacySelectEventPlugin.js +++ b/packages/react-dom/src/events/plugins/LegacySelectEventPlugin.js @@ -26,7 +26,7 @@ import {getNodeFromInstance} from '../../client/ReactDOMComponentTree'; import {hasSelectionCapabilities} from '../../client/ReactInputSelection'; import {DOCUMENT_NODE} from '../../shared/HTMLNodeType'; import {isListeningToAllDependencies} from '../DOMEventListenerMap'; -import accumulateTwoPhaseListeners from '../accumulateTwoPhaseListeners'; +import {accumulateTwoPhaseDispatches} from '../DOMLegacyEventPluginSystem'; const skipSelectionChangeEvent = canUseDOM && 'documentMode' in document && document.documentMode <= 11; @@ -135,7 +135,7 @@ function constructSelectEvent(nativeEvent, nativeEventTarget) { syntheticEvent.type = 'select'; syntheticEvent.target = activeElement; - accumulateTwoPhaseListeners(syntheticEvent); + accumulateTwoPhaseDispatches(syntheticEvent); return syntheticEvent; } @@ -166,16 +166,11 @@ const SelectEventPlugin = { nativeEvent, nativeEventTarget, eventSystemFlags, - container, ) { - const containerOrDoc = - container || getEventTargetDocument(nativeEventTarget); + const doc = getEventTargetDocument(nativeEventTarget); // Track whether all listeners exists for this plugin. If none exist, we do // not extract events. See #3639. - if ( - !containerOrDoc || - !isListeningToAllDependencies('onSelect', containerOrDoc) - ) { + if (!doc || !isListeningToAllDependencies('onSelect', doc)) { return null; } diff --git a/packages/react-dom/src/events/plugins/LegacySimpleEventPlugin.js b/packages/react-dom/src/events/plugins/LegacySimpleEventPlugin.js index cd61625518..532dbec15c 100644 --- a/packages/react-dom/src/events/plugins/LegacySimpleEventPlugin.js +++ b/packages/react-dom/src/events/plugins/LegacySimpleEventPlugin.js @@ -14,10 +14,8 @@ import type { import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; import type {PluginModule} from 'legacy-events/PluginModuleType'; -import type {EventSystemFlags} from '../EventSystemFlags'; import SyntheticEvent from 'legacy-events/SyntheticEvent'; -import {IS_TARGET_PHASE_ONLY} from '../EventSystemFlags'; import * as DOMTopLevelEventTypes from '../DOMTopLevelEventTypes'; import { @@ -37,10 +35,7 @@ import SyntheticTransitionEvent from '../SyntheticTransitionEvent'; import SyntheticUIEvent from '../SyntheticUIEvent'; import SyntheticWheelEvent from '../SyntheticWheelEvent'; import getEventCharCode from '../getEventCharCode'; -import accumulateTwoPhaseListeners from '../accumulateTwoPhaseListeners'; -import accumulateEventTargetListeners from '../accumulateEventTargetListeners'; - -import {enableUseEventAPI} from 'shared/ReactFeatureFlags'; +import {accumulateTwoPhaseDispatches} from '../DOMLegacyEventPluginSystem'; // Only used in DEV for exhaustiveness validation. const knownHTMLTopLevelTypes: Array = [ @@ -86,8 +81,6 @@ const SimpleEventPlugin: PluginModule = { targetInst: null | Fiber, nativeEvent: MouseEvent, nativeEventTarget: null | EventTarget, - eventSystemFlags?: EventSystemFlags, - targetContainer?: null | EventTarget, ): null | ReactSyntheticEvent { const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType); if (!dispatchConfig) { @@ -109,8 +102,6 @@ const SimpleEventPlugin: PluginModule = { break; case DOMTopLevelEventTypes.TOP_BLUR: case DOMTopLevelEventTypes.TOP_FOCUS: - case DOMTopLevelEventTypes.TOP_BEFORE_BLUR: - case DOMTopLevelEventTypes.TOP_AFTER_BLUR: EventConstructor = SyntheticFocusEvent; break; case DOMTopLevelEventTypes.TOP_CLICK: @@ -179,10 +170,7 @@ const SimpleEventPlugin: PluginModule = { break; default: if (__DEV__) { - if ( - knownHTMLTopLevelTypes.indexOf(topLevelType) === -1 && - dispatchConfig.customEvent !== true - ) { + if (knownHTMLTopLevelTypes.indexOf(topLevelType) === -1) { console.error( 'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' + 'is likely caused by a bug in React. Please file an issue.', @@ -201,23 +189,7 @@ const SimpleEventPlugin: PluginModule = { nativeEvent, nativeEventTarget, ); - - // 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 && - eventSystemFlags !== undefined && - eventSystemFlags & IS_TARGET_PHASE_ONLY && - targetContainer != null - ) { - accumulateEventTargetListeners(event, targetContainer); - } else { - accumulateTwoPhaseListeners(event, true); - } + accumulateTwoPhaseDispatches(event); return event; }, };