packages/react-native-renderer/src/legacy-events/ResponderEventPlugin.js JAVASCRIPT 805 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @noflow8 */910import {11  executeDirectDispatch,12  hasDispatches,13  executeDispatchesInOrderStopAtTrue,14  getInstanceFromNode,15  getFiberCurrentPropsFromNode,16} from './EventPluginUtils';17import ResponderSyntheticEvent from './ResponderSyntheticEvent';18import ResponderTouchHistoryStore from './ResponderTouchHistoryStore';19import accumulate from './accumulate';20import {21  TOP_SCROLL,22  TOP_SELECTION_CHANGE,23  TOP_TOUCH_CANCEL,24  isStartish,25  isMoveish,26  isEndish,27  startDependencies,28  moveDependencies,29  endDependencies,30} from './ResponderTopLevelEventTypes';31import accumulateInto from './accumulateInto';32import forEachAccumulated from './forEachAccumulated';33import {HostComponent} from 'react-reconciler/src/ReactWorkTags';3435/**36 * Instance of element that should respond to touch/move types of interactions,37 * as indicated explicitly by relevant callbacks.38 */39let responderInst = null;4041/**42 * Count of current touches. A textInput should become responder iff the43 * selection changes while there is a touch on the screen.44 */45let trackedTouchCount = 0;4647function changeResponder(nextResponderInst, blockHostResponder) {48  const oldResponderInst = responderInst;49  responderInst = nextResponderInst;50  if (ResponderEventPlugin.GlobalResponderHandler !== null) {51    ResponderEventPlugin.GlobalResponderHandler.onChange(52      oldResponderInst,53      nextResponderInst,54      blockHostResponder,55    );56  }57}5859const eventTypes = {60  /**61   * On a `touchStart`/`mouseDown`, is it desired that this element become the62   * responder?63   */64  startShouldSetResponder: {65    phasedRegistrationNames: {66      bubbled: 'onStartShouldSetResponder',67      captured: 'onStartShouldSetResponderCapture',68    },69    dependencies: startDependencies,70  },7172  /**73   * On a `scroll`, is it desired that this element become the responder? This74   * is usually not needed, but should be used to retroactively infer that a75   * `touchStart` had occurred during momentum scroll. During a momentum scroll,76   * a touch start will be immediately followed by a scroll event if the view is77   * currently scrolling.78   *79   * TODO: This shouldn't bubble.80   */81  scrollShouldSetResponder: {82    phasedRegistrationNames: {83      bubbled: 'onScrollShouldSetResponder',84      captured: 'onScrollShouldSetResponderCapture',85    },86    dependencies: [TOP_SCROLL],87  },8889  /**90   * On text selection change, should this element become the responder? This91   * is needed for text inputs or other views with native selection, so the92   * JS view can claim the responder.93   *94   * TODO: This shouldn't bubble.95   */96  selectionChangeShouldSetResponder: {97    phasedRegistrationNames: {98      bubbled: 'onSelectionChangeShouldSetResponder',99      captured: 'onSelectionChangeShouldSetResponderCapture',100    },101    dependencies: [TOP_SELECTION_CHANGE],102  },103104  /**105   * On a `touchMove`/`mouseMove`, is it desired that this element become the106   * responder?107   */108  moveShouldSetResponder: {109    phasedRegistrationNames: {110      bubbled: 'onMoveShouldSetResponder',111      captured: 'onMoveShouldSetResponderCapture',112    },113    dependencies: moveDependencies,114  },115116  /**117   * Direct responder events dispatched directly to responder. Do not bubble.118   */119  responderStart: {120    registrationName: 'onResponderStart',121    dependencies: startDependencies,122  },123  responderMove: {124    registrationName: 'onResponderMove',125    dependencies: moveDependencies,126  },127  responderEnd: {128    registrationName: 'onResponderEnd',129    dependencies: endDependencies,130  },131  responderRelease: {132    registrationName: 'onResponderRelease',133    dependencies: endDependencies,134  },135  responderTerminationRequest: {136    registrationName: 'onResponderTerminationRequest',137    dependencies: [],138  },139  responderGrant: {140    registrationName: 'onResponderGrant',141    dependencies: [],142  },143  responderReject: {144    registrationName: 'onResponderReject',145    dependencies: [],146  },147  responderTerminate: {148    registrationName: 'onResponderTerminate',149    dependencies: [],150  },151};152153// Start of inline: the below functions were inlined from154// EventPropagator.js, as they deviated from ReactDOM's newer155// implementations.156157function getParent(inst) {158  do {159    inst = inst.return;160    // TODO: If this is a HostRoot we might want to bail out.161    // That is depending on if we want nested subtrees (layers) to bubble162    // events to their parent. We could also go through parentNode on the163    // host node but that wouldn't work for React Native and doesn't let us164    // do the portal feature.165  } while (inst && inst.tag !== HostComponent);166  if (inst) {167    return inst;168  }169  return null;170}171172/**173 * Return the lowest common ancestor of A and B, or null if they are in174 * different trees.175 */176export function getLowestCommonAncestor(instA, instB) {177  let depthA = 0;178  for (let tempA = instA; tempA; tempA = getParent(tempA)) {179    depthA++;180  }181  let depthB = 0;182  for (let tempB = instB; tempB; tempB = getParent(tempB)) {183    depthB++;184  }185186  // If A is deeper, crawl up.187  while (depthA - depthB > 0) {188    instA = getParent(instA);189    depthA--;190  }191192  // If B is deeper, crawl up.193  while (depthB - depthA > 0) {194    instB = getParent(instB);195    depthB--;196  }197198  // Walk in lockstep until we find a match.199  let depth = depthA;200  while (depth--) {201    if (instA === instB || instA === instB.alternate) {202      return instA;203    }204    instA = getParent(instA);205    instB = getParent(instB);206  }207  return null;208}209210/**211 * Return if A is an ancestor of B.212 */213function isAncestor(instA, instB) {214  while (instB) {215    if (instA === instB || instA === instB.alternate) {216      return true;217    }218    instB = getParent(instB);219  }220  return false;221}222223/**224 * Simulates the traversal of a two-phase, capture/bubble event dispatch.225 */226function traverseTwoPhase(inst, fn, arg) {227  const path = [];228  while (inst) {229    path.push(inst);230    inst = getParent(inst);231  }232  let i;233  for (i = path.length; i-- > 0; ) {234    fn(path[i], 'captured', arg);235  }236  for (i = 0; i < path.length; i++) {237    fn(path[i], 'bubbled', arg);238  }239}240241function getListener(inst, registrationName) {242  const stateNode = inst.stateNode;243  if (stateNode === null) {244    // Work in progress (ex: onload events in incremental mode).245    return null;246  }247  const props = getFiberCurrentPropsFromNode(stateNode);248  if (props === null) {249    // Work in progress.250    return null;251  }252  const listener = props[registrationName];253254  if (listener && typeof listener !== 'function') {255    throw new Error(256      `Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`,257    );258  }259260  return listener;261}262263function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {264  const registrationName =265    event.dispatchConfig.phasedRegistrationNames[propagationPhase];266  return getListener(inst, registrationName);267}268269function accumulateDirectionalDispatches(inst, phase, event) {270  if (__DEV__) {271    if (!inst) {272      console.error('Dispatching inst must not be null');273    }274  }275  const listener = listenerAtPhase(inst, event, phase);276  if (listener) {277    event._dispatchListeners = accumulateInto(278      event._dispatchListeners,279      listener,280    );281    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);282  }283}284285/**286 * Accumulates without regard to direction, does not look for phased287 * registration names. Same as `accumulateDirectDispatchesSingle` but without288 * requiring that the `dispatchMarker` be the same as the dispatched ID.289 */290function accumulateDispatches(291  inst: Object,292  ignoredDirection: ?boolean,293  event: Object,294): void {295  if (inst && event && event.dispatchConfig.registrationName) {296    const registrationName = event.dispatchConfig.registrationName;297    const listener = getListener(inst, registrationName);298    if (listener) {299      event._dispatchListeners = accumulateInto(300        event._dispatchListeners,301        listener,302      );303      event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);304    }305  }306}307308/**309 * Accumulates dispatches on an `SyntheticEvent`, but only for the310 * `dispatchMarker`.311 * @param {SyntheticEvent} event312 */313function accumulateDirectDispatchesSingle(event: Object) {314  if (event && event.dispatchConfig.registrationName) {315    accumulateDispatches(event._targetInst, null, event);316  }317}318319function accumulateDirectDispatches(events: ?(Array<Object> | Object)) {320  forEachAccumulated(events, accumulateDirectDispatchesSingle);321}322323function accumulateTwoPhaseDispatchesSingleSkipTarget(event) {324  if (event && event.dispatchConfig.phasedRegistrationNames) {325    const targetInst = event._targetInst;326    const parentInst = targetInst ? getParent(targetInst) : null;327    traverseTwoPhase(parentInst, accumulateDirectionalDispatches, event);328  }329}330331function accumulateTwoPhaseDispatchesSkipTarget(events) {332  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);333}334335function accumulateTwoPhaseDispatchesSingle(event) {336  if (event && event.dispatchConfig.phasedRegistrationNames) {337    traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);338  }339}340341function accumulateTwoPhaseDispatches(events) {342  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);343}344// End of inline345346/**347 *348 * Responder System:349 * ----------------350 *351 * - A global, solitary "interaction lock" on a view.352 * - If a node becomes the responder, it should convey visual feedback353 *   immediately to indicate so, either by highlighting or moving accordingly.354 * - To be the responder means, that touches are exclusively important to that355 *   responder view, and no other view.356 * - While touches are still occurring, the responder lock can be transferred to357 *   a new view, but only to increasingly "higher" views (meaning ancestors of358 *   the current responder).359 *360 * Responder being granted:361 * ------------------------362 *363 * - Touch starts, moves, and scrolls can cause an ID to become the responder.364 * - We capture/bubble `startShouldSetResponder`/`moveShouldSetResponder` to365 *   the "appropriate place".366 * - If nothing is currently the responder, the "appropriate place" is the367 *   initiating event's `targetID`.368 * - If something *is* already the responder, the "appropriate place" is the369 *   first common ancestor of the event target and the current `responderInst`.370 * - Some negotiation happens: See the timing diagram below.371 * - Scrolled views automatically become responder. The reasoning is that a372 *   platform scroll view that isn't built on top of the responder system has373 *   began scrolling, and the active responder must now be notified that the374 *   interaction is no longer locked to it - the system has taken over.375 *376 * - Responder being released:377 *   As soon as no more touches that *started* inside of descendants of the378 *   *current* responderInst, an `onResponderRelease` event is dispatched to the379 *   current responder, and the responder lock is released.380 *381 * TODO:382 * - on "end", a callback hook for `onResponderEndShouldRemainResponder` that383 *   determines if the responder lock should remain.384 * - If a view shouldn't "remain" the responder, any active touches should by385 *   default be considered "dead" and do not influence future negotiations or386 *   bubble paths. It should be as if those touches do not exist.387 * -- For multitouch: Usually a translate-z will choose to "remain" responder388 *  after one out of many touches ended. For translate-y, usually the view389 *  doesn't wish to "remain" responder after one of many touches end.390 * - Consider building this on top of a `stopPropagation` model similar to391 *   `W3C` events.392 * - Ensure that `onResponderTerminate` is called on touch cancels, whether or393 *   not `onResponderTerminationRequest` returns `true` or `false`.394 *395 */396397/*                                             Negotiation Performed398                                             +-----------------------+399                                            /                         \400Process low level events to    +     Current Responder      +   wantsResponderID401determine who to perform negot-|   (if any exists at all)   |402iation/transition              | Otherwise just pass through|403-------------------------------+----------------------------+------------------+404Bubble to find first ID        |                            |405to return true:wantsResponderID|                            |406                               |                            |407     +-------------+           |                            |408     | onTouchStart|           |                            |409     +------+------+     none  |                            |410            |            return|                            |411+-----------v-------------+true| +------------------------+ |412|onStartShouldSetResponder|----->|onResponderStart (cur)  |<-----------+413+-----------+-------------+    | +------------------------+ |          |414            |                  |                            | +--------+-------+415            | returned true for|       false:REJECT +-------->|onResponderReject416            | wantsResponderID |                    |       | +----------------+417            | (now attempt     | +------------------+-----+ |418            |  handoff)        | |   onResponder          | |419            +------------------->|      TerminationRequest| |420                               | +------------------+-----+ |421                               |                    |       | +----------------+422                               |         true:GRANT +-------->|onResponderGrant|423                               |                            | +--------+-------+424                               | +------------------------+ |          |425                               | |   onResponderTerminate |<-----------+426                               | +------------------+-----+ |427                               |                    |       | +----------------+428                               |                    +-------->|onResponderStart|429                               |                            | +----------------+430Bubble to find first ID        |                            |431to return true:wantsResponderID|                            |432                               |                            |433     +-------------+           |                            |434     | onTouchMove |           |                            |435     +------+------+     none  |                            |436            |            return|                            |437+-----------v-------------+true| +------------------------+ |438|onMoveShouldSetResponder |----->|onResponderMove (cur)   |<-----------+439+-----------+-------------+    | +------------------------+ |          |440            |                  |                            | +--------+-------+441            | returned true for|       false:REJECT +-------->|onResponderRejec|442            | wantsResponderID |                    |       | +----------------+443            | (now attempt     | +------------------+-----+ |444            |  handoff)        | |   onResponder          | |445            +------------------->|      TerminationRequest| |446                               | +------------------+-----+ |447                               |                    |       | +----------------+448                               |         true:GRANT +-------->|onResponderGrant|449                               |                            | +--------+-------+450                               | +------------------------+ |          |451                               | |   onResponderTerminate |<-----------+452                               | +------------------+-----+ |453                               |                    |       | +----------------+454                               |                    +-------->|onResponderMove |455                               |                            | +----------------+456                               |                            |457                               |                            |458      Some active touch started|                            |459      inside current responder | +------------------------+ |460      +------------------------->|      onResponderEnd    | |461      |                        | +------------------------+ |462  +---+---------+              |                            |463  | onTouchEnd  |              |                            |464  +---+---------+              |                            |465      |                        | +------------------------+ |466      +------------------------->|     onResponderEnd     | |467      No active touches started| +-----------+------------+ |468      inside current responder |             |              |469                               |             v              |470                               | +------------------------+ |471                               | |    onResponderRelease  | |472                               | +------------------------+ |473                               |                            |474                               +                            + */475476/**477 * A note about event ordering in the `EventPluginRegistry`.478 *479 * Suppose plugins are injected in the following order:480 *481 * `[R, S, C]`482 *483 * To help illustrate the example, assume `S` is `SimpleEventPlugin` (for484 * `onClick` etc) and `R` is `ResponderEventPlugin`.485 *486 * "Deferred-Dispatched Events":487 *488 * - The current event plugin system will traverse the list of injected plugins,489 *   in order, and extract events by collecting the plugin's return value of490 *   `extractEvents()`.491 * - These events that are returned from `extractEvents` are "deferred492 *   dispatched events".493 * - When returned from `extractEvents`, deferred-dispatched events contain an494 *   "accumulation" of deferred dispatches.495 * - These deferred dispatches are accumulated/collected before they are496 *   returned, but processed at a later time by the `EventPluginRegistry` (hence the497 *   name deferred).498 *499 * In the process of returning their deferred-dispatched events, event plugins500 * themselves can dispatch events on-demand without returning them from501 * `extractEvents`. Plugins might want to do this, so that they can use event502 * dispatching as a tool that helps them decide which events should be extracted503 * in the first place.504 *505 * "On-Demand-Dispatched Events":506 *507 * - On-demand-dispatched events are not returned from `extractEvents`.508 * - On-demand-dispatched events are dispatched during the process of returning509 *   the deferred-dispatched events.510 * - They should not have side effects.511 * - They should be avoided, and/or eventually be replaced with another512 *   abstraction that allows event plugins to perform multiple "rounds" of event513 *   extraction.514 *515 * Therefore, the sequence of event dispatches becomes:516 *517 * - `R`s on-demand events (if any)   (dispatched by `R` on-demand)518 * - `S`s on-demand events (if any)   (dispatched by `S` on-demand)519 * - `C`s on-demand events (if any)   (dispatched by `C` on-demand)520 * - `R`s extracted events (if any)   (dispatched by `EventPluginRegistry`)521 * - `S`s extracted events (if any)   (dispatched by `EventPluginRegistry`)522 * - `C`s extracted events (if any)   (dispatched by `EventPluginRegistry`)523 *524 * In the case of `ResponderEventPlugin`: If the `startShouldSetResponder`525 * on-demand dispatch returns `true` (and some other details are satisfied) the526 * `onResponderGrant` deferred dispatched event is returned from527 * `extractEvents`. The sequence of dispatch executions in this case528 * will appear as follows:529 *530 * - `startShouldSetResponder` (`ResponderEventPlugin` dispatches on-demand)531 * - `touchStartCapture`       (`EventPluginRegistry` dispatches as usual)532 * - `touchStart`              (`EventPluginRegistry` dispatches as usual)533 * - `responderGrant/Reject`   (`EventPluginRegistry` dispatches as usual)534 */535536function setResponderAndExtractTransfer(537  topLevelType,538  targetInst,539  nativeEvent,540  nativeEventTarget,541) {542  const shouldSetEventType = isStartish(topLevelType)543    ? eventTypes.startShouldSetResponder544    : isMoveish(topLevelType)545      ? eventTypes.moveShouldSetResponder546      : topLevelType === TOP_SELECTION_CHANGE547        ? eventTypes.selectionChangeShouldSetResponder548        : eventTypes.scrollShouldSetResponder;549550  // TODO: stop one short of the current responder.551  const bubbleShouldSetFrom = !responderInst552    ? targetInst553    : getLowestCommonAncestor(responderInst, targetInst);554555  // When capturing/bubbling the "shouldSet" event, we want to skip the target556  // (deepest ID) if it happens to be the current responder. The reasoning:557  // It's strange to get an `onMoveShouldSetResponder` when you're *already*558  // the responder.559  const skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderInst;560  const shouldSetEvent = ResponderSyntheticEvent.getPooled(561    shouldSetEventType,562    bubbleShouldSetFrom,563    nativeEvent,564    nativeEventTarget,565  );566  shouldSetEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;567  if (skipOverBubbleShouldSetFrom) {568    accumulateTwoPhaseDispatchesSkipTarget(shouldSetEvent);569  } else {570    accumulateTwoPhaseDispatches(shouldSetEvent);571  }572  const wantsResponderInst = executeDispatchesInOrderStopAtTrue(shouldSetEvent);573  if (!shouldSetEvent.isPersistent()) {574    shouldSetEvent.constructor.release(shouldSetEvent);575  }576577  if (!wantsResponderInst || wantsResponderInst === responderInst) {578    return null;579  }580  let extracted;581  const grantEvent = ResponderSyntheticEvent.getPooled(582    eventTypes.responderGrant,583    wantsResponderInst,584    nativeEvent,585    nativeEventTarget,586  );587  grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;588589  accumulateDirectDispatches(grantEvent);590  const blockHostResponder = executeDirectDispatch(grantEvent) === true;591  if (responderInst) {592    const terminationRequestEvent = ResponderSyntheticEvent.getPooled(593      eventTypes.responderTerminationRequest,594      responderInst,595      nativeEvent,596      nativeEventTarget,597    );598    terminationRequestEvent.touchHistory =599      ResponderTouchHistoryStore.touchHistory;600    accumulateDirectDispatches(terminationRequestEvent);601    const shouldSwitch =602      !hasDispatches(terminationRequestEvent) ||603      executeDirectDispatch(terminationRequestEvent);604    if (!terminationRequestEvent.isPersistent()) {605      terminationRequestEvent.constructor.release(terminationRequestEvent);606    }607608    if (shouldSwitch) {609      const terminateEvent = ResponderSyntheticEvent.getPooled(610        eventTypes.responderTerminate,611        responderInst,612        nativeEvent,613        nativeEventTarget,614      );615      terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;616      accumulateDirectDispatches(terminateEvent);617      extracted = accumulate(extracted, [grantEvent, terminateEvent]);618      changeResponder(wantsResponderInst, blockHostResponder);619    } else {620      const rejectEvent = ResponderSyntheticEvent.getPooled(621        eventTypes.responderReject,622        wantsResponderInst,623        nativeEvent,624        nativeEventTarget,625      );626      rejectEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;627      accumulateDirectDispatches(rejectEvent);628      extracted = accumulate(extracted, rejectEvent);629    }630  } else {631    extracted = accumulate(extracted, grantEvent);632    changeResponder(wantsResponderInst, blockHostResponder);633  }634  return extracted;635}636637/**638 * A transfer is a negotiation between a currently set responder and the next639 * element to claim responder status. Any start event could trigger a transfer640 * of responderInst. Any move event could trigger a transfer.641 *642 * @param {string} topLevelType Record from `BrowserEventConstants`.643 * @return {boolean} True if a transfer of responder could possibly occur.644 */645function canTriggerTransfer(topLevelType, topLevelInst, nativeEvent) {646  return (647    topLevelInst &&648    // responderIgnoreScroll: We are trying to migrate away from specifically649    // tracking native scroll events here and responderIgnoreScroll indicates we650    // will send topTouchCancel to handle canceling touch events instead651    ((topLevelType === TOP_SCROLL && !nativeEvent.responderIgnoreScroll) ||652      (trackedTouchCount > 0 && topLevelType === TOP_SELECTION_CHANGE) ||653      isStartish(topLevelType) ||654      isMoveish(topLevelType))655  );656}657658/**659 * Returns whether or not this touch end event makes it such that there are no660 * longer any touches that started inside of the current `responderInst`.661 *662 * @param {NativeEvent} nativeEvent Native touch end event.663 * @return {boolean} Whether or not this touch end event ends the responder.664 */665function noResponderTouches(nativeEvent) {666  const touches = nativeEvent.touches;667  if (!touches || touches.length === 0) {668    return true;669  }670  for (let i = 0; i < touches.length; i++) {671    const activeTouch = touches[i];672    const target = activeTouch.target;673    if (target !== null && target !== undefined && target !== 0) {674      // Is the original touch location inside of the current responder?675      const targetInst = getInstanceFromNode(target);676      if (isAncestor(responderInst, targetInst)) {677        return false;678      }679    }680  }681  return true;682}683684const ResponderEventPlugin = {685  /* For unit testing only */686  _getResponder: function () {687    return responderInst;688  },689690  eventTypes: eventTypes,691692  /**693   * We must be resilient to `targetInst` being `null` on `touchMove` or694   * `touchEnd`. On certain platforms, this means that a native scroll has695   * assumed control and the original touch targets are destroyed.696   */697  extractEvents: function (698    topLevelType,699    targetInst,700    nativeEvent,701    nativeEventTarget,702    eventSystemFlags,703  ) {704    if (isStartish(topLevelType)) {705      trackedTouchCount += 1;706    } else if (isEndish(topLevelType)) {707      if (trackedTouchCount >= 0) {708        trackedTouchCount -= 1;709      } else {710        if (__DEV__) {711          console.warn(712            'Ended a touch event which was not counted in `trackedTouchCount`.',713          );714        }715        return null;716      }717    }718719    ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);720721    let extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent)722      ? setResponderAndExtractTransfer(723          topLevelType,724          targetInst,725          nativeEvent,726          nativeEventTarget,727        )728      : null;729    // Responder may or may not have transferred on a new touch start/move.730    // Regardless, whoever is the responder after any potential transfer, we731    // direct all touch start/move/ends to them in the form of732    // `onResponderMove/Start/End`. These will be called for *every* additional733    // finger that move/start/end, dispatched directly to whoever is the734    // current responder at that moment, until the responder is "released".735    //736    // These multiple individual change touch events are are always bookended737    // by `onResponderGrant`, and one of738    // (`onResponderRelease/onResponderTerminate`).739    const isResponderTouchStart = responderInst && isStartish(topLevelType);740    const isResponderTouchMove = responderInst && isMoveish(topLevelType);741    const isResponderTouchEnd = responderInst && isEndish(topLevelType);742    const incrementalTouch = isResponderTouchStart743      ? eventTypes.responderStart744      : isResponderTouchMove745        ? eventTypes.responderMove746        : isResponderTouchEnd747          ? eventTypes.responderEnd748          : null;749750    if (incrementalTouch) {751      const gesture = ResponderSyntheticEvent.getPooled(752        incrementalTouch,753        responderInst,754        nativeEvent,755        nativeEventTarget,756      );757      gesture.touchHistory = ResponderTouchHistoryStore.touchHistory;758      accumulateDirectDispatches(gesture);759      extracted = accumulate(extracted, gesture);760    }761762    const isResponderTerminate =763      responderInst && topLevelType === TOP_TOUCH_CANCEL;764    const isResponderRelease =765      responderInst &&766      !isResponderTerminate &&767      isEndish(topLevelType) &&768      noResponderTouches(nativeEvent);769    const finalTouch = isResponderTerminate770      ? eventTypes.responderTerminate771      : isResponderRelease772        ? eventTypes.responderRelease773        : null;774    if (finalTouch) {775      const finalEvent = ResponderSyntheticEvent.getPooled(776        finalTouch,777        responderInst,778        nativeEvent,779        nativeEventTarget,780      );781      finalEvent.touchHistory = ResponderTouchHistoryStore.touchHistory;782      accumulateDirectDispatches(finalEvent);783      extracted = accumulate(extracted, finalEvent);784      changeResponder(null);785    }786787    return extracted;788  },789790  GlobalResponderHandler: null,791792  injection: {793    /**794     * @param {{onChange: (ReactID, ReactID) => void} GlobalResponderHandler795     * Object that handles any change in responder. Use this to inject796     * integration with an existing touch handling system etc.797     */798    injectGlobalResponderHandler(GlobalResponderHandler) {799      ResponderEventPlugin.GlobalResponderHandler = GlobalResponderHandler;800    },801  },802};803804export default ResponderEventPlugin;

