| 1 | /** |
| 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| 3 | * |
| 4 | * This source code is licensed under the MIT license found in the |
| 5 | * LICENSE file in the root directory of this source tree. |
| 6 | * |
| 7 | * @flow |
| 8 | */ |
| 9 | |
| 10 | import type {AnyNativeEvent} from '../PluginModuleType'; |
| 11 | import type {DOMEventName} from '../DOMEventNames'; |
| 12 | import type {DispatchQueue} from '../DOMPluginEventSystem'; |
| 13 | import type {EventSystemFlags} from '../EventSystemFlags'; |
| 14 | import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; |
| 15 | import type {KnownReactSyntheticEvent} from '../ReactSyntheticEventType'; |
| 16 | |
| 17 | import {registerDirectEvent} from '../EventRegistry'; |
| 18 | import {isReplayingEvent} from '../CurrentReplayingEvent'; |
| 19 | import {SyntheticMouseEvent, SyntheticPointerEvent} from '../SyntheticEvent'; |
| 20 | import { |
| 21 | getClosestInstanceFromNode, |
| 22 | getNodeFromInstance, |
| 23 | isContainerMarkedAsRoot, |
| 24 | } from '../../client/ReactDOMComponentTree'; |
| 25 | import {accumulateEnterLeaveTwoPhaseListeners} from '../DOMPluginEventSystem'; |
| 26 | |
| 27 | import { |
| 28 | HostComponent, |
| 29 | HostSingleton, |
| 30 | HostText, |
| 31 | } from 'react-reconciler/src/ReactWorkTags'; |
| 32 | import {getNearestMountedFiber} from 'react-reconciler/src/ReactFiberTreeReflection'; |
| 33 | |
| 34 | function registerEvents() { |
| 35 | registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']); |
| 36 | registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']); |
| 37 | registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']); |
| 38 | registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']); |
| 39 | } |
| 40 | |
| 41 | /** |
| 42 | * For almost every interaction we care about, there will be both a top-level |
| 43 | * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that |
| 44 | * we do not extract duplicate events. However, moving the mouse into the |
| 45 | * browser from outside will not fire a `mouseout` event. In this case, we use |
| 46 | * the `mouseover` top-level event. |
| 47 | */ |
| 48 | function extractEvents( |
| 49 | dispatchQueue: DispatchQueue, |
| 50 | domEventName: DOMEventName, |
| 51 | targetInst: null | Fiber, |
| 52 | nativeEvent: AnyNativeEvent, |
| 53 | nativeEventTarget: null | EventTarget, |
| 54 | eventSystemFlags: EventSystemFlags, |
| 55 | targetContainer: EventTarget, |
| 56 | ) { |
| 57 | const isOverEvent = |
| 58 | domEventName === 'mouseover' || domEventName === 'pointerover'; |
| 59 | const isOutEvent = |
| 60 | domEventName === 'mouseout' || domEventName === 'pointerout'; |
| 61 | |
| 62 | if (isOverEvent && !isReplayingEvent(nativeEvent)) { |
| 63 | // If this is an over event with a target, we might have already dispatched |
| 64 | // the event in the out event of the other target. If this is replayed, |
| 65 | // then it's because we couldn't dispatch against this target previously |
| 66 | // so we have to do it now instead. |
| 67 | const related = |
| 68 | (nativeEvent as any).relatedTarget || (nativeEvent as any).fromElement; |
| 69 | if (related) { |
| 70 | // If the related node is managed by React, we can assume that we have |
| 71 | // already dispatched the corresponding events during its mouseout. |
| 72 | if ( |
| 73 | getClosestInstanceFromNode(related) || |
| 74 | isContainerMarkedAsRoot(related) |
| 75 | ) { |
| 76 | return; |
| 77 | } |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | if (!isOutEvent && !isOverEvent) { |
| 82 | // Must not be a mouse or pointer in or out - ignoring. |
| 83 | return; |
| 84 | } |
| 85 | |
| 86 | let win; |
| 87 | // TODO: why is this nullable in the types but we read from it? |
| 88 | if ((nativeEventTarget as any).window === nativeEventTarget) { |
| 89 | // `nativeEventTarget` is probably a window object. |
| 90 | win = nativeEventTarget; |
| 91 | } else { |
| 92 | // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8. |
| 93 | const doc = (nativeEventTarget as any).ownerDocument; |
| 94 | if (doc) { |
| 95 | win = doc.defaultView || doc.parentWindow; |
| 96 | } else { |
| 97 | win = window; |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | let from; |
| 102 | let to; |
| 103 | if (isOutEvent) { |
| 104 | const related = nativeEvent.relatedTarget || (nativeEvent as any).toElement; |
| 105 | from = targetInst; |
| 106 | to = related ? getClosestInstanceFromNode(related as any) : null; |
| 107 | if (to !== null) { |
| 108 | const nearestMounted = getNearestMountedFiber(to); |
| 109 | const tag = to.tag; |
| 110 | if ( |
| 111 | to !== nearestMounted || |
| 112 | (tag !== HostComponent && tag !== HostSingleton && tag !== HostText) |
| 113 | ) { |
| 114 | to = null; |
| 115 | } |
| 116 | } |
| 117 | } else { |
| 118 | // Moving to a node from outside the window. |
| 119 | from = null; |
| 120 | to = targetInst; |
| 121 | } |
| 122 | |
| 123 | if (from === to) { |
| 124 | // Nothing pertains to our managed components. |
| 125 | return; |
| 126 | } |
| 127 | |
| 128 | let SyntheticEventCtor = SyntheticMouseEvent; |
| 129 | let leaveEventType = 'onMouseLeave'; |
| 130 | let enterEventType = 'onMouseEnter'; |
| 131 | let eventTypePrefix = 'mouse'; |
| 132 | if (domEventName === 'pointerout' || domEventName === 'pointerover') { |
| 133 | SyntheticEventCtor = SyntheticPointerEvent; |
| 134 | leaveEventType = 'onPointerLeave'; |
| 135 | enterEventType = 'onPointerEnter'; |
| 136 | eventTypePrefix = 'pointer'; |
| 137 | } |
| 138 | |
| 139 | const fromNode = from == null ? win : getNodeFromInstance(from); |
| 140 | const toNode = to == null ? win : getNodeFromInstance(to); |
| 141 | |
| 142 | const leave: KnownReactSyntheticEvent = new SyntheticEventCtor( |
| 143 | leaveEventType, |
| 144 | eventTypePrefix + 'leave', |
| 145 | from, |
| 146 | nativeEvent, |
| 147 | nativeEventTarget, |
| 148 | ); |
| 149 | leave.target = fromNode; |
| 150 | leave.relatedTarget = toNode; |
| 151 | |
| 152 | let enter: KnownReactSyntheticEvent | null = null; |
| 153 | |
| 154 | // We should only process this nativeEvent if we are processing |
| 155 | // the first ancestor. Next time, we will ignore the event. |
| 156 | const nativeTargetInst = getClosestInstanceFromNode(nativeEventTarget as any); |
| 157 | if (nativeTargetInst === targetInst) { |
| 158 | const enterEvent: KnownReactSyntheticEvent = new SyntheticEventCtor( |
| 159 | enterEventType, |
| 160 | eventTypePrefix + 'enter', |
| 161 | to, |
| 162 | nativeEvent, |
| 163 | nativeEventTarget, |
| 164 | ); |
| 165 | enterEvent.target = toNode; |
| 166 | enterEvent.relatedTarget = fromNode; |
| 167 | enter = enterEvent; |
| 168 | } |
| 169 | |
| 170 | accumulateEnterLeaveTwoPhaseListeners(dispatchQueue, leave, enter, from, to); |
| 171 | } |
| 172 | |
| 173 | export {registerEvents, extractEvents}; |