main
js 804 lines 28.8 KB
Raw
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 * @noflow
8 */
9
10 import {
11 executeDirectDispatch,
12 hasDispatches,
13 executeDispatchesInOrderStopAtTrue,
14 getInstanceFromNode,
15 getFiberCurrentPropsFromNode,
16 } from './EventPluginUtils';
17 import ResponderSyntheticEvent from './ResponderSyntheticEvent';
18 import ResponderTouchHistoryStore from './ResponderTouchHistoryStore';
19 import accumulate from './accumulate';
20 import {
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';
31 import accumulateInto from './accumulateInto';
32 import forEachAccumulated from './forEachAccumulated';
33 import {HostComponent} from 'react-reconciler/src/ReactWorkTags';
34
35 /**
36 * Instance of element that should respond to touch/move types of interactions,
37 * as indicated explicitly by relevant callbacks.
38 */
39 let responderInst = null;
40
41 /**
42 * Count of current touches. A textInput should become responder iff the
43 * selection changes while there is a touch on the screen.
44 */
45 let trackedTouchCount = 0;
46
47 function 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 }
58
59 const eventTypes = {
60 /**
61 * On a `touchStart`/`mouseDown`, is it desired that this element become the
62 * responder?
63 */
64 startShouldSetResponder: {
65 phasedRegistrationNames: {
66 bubbled: 'onStartShouldSetResponder',
67 captured: 'onStartShouldSetResponderCapture',
68 },
69 dependencies: startDependencies,
70 },
71
72 /**
73 * On a `scroll`, is it desired that this element become the responder? This
74 * is usually not needed, but should be used to retroactively infer that a
75 * `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 is
77 * 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 },
88
89 /**
90 * On text selection change, should this element become the responder? This
91 * is needed for text inputs or other views with native selection, so the
92 * 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 },
103
104 /**
105 * On a `touchMove`/`mouseMove`, is it desired that this element become the
106 * responder?
107 */
108 moveShouldSetResponder: {
109 phasedRegistrationNames: {
110 bubbled: 'onMoveShouldSetResponder',
111 captured: 'onMoveShouldSetResponderCapture',
112 },
113 dependencies: moveDependencies,
114 },
115
116 /**
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 };
152
153 // Start of inline: the below functions were inlined from
154 // EventPropagator.js, as they deviated from ReactDOM's newer
155 // implementations.
156
157 function 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 bubble
162 // events to their parent. We could also go through parentNode on the
163 // host node but that wouldn't work for React Native and doesn't let us
164 // do the portal feature.
165 } while (inst && inst.tag !== HostComponent);
166 if (inst) {
167 return inst;
168 }
169 return null;
170 }
171
172 /**
173 * Return the lowest common ancestor of A and B, or null if they are in
174 * different trees.
175 */
176 export 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 }
185
186 // If A is deeper, crawl up.
187 while (depthA - depthB > 0) {
188 instA = getParent(instA);
189 depthA--;
190 }
191
192 // If B is deeper, crawl up.
193 while (depthB - depthA > 0) {
194 instB = getParent(instB);
195 depthB--;
196 }
197
198 // 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 }
209
210 /**
211 * Return if A is an ancestor of B.
212 */
213 function 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 }
222
223 /**
224 * Simulates the traversal of a two-phase, capture/bubble event dispatch.
225 */
226 function 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 }
240
241 function 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];
253
254 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 }
259
260 return listener;
261 }
262
263 function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
264 const registrationName =
265 event.dispatchConfig.phasedRegistrationNames[propagationPhase];
266 return getListener(inst, registrationName);
267 }
268
269 function 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 }
284
285 /**
286 * Accumulates without regard to direction, does not look for phased
287 * registration names. Same as `accumulateDirectDispatchesSingle` but without
288 * requiring that the `dispatchMarker` be the same as the dispatched ID.
289 */
290 function 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 }
307
308 /**
309 * Accumulates dispatches on an `SyntheticEvent`, but only for the
310 * `dispatchMarker`.
311 * @param {SyntheticEvent} event
312 */
313 function accumulateDirectDispatchesSingle(event: Object) {
314 if (event && event.dispatchConfig.registrationName) {
315 accumulateDispatches(event._targetInst, null, event);
316 }
317 }
318
319 function accumulateDirectDispatches(events: ?(Array<Object> | Object)) {
320 forEachAccumulated(events, accumulateDirectDispatchesSingle);
321 }
322
323 function 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 }
330
331 function accumulateTwoPhaseDispatchesSkipTarget(events) {
332 forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget);
333 }
334
335 function accumulateTwoPhaseDispatchesSingle(event) {
336 if (event && event.dispatchConfig.phasedRegistrationNames) {
337 traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
338 }
339 }
340
341 function accumulateTwoPhaseDispatches(events) {
342 forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
343 }
344 // End of inline
345
346 /**
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 feedback
353 * immediately to indicate so, either by highlighting or moving accordingly.
354 * - To be the responder means, that touches are exclusively important to that
355 * responder view, and no other view.
356 * - While touches are still occurring, the responder lock can be transferred to
357 * a new view, but only to increasingly "higher" views (meaning ancestors of
358 * 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` to
365 * the "appropriate place".
366 * - If nothing is currently the responder, the "appropriate place" is the
367 * initiating event's `targetID`.
368 * - If something *is* already the responder, the "appropriate place" is the
369 * 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 a
372 * platform scroll view that isn't built on top of the responder system has
373 * began scrolling, and the active responder must now be notified that the
374 * 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 the
378 * *current* responderInst, an `onResponderRelease` event is dispatched to the
379 * current responder, and the responder lock is released.
380 *
381 * TODO:
382 * - on "end", a callback hook for `onResponderEndShouldRemainResponder` that
383 * determines if the responder lock should remain.
384 * - If a view shouldn't "remain" the responder, any active touches should by
385 * default be considered "dead" and do not influence future negotiations or
386 * bubble paths. It should be as if those touches do not exist.
387 * -- For multitouch: Usually a translate-z will choose to "remain" responder
388 * after one out of many touches ended. For translate-y, usually the view
389 * doesn't wish to "remain" responder after one of many touches end.
390 * - Consider building this on top of a `stopPropagation` model similar to
391 * `W3C` events.
392 * - Ensure that `onResponderTerminate` is called on touch cancels, whether or
393 * not `onResponderTerminationRequest` returns `true` or `false`.
394 *
395 */
396
397 /* Negotiation Performed
398 +-----------------------+
399 / \
400 Process low level events to + Current Responder + wantsResponderID
401 determine who to perform negot-| (if any exists at all) |
402 iation/transition | Otherwise just pass through|
403 -------------------------------+----------------------------+------------------+
404 Bubble to find first ID | |
405 to 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 +-------->|onResponderReject
416 | wantsResponderID | | | +----------------+
417 | (now attempt | +------------------+-----+ |
418 | handoff) | | onResponder | |
419 +------------------->| TerminationRequest| |
420 | +------------------+-----+ |
421 | | | +----------------+
422 | true:GRANT +-------->|onResponderGrant|
423 | | +--------+-------+
424 | +------------------------+ | |
425 | | onResponderTerminate |<-----------+
426 | +------------------+-----+ |
427 | | | +----------------+
428 | +-------->|onResponderStart|
429 | | +----------------+
430 Bubble to find first ID | |
431 to 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 + + */
475
476 /**
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` (for
484 * `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 of
490 * `extractEvents()`.
491 * - These events that are returned from `extractEvents` are "deferred
492 * dispatched events".
493 * - When returned from `extractEvents`, deferred-dispatched events contain an
494 * "accumulation" of deferred dispatches.
495 * - These deferred dispatches are accumulated/collected before they are
496 * returned, but processed at a later time by the `EventPluginRegistry` (hence the
497 * name deferred).
498 *
499 * In the process of returning their deferred-dispatched events, event plugins
500 * themselves can dispatch events on-demand without returning them from
501 * `extractEvents`. Plugins might want to do this, so that they can use event
502 * dispatching as a tool that helps them decide which events should be extracted
503 * 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 returning
509 * the deferred-dispatched events.
510 * - They should not have side effects.
511 * - They should be avoided, and/or eventually be replaced with another
512 * abstraction that allows event plugins to perform multiple "rounds" of event
513 * 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) the
526 * `onResponderGrant` deferred dispatched event is returned from
527 * `extractEvents`. The sequence of dispatch executions in this case
528 * 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 */
535
536 function setResponderAndExtractTransfer(
537 topLevelType,
538 targetInst,
539 nativeEvent,
540 nativeEventTarget,
541 ) {
542 const shouldSetEventType = isStartish(topLevelType)
543 ? eventTypes.startShouldSetResponder
544 : isMoveish(topLevelType)
545 ? eventTypes.moveShouldSetResponder
546 : topLevelType === TOP_SELECTION_CHANGE
547 ? eventTypes.selectionChangeShouldSetResponder
548 : eventTypes.scrollShouldSetResponder;
549
550 // TODO: stop one short of the current responder.
551 const bubbleShouldSetFrom = !responderInst
552 ? targetInst
553 : getLowestCommonAncestor(responderInst, targetInst);
554
555 // When capturing/bubbling the "shouldSet" event, we want to skip the target
556 // (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 }
576
577 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;
588
589 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 }
607
608 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 }
636
637 /**
638 * A transfer is a negotiation between a currently set responder and the next
639 * element to claim responder status. Any start event could trigger a transfer
640 * 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 */
645 function canTriggerTransfer(topLevelType, topLevelInst, nativeEvent) {
646 return (
647 topLevelInst &&
648 // responderIgnoreScroll: We are trying to migrate away from specifically
649 // tracking native scroll events here and responderIgnoreScroll indicates we
650 // will send topTouchCancel to handle canceling touch events instead
651 ((topLevelType === TOP_SCROLL && !nativeEvent.responderIgnoreScroll) ||
652 (trackedTouchCount > 0 && topLevelType === TOP_SELECTION_CHANGE) ||
653 isStartish(topLevelType) ||
654 isMoveish(topLevelType))
655 );
656 }
657
658 /**
659 * Returns whether or not this touch end event makes it such that there are no
660 * 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 */
665 function 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 }
683
684 const ResponderEventPlugin = {
685 /* For unit testing only */
686 _getResponder: function () {
687 return responderInst;
688 },
689
690 eventTypes: eventTypes,
691
692 /**
693 * We must be resilient to `targetInst` being `null` on `touchMove` or
694 * `touchEnd`. On certain platforms, this means that a native scroll has
695 * 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 }
718
719 ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent);
720
721 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, we
731 // direct all touch start/move/ends to them in the form of
732 // `onResponderMove/Start/End`. These will be called for *every* additional
733 // finger that move/start/end, dispatched directly to whoever is the
734 // current responder at that moment, until the responder is "released".
735 //
736 // These multiple individual change touch events are are always bookended
737 // by `onResponderGrant`, and one of
738 // (`onResponderRelease/onResponderTerminate`).
739 const isResponderTouchStart = responderInst && isStartish(topLevelType);
740 const isResponderTouchMove = responderInst && isMoveish(topLevelType);
741 const isResponderTouchEnd = responderInst && isEndish(topLevelType);
742 const incrementalTouch = isResponderTouchStart
743 ? eventTypes.responderStart
744 : isResponderTouchMove
745 ? eventTypes.responderMove
746 : isResponderTouchEnd
747 ? eventTypes.responderEnd
748 : null;
749
750 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 }
761
762 const isResponderTerminate =
763 responderInst && topLevelType === TOP_TOUCH_CANCEL;
764 const isResponderRelease =
765 responderInst &&
766 !isResponderTerminate &&
767 isEndish(topLevelType) &&
768 noResponderTouches(nativeEvent);
769 const finalTouch = isResponderTerminate
770 ? eventTypes.responderTerminate
771 : isResponderRelease
772 ? eventTypes.responderRelease
773 : 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 }
786
787 return extracted;
788 },
789
790 GlobalResponderHandler: null,
791
792 injection: {
793 /**
794 * @param {{onChange: (ReactID, ReactID) => void} GlobalResponderHandler
795 * Object that handles any change in responder. Use this to inject
796 * integration with an existing touch handling system etc.
797 */
798 injectGlobalResponderHandler(GlobalResponderHandler) {
799 ResponderEventPlugin.GlobalResponderHandler = GlobalResponderHandler;
800 },
801 },
802 };
803
804 export default ResponderEventPlugin;