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 {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
    
  11. import type {AnyNativeEvent} from '../events/PluginModuleType';
    
  12. import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
    
  13. import type {Container, SuspenseInstance} from '../client/ReactFiberConfigDOM';
    
  14. import type {DOMEventName} from '../events/DOMEventNames';
    
  15. 
    
  16. import {
    
  17.   isDiscreteEventThatRequiresHydration,
    
  18.   clearIfContinuousEvent,
    
  19.   queueIfContinuousEvent,
    
  20. } from './ReactDOMEventReplaying';
    
  21. import {attemptSynchronousHydration} from 'react-reconciler/src/ReactFiberReconciler';
    
  22. import {
    
  23.   getNearestMountedFiber,
    
  24.   getContainerFromFiber,
    
  25.   getSuspenseInstanceFromFiber,
    
  26. } from 'react-reconciler/src/ReactFiberTreeReflection';
    
  27. import {HostRoot, SuspenseComponent} from 'react-reconciler/src/ReactWorkTags';
    
  28. import {type EventSystemFlags, IS_CAPTURE_PHASE} from './EventSystemFlags';
    
  29. 
    
  30. import getEventTarget from './getEventTarget';
    
  31. import {
    
  32.   getInstanceFromNode,
    
  33.   getClosestInstanceFromNode,
    
  34. } from '../client/ReactDOMComponentTree';
    
  35. 
    
  36. import {dispatchEventForPluginEventSystem} from './DOMPluginEventSystem';
    
  37. 
    
  38. import {
    
  39.   getCurrentPriorityLevel as getCurrentSchedulerPriorityLevel,
    
  40.   IdlePriority as IdleSchedulerPriority,
    
  41.   ImmediatePriority as ImmediateSchedulerPriority,
    
  42.   LowPriority as LowSchedulerPriority,
    
  43.   NormalPriority as NormalSchedulerPriority,
    
  44.   UserBlockingPriority as UserBlockingSchedulerPriority,
    
  45. } from 'react-reconciler/src/Scheduler';
    
  46. import {
    
  47.   DiscreteEventPriority,
    
  48.   ContinuousEventPriority,
    
  49.   DefaultEventPriority,
    
  50.   IdleEventPriority,
    
  51.   getCurrentUpdatePriority,
    
  52.   setCurrentUpdatePriority,
    
  53. } from 'react-reconciler/src/ReactEventPriorities';
    
  54. import ReactSharedInternals from 'shared/ReactSharedInternals';
    
  55. import {isRootDehydrated} from 'react-reconciler/src/ReactFiberShellHydration';
    
  56. 
    
  57. const {ReactCurrentBatchConfig} = ReactSharedInternals;
    
  58. 
    
  59. // TODO: can we stop exporting these?
    
  60. let _enabled: boolean = true;
    
  61. 
    
  62. // This is exported in FB builds for use by legacy FB layer infra.
    
  63. // We'd like to remove this but it's not clear if this is safe.
    
  64. export function setEnabled(enabled: ?boolean): void {
    
  65.   _enabled = !!enabled;
    
  66. }
    
  67. 
    
  68. export function isEnabled(): boolean {
    
  69.   return _enabled;
    
  70. }
    
  71. 
    
  72. export function createEventListenerWrapper(
    
  73.   targetContainer: EventTarget,
    
  74.   domEventName: DOMEventName,
    
  75.   eventSystemFlags: EventSystemFlags,
    
  76. ): Function {
    
  77.   return dispatchEvent.bind(
    
  78.     null,
    
  79.     domEventName,
    
  80.     eventSystemFlags,
    
  81.     targetContainer,
    
  82.   );
    
  83. }
    
  84. 
    
  85. export function createEventListenerWrapperWithPriority(
    
  86.   targetContainer: EventTarget,
    
  87.   domEventName: DOMEventName,
    
  88.   eventSystemFlags: EventSystemFlags,
    
  89. ): Function {
    
  90.   const eventPriority = getEventPriority(domEventName);
    
  91.   let listenerWrapper;
    
  92.   switch (eventPriority) {
    
  93.     case DiscreteEventPriority:
    
  94.       listenerWrapper = dispatchDiscreteEvent;
    
  95.       break;
    
  96.     case ContinuousEventPriority:
    
  97.       listenerWrapper = dispatchContinuousEvent;
    
  98.       break;
    
  99.     case DefaultEventPriority:
    
  100.     default:
    
  101.       listenerWrapper = dispatchEvent;
    
  102.       break;
    
  103.   }
    
  104.   return listenerWrapper.bind(
    
  105.     null,
    
  106.     domEventName,
    
  107.     eventSystemFlags,
    
  108.     targetContainer,
    
  109.   );
    
  110. }
    
  111. 
    
  112. function dispatchDiscreteEvent(
    
  113.   domEventName: DOMEventName,
    
  114.   eventSystemFlags: EventSystemFlags,
    
  115.   container: EventTarget,
    
  116.   nativeEvent: AnyNativeEvent,
    
  117. ) {
    
  118.   const previousPriority = getCurrentUpdatePriority();
    
  119.   const prevTransition = ReactCurrentBatchConfig.transition;
    
  120.   ReactCurrentBatchConfig.transition = null;
    
  121.   try {
    
  122.     setCurrentUpdatePriority(DiscreteEventPriority);
    
  123.     dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
    
  124.   } finally {
    
  125.     setCurrentUpdatePriority(previousPriority);
    
  126.     ReactCurrentBatchConfig.transition = prevTransition;
    
  127.   }
    
  128. }
    
  129. 
    
  130. function dispatchContinuousEvent(
    
  131.   domEventName: DOMEventName,
    
  132.   eventSystemFlags: EventSystemFlags,
    
  133.   container: EventTarget,
    
  134.   nativeEvent: AnyNativeEvent,
    
  135. ) {
    
  136.   const previousPriority = getCurrentUpdatePriority();
    
  137.   const prevTransition = ReactCurrentBatchConfig.transition;
    
  138.   ReactCurrentBatchConfig.transition = null;
    
  139.   try {
    
  140.     setCurrentUpdatePriority(ContinuousEventPriority);
    
  141.     dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
    
  142.   } finally {
    
  143.     setCurrentUpdatePriority(previousPriority);
    
  144.     ReactCurrentBatchConfig.transition = prevTransition;
    
  145.   }
    
  146. }
    
  147. 
    
  148. export function dispatchEvent(
    
  149.   domEventName: DOMEventName,
    
  150.   eventSystemFlags: EventSystemFlags,
    
  151.   targetContainer: EventTarget,
    
  152.   nativeEvent: AnyNativeEvent,
    
  153. ): void {
    
  154.   if (!_enabled) {
    
  155.     return;
    
  156.   }
    
  157. 
    
  158.   let blockedOn = findInstanceBlockingEvent(nativeEvent);
    
  159.   if (blockedOn === null) {
    
  160.     dispatchEventForPluginEventSystem(
    
  161.       domEventName,
    
  162.       eventSystemFlags,
    
  163.       nativeEvent,
    
  164.       return_targetInst,
    
  165.       targetContainer,
    
  166.     );
    
  167.     clearIfContinuousEvent(domEventName, nativeEvent);
    
  168.     return;
    
  169.   }
    
  170. 
    
  171.   if (
    
  172.     queueIfContinuousEvent(
    
  173.       blockedOn,
    
  174.       domEventName,
    
  175.       eventSystemFlags,
    
  176.       targetContainer,
    
  177.       nativeEvent,
    
  178.     )
    
  179.   ) {
    
  180.     nativeEvent.stopPropagation();
    
  181.     return;
    
  182.   }
    
  183.   // We need to clear only if we didn't queue because
    
  184.   // queueing is accumulative.
    
  185.   clearIfContinuousEvent(domEventName, nativeEvent);
    
  186. 
    
  187.   if (
    
  188.     eventSystemFlags & IS_CAPTURE_PHASE &&
    
  189.     isDiscreteEventThatRequiresHydration(domEventName)
    
  190.   ) {
    
  191.     while (blockedOn !== null) {
    
  192.       const fiber = getInstanceFromNode(blockedOn);
    
  193.       if (fiber !== null) {
    
  194.         attemptSynchronousHydration(fiber);
    
  195.       }
    
  196.       const nextBlockedOn = findInstanceBlockingEvent(nativeEvent);
    
  197.       if (nextBlockedOn === null) {
    
  198.         dispatchEventForPluginEventSystem(
    
  199.           domEventName,
    
  200.           eventSystemFlags,
    
  201.           nativeEvent,
    
  202.           return_targetInst,
    
  203.           targetContainer,
    
  204.         );
    
  205.       }
    
  206.       if (nextBlockedOn === blockedOn) {
    
  207.         break;
    
  208.       }
    
  209.       blockedOn = nextBlockedOn;
    
  210.     }
    
  211.     if (blockedOn !== null) {
    
  212.       nativeEvent.stopPropagation();
    
  213.     }
    
  214.     return;
    
  215.   }
    
  216. 
    
  217.   // This is not replayable so we'll invoke it but without a target,
    
  218.   // in case the event system needs to trace it.
    
  219.   dispatchEventForPluginEventSystem(
    
  220.     domEventName,
    
  221.     eventSystemFlags,
    
  222.     nativeEvent,
    
  223.     null,
    
  224.     targetContainer,
    
  225.   );
    
  226. }
    
  227. 
    
  228. export function findInstanceBlockingEvent(
    
  229.   nativeEvent: AnyNativeEvent,
    
  230. ): null | Container | SuspenseInstance {
    
  231.   const nativeEventTarget = getEventTarget(nativeEvent);
    
  232.   return findInstanceBlockingTarget(nativeEventTarget);
    
  233. }
    
  234. 
    
  235. export let return_targetInst: null | Fiber = null;
    
  236. 
    
  237. // Returns a SuspenseInstance or Container if it's blocked.
    
  238. // The return_targetInst field above is conceptually part of the return value.
    
  239. export function findInstanceBlockingTarget(
    
  240.   targetNode: Node,
    
  241. ): null | Container | SuspenseInstance {
    
  242.   // TODO: Warn if _enabled is false.
    
  243. 
    
  244.   return_targetInst = null;
    
  245. 
    
  246.   let targetInst = getClosestInstanceFromNode(targetNode);
    
  247. 
    
  248.   if (targetInst !== null) {
    
  249.     const nearestMounted = getNearestMountedFiber(targetInst);
    
  250.     if (nearestMounted === null) {
    
  251.       // This tree has been unmounted already. Dispatch without a target.
    
  252.       targetInst = null;
    
  253.     } else {
    
  254.       const tag = nearestMounted.tag;
    
  255.       if (tag === SuspenseComponent) {
    
  256.         const instance = getSuspenseInstanceFromFiber(nearestMounted);
    
  257.         if (instance !== null) {
    
  258.           // Queue the event to be replayed later. Abort dispatching since we
    
  259.           // don't want this event dispatched twice through the event system.
    
  260.           // TODO: If this is the first discrete event in the queue. Schedule an increased
    
  261.           // priority for this boundary.
    
  262.           return instance;
    
  263.         }
    
  264.         // This shouldn't happen, something went wrong but to avoid blocking
    
  265.         // the whole system, dispatch the event without a target.
    
  266.         // TODO: Warn.
    
  267.         targetInst = null;
    
  268.       } else if (tag === HostRoot) {
    
  269.         const root: FiberRoot = nearestMounted.stateNode;
    
  270.         if (isRootDehydrated(root)) {
    
  271.           // If this happens during a replay something went wrong and it might block
    
  272.           // the whole system.
    
  273.           return getContainerFromFiber(nearestMounted);
    
  274.         }
    
  275.         targetInst = null;
    
  276.       } else if (nearestMounted !== targetInst) {
    
  277.         // If we get an event (ex: img onload) before committing that
    
  278.         // component's mount, ignore it for now (that is, treat it as if it was an
    
  279.         // event on a non-React tree). We might also consider queueing events and
    
  280.         // dispatching them after the mount.
    
  281.         targetInst = null;
    
  282.       }
    
  283.     }
    
  284.   }
    
  285.   return_targetInst = targetInst;
    
  286.   // We're not blocked on anything.
    
  287.   return null;
    
  288. }
    
  289. 
    
  290. export function getEventPriority(domEventName: DOMEventName): EventPriority {
    
  291.   switch (domEventName) {
    
  292.     // Used by SimpleEventPlugin:
    
  293.     case 'cancel':
    
  294.     case 'click':
    
  295.     case 'close':
    
  296.     case 'contextmenu':
    
  297.     case 'copy':
    
  298.     case 'cut':
    
  299.     case 'auxclick':
    
  300.     case 'dblclick':
    
  301.     case 'dragend':
    
  302.     case 'dragstart':
    
  303.     case 'drop':
    
  304.     case 'focusin':
    
  305.     case 'focusout':
    
  306.     case 'input':
    
  307.     case 'invalid':
    
  308.     case 'keydown':
    
  309.     case 'keypress':
    
  310.     case 'keyup':
    
  311.     case 'mousedown':
    
  312.     case 'mouseup':
    
  313.     case 'paste':
    
  314.     case 'pause':
    
  315.     case 'play':
    
  316.     case 'pointercancel':
    
  317.     case 'pointerdown':
    
  318.     case 'pointerup':
    
  319.     case 'ratechange':
    
  320.     case 'reset':
    
  321.     case 'resize':
    
  322.     case 'seeked':
    
  323.     case 'submit':
    
  324.     case 'touchcancel':
    
  325.     case 'touchend':
    
  326.     case 'touchstart':
    
  327.     case 'volumechange':
    
  328.     // Used by polyfills: (fall through)
    
  329.     case 'change':
    
  330.     case 'selectionchange':
    
  331.     case 'textInput':
    
  332.     case 'compositionstart':
    
  333.     case 'compositionend':
    
  334.     case 'compositionupdate':
    
  335.     // Only enableCreateEventHandleAPI: (fall through)
    
  336.     case 'beforeblur':
    
  337.     case 'afterblur':
    
  338.     // Not used by React but could be by user code: (fall through)
    
  339.     case 'beforeinput':
    
  340.     case 'blur':
    
  341.     case 'fullscreenchange':
    
  342.     case 'focus':
    
  343.     case 'hashchange':
    
  344.     case 'popstate':
    
  345.     case 'select':
    
  346.     case 'selectstart':
    
  347.       return DiscreteEventPriority;
    
  348.     case 'drag':
    
  349.     case 'dragenter':
    
  350.     case 'dragexit':
    
  351.     case 'dragleave':
    
  352.     case 'dragover':
    
  353.     case 'mousemove':
    
  354.     case 'mouseout':
    
  355.     case 'mouseover':
    
  356.     case 'pointermove':
    
  357.     case 'pointerout':
    
  358.     case 'pointerover':
    
  359.     case 'scroll':
    
  360.     case 'toggle':
    
  361.     case 'touchmove':
    
  362.     case 'wheel':
    
  363.     // Not used by React but could be by user code: (fall through)
    
  364.     case 'mouseenter':
    
  365.     case 'mouseleave':
    
  366.     case 'pointerenter':
    
  367.     case 'pointerleave':
    
  368.       return ContinuousEventPriority;
    
  369.     case 'message': {
    
  370.       // We might be in the Scheduler callback.
    
  371.       // Eventually this mechanism will be replaced by a check
    
  372.       // of the current priority on the native scheduler.
    
  373.       const schedulerPriority = getCurrentSchedulerPriorityLevel();
    
  374.       switch (schedulerPriority) {
    
  375.         case ImmediateSchedulerPriority:
    
  376.           return DiscreteEventPriority;
    
  377.         case UserBlockingSchedulerPriority:
    
  378.           return ContinuousEventPriority;
    
  379.         case NormalSchedulerPriority:
    
  380.         case LowSchedulerPriority:
    
  381.           // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
    
  382.           return DefaultEventPriority;
    
  383.         case IdleSchedulerPriority:
    
  384.           return IdleEventPriority;
    
  385.         default:
    
  386.           return DefaultEventPriority;
    
  387.       }
    
  388.     }
    
  389.     default:
    
  390.       return DefaultEventPriority;
    
  391.   }
    
  392. }