Code quality findings 18

Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (ResponderEventPlugin.GlobalResponderHandler !== null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
} while (inst && inst.tag !== HostComponent);
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (instA === instB || instA === instB.alternate) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (instA === instB || instA === instB.alternate) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (stateNode === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (props === null) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (listener && typeof listener !== 'function') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
if (listener && typeof listener !== 'function') {
Be cautious with typeof; it has limitations (e.g., typeof null === 'object')
info correctness typeof-pitfall
`Expected \`${registrationName}\` listener to be a function, instead got a value of \`${typeof listener}\` type.`,
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
: topLevelType === TOP_SELECTION_CHANGE
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
const skipOverBubbleShouldSetFrom = bubbleShouldSetFrom === responderInst;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (!wantsResponderInst || wantsResponderInst === responderInst) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
const blockHostResponder = executeDirectDispatch(grantEvent) === true;
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
((topLevelType === TOP_SCROLL && !nativeEvent.responderIgnoreScroll) ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
(trackedTouchCount > 0 && topLevelType === TOP_SELECTION_CHANGE) ||
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (!touches || touches.length === 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
if (target !== null && target !== undefined && target !== 0) {
Use strict equality (===) to prevent type coercion bugs
info correctness loose-equality
responderInst && topLevelType === TOP_TOUCH_CANCEL;

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.