@samitouri / QOS-React-2 / commits / eb2ace1281

[Flare] Bring Flare support to React Native Fabric (#15887)

Dominic Gannaway committed Jun 28, 2019 at 01:22 UTC eb2ace1281611933ff1d7bd6f757d9bb17b2f5e1
13 files changed +1680 -27
packages/react-art/src/ReactARTHostConfig.js
+3 -3
@@ -428,19 +428,19 @@ export function unhideTextInstance(textInstance, text): void {
428 }
429
430 export function mountEventComponent(
431 - eventComponentInstance: ReactEventComponentInstance,
431 + eventComponentInstance: ReactEventComponentInstance<any, any, any>,
432 ) {
433 throw new Error('Not yet implemented.');
434 }
435
436 export function updateEventComponent(
437 - eventComponentInstance: ReactEventComponentInstance,
437 + eventComponentInstance: ReactEventComponentInstance<any, any, any>,
438 ) {
439 throw new Error('Not yet implemented.');
440 }
441
442 export function unmountEventComponent(
443 - eventComponentInstance: ReactEventComponentInstance,
443 + eventComponentInstance: ReactEventComponentInstance<any, any, any>,
444 ): void {
445 throw new Error('Not yet implemented.');
446 }
packages/react-dom/src/client/ReactDOMHostConfig.js
+1 -1
@@ -32,7 +32,6 @@ import {
32 setEnabled as ReactBrowserEventEmitterSetEnabled,
33 } from '../events/ReactBrowserEventEmitter';
34 import {getChildNamespace} from '../shared/DOMNamespaces';
35 -import {addRootEventTypesForComponentInstance} from '../events/DOMEventResponderSystem';
35 import {
36 ELEMENT_NODE,
37 TEXT_NODE,
@@ -48,6 +47,7 @@ import type {
47 ReactDOMEventComponentInstance,
48 } from 'shared/ReactDOMTypes';
49 import {
50 + addRootEventTypesForComponentInstance,
51 mountEventResponder,
52 unmountEventResponder,
53 } from '../events/DOMEventResponderSystem';
packages/react-dom/src/events/DOMEventResponderSystem.js
+1 -1
@@ -231,7 +231,7 @@ const eventResponderContext: ReactDOMResponderContext = {
231 }
232 return false;
233 },
234 - isTargetWithinElement(
234 + isTargetWithinNode(
235 childTarget: Element | Document,
236 parentTarget: Element | Document,
237 ): boolean {
packages/react-dom/src/events/ReactDOMEventListener.js
+1 -1
@@ -332,7 +332,7 @@ export function dispatchEvent(
332 targetInst,
333 );
334 } else {
335 - // Responder event system (experimental event API)
335 + // React Flare event system
336 dispatchEventForResponderEventSystem(
337 topLevelType,
338 targetInst,
packages/react-events/README.md
+1 -1
@@ -121,7 +121,7 @@ Component instance.
121
122 Returns `true` if the instance has taken ownership of the responder.
123
124 -### isTargetWithinElement(target: Element, element: Element): boolean
124 +### isTargetWithinNode(target: Element, element: Element): boolean
125
126 Returns `true` if `target` is a child of `element`.
127
packages/react-events/src/dom/Press.js
+3 -3
@@ -823,7 +823,7 @@ const PressResponder: ReactDOMEventResponder = {
823 if (
824 state.pressTarget !== null &&
825 (pointerType !== 'mouse' ||
826 - !context.isTargetWithinElement(target, state.pressTarget))
826 + !context.isTargetWithinNode(target, state.pressTarget))
827 ) {
828 // Calculate the responder region we use for deactivation, as the
829 // element dimensions may have changed since activation.
@@ -936,7 +936,7 @@ const PressResponder: ReactDOMEventResponder = {
936 !isKeyboardEvent &&
937 state.pressTarget !== null &&
938 (pointerType !== 'mouse' ||
939 - !context.isTargetWithinElement(target, state.pressTarget))
939 + !context.isTargetWithinNode(target, state.pressTarget))
940 ) {
941 // If the event target isn't within the press target, check if we're still
942 // within the responder region. The region may have changed if the
@@ -993,7 +993,7 @@ const PressResponder: ReactDOMEventResponder = {
993 if (
994 pressTarget !== null &&
995 (scrollTarget === doc ||
996 - context.isTargetWithinElement(pressTarget, scrollTarget))
996 + context.isTargetWithinNode(pressTarget, scrollTarget))
997 ) {
998 dispatchCancel(event, context, props, state);
999 }
packages/react-events/src/rn/Press.js new
+683
@@ -0,0 +1,683 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 {
11 + ReactNativeResponderEvent,
12 + ReactNativeResponderContext,
13 + ReactNativeEventResponderEventType,
14 + ReactNativeEventTarget,
15 + PointerType,
16 + ReactFaricEventTouch,
17 + EventPriority,
18 +} from 'react-native-renderer/src/ReactNativeTypes';
19 +import type {ReactEventResponder} from 'shared/ReactTypes';
20 +import React from 'react';
21 +import {
22 + DiscreteEvent,
23 + UserBlockingEvent,
24 +} from 'react-native-renderer/src/ReactNativeTypes';
25 +
26 +type ReactNativeEventResponder = ReactEventResponder<
27 + ReactNativeEventResponderEventType,
28 + ReactNativeResponderEvent,
29 + ReactNativeResponderContext,
30 +>;
31 +
32 +type PressProps = {
33 + disabled: boolean,
34 + delayLongPress: number,
35 + delayPressEnd: number,
36 + delayPressStart: number,
37 + onContextMenu: (e: PressEvent) => void,
38 + onLongPress: (e: PressEvent) => void,
39 + onLongPressChange: boolean => void,
40 + onLongPressShouldCancelPress: () => boolean,
41 + onPress: (e: PressEvent) => void,
42 + onPressChange: boolean => void,
43 + onPressEnd: (e: PressEvent) => void,
44 + onPressMove: (e: PressEvent) => void,
45 + onPressStart: (e: PressEvent) => void,
46 + pressRetentionOffset: {
47 + top: number,
48 + right: number,
49 + bottom: number,
50 + left: number,
51 + },
52 +};
53 +
54 +type PressEvent = {|
55 + target: ReactNativeEventTarget,
56 + type: PressEventType,
57 + pointerType: PointerType,
58 + timeStamp: number,
59 + locationX: null | number,
60 + locationY: null | number,
61 + pageX: null | number,
62 + pageY: null | number,
63 + screenX: null | number,
64 + screenY: null | number,
65 + force: null | number,
66 +|};
67 +
68 +type PressState = {
69 + activationPosition: null | $ReadOnly<{|
70 + x: number,
71 + y: number,
72 + |}>,
73 + addedRootEvents: boolean,
74 + isActivePressed: boolean,
75 + isActivePressStart: boolean,
76 + isLongPressed: boolean,
77 + isPressed: boolean,
78 + isPressWithinResponderRegion: boolean,
79 + longPressTimeout: null | number,
80 + pointerType: PointerType,
81 + pressTarget: null | ReactNativeEventTarget,
82 + pressEndTimeout: null | number,
83 + pressStartTimeout: null | number,
84 + responderRegionOnActivation: null | $ReadOnly<{|
85 + bottom: number,
86 + left: number,
87 + right: number,
88 + top: number,
89 + |}>,
90 + responderRegionOnDeactivation: null | $ReadOnly<{|
91 + bottom: number,
92 + left: number,
93 + right: number,
94 + top: number,
95 + |}>,
96 + activePointerId: null | number,
97 + touchEvent: null | ReactFaricEventTouch,
98 +};
99 +
100 +type PressEventType =
101 + | 'press'
102 + | 'pressmove'
103 + | 'pressstart'
104 + | 'pressend'
105 + | 'presschange'
106 + | 'longpress'
107 + | 'longpresschange';
108 +
109 +const DEFAULT_PRESS_END_DELAY_MS = 0;
110 +const DEFAULT_PRESS_START_DELAY_MS = 0;
111 +const DEFAULT_LONG_PRESS_DELAY_MS = 500;
112 +const DEFAULT_PRESS_RETENTION_OFFSET = {
113 + bottom: 20,
114 + top: 20,
115 + left: 20,
116 + right: 20,
117 +};
118 +
119 +const targetEventTypes = ['topTouchStart'];
120 +const rootEventTypes = ['topTouchMove', 'topTouchEnd', 'topTouchCancel'];
121 +
122 +function calculateDelayMS(delay: ?number, min = 0, fallback = 0) {
123 + const maybeNumber = delay == null ? null : delay;
124 + return Math.max(min, maybeNumber != null ? maybeNumber : fallback);
125 +}
126 +
127 +function createPressEvent(
128 + context: ReactNativeResponderContext,
129 + type: PressEventType,
130 + target: ReactNativeEventTarget,
131 + pointerType: PointerType,
132 + touchEvent: null | ReactFaricEventTouch,
133 +): PressEvent {
134 + let timestamp = context.getTimeStamp();
135 + let locationX = null;
136 + let locationY = null;
137 + let pageX = null;
138 + let pageY = null;
139 + let screenX = null;
140 + let screenY = null;
141 + let force = null;
142 +
143 + if (touchEvent) {
144 + ({
145 + force,
146 + pageX,
147 + pageY,
148 + screenX,
149 + screenY,
150 + timestamp,
151 + locationX,
152 + locationY,
153 + } = touchEvent);
154 + }
155 + return {
156 + target,
157 + type,
158 + pointerType,
159 + timeStamp: timestamp,
160 + pageX,
161 + pageY,
162 + screenX,
163 + screenY,
164 + locationX,
165 + locationY,
166 + force,
167 + };
168 +}
169 +
170 +function dispatchEvent(
171 + event: ?ReactNativeResponderEvent,
172 + context: ReactNativeResponderContext,
173 + state: PressState,
174 + name: PressEventType,
175 + listener: (e: Object) => void,
176 + eventPriority: EventPriority,
177 +): void {
178 + const target = ((state.pressTarget: any): ReactNativeEventTarget);
179 + const pointerType = state.pointerType;
180 + const syntheticEvent = createPressEvent(
181 + context,
182 + name,
183 + target,
184 + pointerType,
185 + state.touchEvent,
186 + );
187 + context.dispatchEvent(syntheticEvent, listener, eventPriority);
188 +}
189 +
190 +function dispatchCancel(event, context, props, state): void {
191 + state.touchEvent = null;
192 + if (state.isPressed) {
193 + dispatchPressEndEvents(event, context, props, state);
194 + }
195 + removeRootEventTypes(context, state);
196 +}
197 +
198 +function addRootEventTypes(context, state): void {
199 + if (!state.addedRootEvents) {
200 + state.addedRootEvents = true;
201 + context.addRootEventTypes(rootEventTypes);
202 + }
203 +}
204 +
205 +function removeRootEventTypes(context, state): void {
206 + if (state.addedRootEvents) {
207 + state.addedRootEvents = false;
208 + context.removeRootEventTypes(rootEventTypes);
209 + }
210 +}
211 +
212 +function getTouchFromPressEvent(nativeEvent): ReactFaricEventTouch | null {
213 + const targetTouches = nativeEvent.targetTouches;
214 + if (targetTouches.length > 0) {
215 + return targetTouches[0];
216 + }
217 + return null;
218 +}
219 +
220 +function getTouchById(nativeEvent, pointerId): null | ReactFaricEventTouch {
221 + const changedTouches = nativeEvent.changedTouches;
222 + for (let i = 0; i < changedTouches.length; i++) {
223 + const touch = changedTouches[i];
224 + if (touch.identifier === pointerId) {
225 + return touch;
226 + }
227 + }
228 + return null;
229 +}
230 +
231 +function calculateResponderRegion(
232 + context: ReactNativeResponderContext,
233 + target: ReactNativeEventTarget,
234 + props: PressProps,
235 + cb: ({|
236 + bottom: number,
237 + top: number,
238 + left: number,
239 + right: number,
240 + |}) => void,
241 +) {
242 + const pressRetentionOffset = Object.assign(
243 + {},
244 + DEFAULT_PRESS_RETENTION_OFFSET,
245 + props.pressRetentionOffset,
246 + );
247 +
248 + context.getTargetBoundingRect(target, rect => {
249 + let {left, right, bottom, top} = rect;
250 +
251 + if (pressRetentionOffset) {
252 + if (pressRetentionOffset.bottom != null) {
253 + bottom += pressRetentionOffset.bottom;
254 + }
255 + if (pressRetentionOffset.left != null) {
256 + left -= pressRetentionOffset.left;
257 + }
258 + if (pressRetentionOffset.right != null) {
259 + right += pressRetentionOffset.right;
260 + }
261 + if (pressRetentionOffset.top != null) {
262 + top -= pressRetentionOffset.top;
263 + }
264 + }
265 +
266 + cb({
267 + bottom,
268 + top,
269 + left,
270 + right,
271 + });
272 + });
273 +}
274 +
275 +function dispatchPressChangeEvent(event, context, props, state): void {
276 + const bool = state.isActivePressed;
277 + const listener = () => {
278 + props.onPressChange(bool);
279 + };
280 + dispatchEvent(event, context, state, 'presschange', listener, DiscreteEvent);
281 +}
282 +
283 +function dispatchLongPressChangeEvent(event, context, props, state): void {
284 + const bool = state.isLongPressed;
285 + const listener = () => {
286 + props.onLongPressChange(bool);
287 + };
288 + dispatchEvent(
289 + event,
290 + context,
291 + state,
292 + 'longpresschange',
293 + listener,
294 + DiscreteEvent,
295 + );
296 +}
297 +
298 +function activate(event: ReactNativeResponderEvent, context, props, state) {
299 + const {pageX: x, pageY: y} = ((state.touchEvent: any): ReactFaricEventTouch);
300 + const wasActivePressed = state.isActivePressed;
301 + state.isActivePressed = true;
302 + if (x !== null && y !== null) {
303 + state.activationPosition = {x, y};
304 + }
305 +
306 + if (props.onPressStart) {
307 + dispatchEvent(
308 + event,
309 + context,
310 + state,
311 + 'pressstart',
312 + props.onPressStart,
313 + DiscreteEvent,
314 + );
315 + }
316 + if (!wasActivePressed && props.onPressChange) {
317 + dispatchPressChangeEvent(event, context, props, state);
318 + }
319 +}
320 +
321 +function deactivate(event, context, props, state) {
322 + const wasLongPressed = state.isLongPressed;
323 + state.isActivePressed = false;
324 + state.isLongPressed = false;
325 +
326 + if (props.onPressEnd) {
327 + dispatchEvent(
328 + event,
329 + context,
330 + state,
331 + 'pressend',
332 + props.onPressEnd,
333 + DiscreteEvent,
334 + );
335 + }
336 + if (props.onPressChange) {
337 + dispatchPressChangeEvent(event, context, props, state);
338 + }
339 + if (wasLongPressed && props.onLongPressChange) {
340 + dispatchLongPressChangeEvent(event, context, props, state);
341 + }
342 +}
343 +
344 +function validateRegion(touchEvent, state) {
345 + const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
346 + let left, top, right, bottom;
347 +
348 + if (responderRegionOnActivation != null) {
349 + left = responderRegionOnActivation.left;
350 + top = responderRegionOnActivation.top;
351 + right = responderRegionOnActivation.right;
352 + bottom = responderRegionOnActivation.bottom;
353 +
354 + if (responderRegionOnDeactivation != null) {
355 + left = Math.min(left, responderRegionOnDeactivation.left);
356 + top = Math.min(top, responderRegionOnDeactivation.top);
357 + right = Math.max(right, responderRegionOnDeactivation.right);
358 + bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
359 + }
360 + }
361 + const {pageX: x, pageY: y} = touchEvent;
362 +
363 + state.isPressWithinResponderRegion =
364 + left != null &&
365 + right != null &&
366 + top != null &&
367 + bottom != null &&
368 + x !== null &&
369 + y !== null &&
370 + (x >= left && x <= right && y >= top && y <= bottom);
371 +}
372 +
373 +function updateIsPressWithinResponderRegion(
374 + target,
375 + touchEvent,
376 + context,
377 + props,
378 + state,
379 + cb,
380 +): void {
381 + const pressTarget = state.pressTarget;
382 + if (pressTarget != null) {
383 + // Calculate the responder region we use for deactivation if not
384 + // already done during move event.
385 + if (state.responderRegionOnDeactivation == null) {
386 + calculateResponderRegion(context, pressTarget, props, obj => {
387 + state.responderRegionOnDeactivation = obj;
388 + validateRegion(touchEvent, state);
389 + cb();
390 + });
391 + return;
392 + } else {
393 + validateRegion(touchEvent, state);
394 + }
395 + } else {
396 + state.isPressWithinResponderRegion = true;
397 + }
398 + cb();
399 +}
400 +
401 +function dispatchPressStartEvents(event, context, props, state) {
402 + state.isPressed = true;
403 +
404 + if (state.pressEndTimeout !== null) {
405 + context.clearTimeout(state.pressEndTimeout);
406 + state.pressEndTimeout = null;
407 + }
408 +
409 + const dispatch = () => {
410 + state.isActivePressStart = true;
411 + activate(event, context, props, state);
412 +
413 + if (
414 + (props.onLongPress || props.onLongPressChange) &&
415 + !state.isLongPressed
416 + ) {
417 + const delayLongPress = calculateDelayMS(
418 + props.delayLongPress,
419 + 10,
420 + DEFAULT_LONG_PRESS_DELAY_MS,
421 + );
422 + state.longPressTimeout = context.setTimeout(() => {
423 + state.isLongPressed = true;
424 + state.longPressTimeout = null;
425 + if (props.onLongPress) {
426 + dispatchEvent(
427 + event,
428 + context,
429 + state,
430 + 'longpress',
431 + props.onLongPress,
432 + DiscreteEvent,
433 + );
434 + }
435 + if (props.onLongPressChange) {
436 + dispatchLongPressChangeEvent(event, context, props, state);
437 + }
438 + }, delayLongPress);
439 + }
440 + };
441 +
442 + if (!state.isActivePressStart) {
443 + const delayPressStart = calculateDelayMS(
444 + props.delayPressStart,
445 + 0,
446 + DEFAULT_PRESS_START_DELAY_MS,
447 + );
448 + if (delayPressStart > 0) {
449 + state.pressStartTimeout = context.setTimeout(() => {
450 + state.pressStartTimeout = null;
451 + dispatch();
452 + }, delayPressStart);
453 + } else {
454 + dispatch();
455 + }
456 + }
457 +}
458 +
459 +function dispatchPressEndEvents(event, context, props, state): void {
460 + const wasActivePressStart = state.isActivePressStart;
461 + let activationWasForced = false;
462 +
463 + state.isActivePressStart = false;
464 + state.isPressed = false;
465 +
466 + if (state.longPressTimeout !== null) {
467 + context.clearTimeout(state.longPressTimeout);
468 + state.longPressTimeout = null;
469 + }
470 +
471 + if (!wasActivePressStart && state.pressStartTimeout !== null) {
472 + context.clearTimeout(state.pressStartTimeout);
473 + state.pressStartTimeout = null;
474 + // don't activate if a press has moved beyond the responder region
475 + if (state.isPressWithinResponderRegion && event != null) {
476 + // if we haven't yet activated (due to delays), activate now
477 + activate(event, context, props, state);
478 + activationWasForced = true;
479 + }
480 + }
481 +
482 + if (state.isActivePressed) {
483 + const delayPressEnd = calculateDelayMS(
484 + props.delayPressEnd,
485 + // if activation and deactivation occur during the same event there's no
486 + // time for visual user feedback therefore a small delay is added before
487 + // deactivating.
488 + activationWasForced ? 10 : 0,
489 + DEFAULT_PRESS_END_DELAY_MS,
490 + );
491 + if (delayPressEnd > 0) {
492 + state.pressEndTimeout = context.setTimeout(() => {
493 + state.pressEndTimeout = null;
494 + deactivate(event, context, props, state);
495 + }, delayPressEnd);
496 + } else {
497 + deactivate(event, context, props, state);
498 + }
499 + }
500 +
501 + state.responderRegionOnDeactivation = null;
502 +}
503 +
504 +const PressResponder: ReactNativeEventResponder = {
505 + displayName: 'Press',
506 + targetEventTypes,
507 + allowEventHooks: true,
508 + allowMultipleHostChildren: false,
509 + createInitialState(): PressState {
510 + return {
511 + activationPosition: null,
512 + activePointerId: null,
513 + addedRootEvents: false,
514 + isActivePressed: false,
515 + isActivePressStart: false,
516 + isLongPressed: false,
517 + isPressed: false,
518 + isPressWithinResponderRegion: false,
519 + longPressTimeout: null,
520 + pointerType: '',
521 + pressEndTimeout: null,
522 + pressStartTimeout: null,
523 + pressTarget: null,
524 + responderRegionOnActivation: null,
525 + responderRegionOnDeactivation: null,
526 + touchEvent: null,
527 + };
528 + },
529 + onEvent(
530 + event: ReactNativeResponderEvent,
531 + context: ReactNativeResponderContext,
532 + props: PressProps,
533 + state: PressState,
534 + ): void {
535 + const {nativeEvent, type} = event;
536 +
537 + if (type === 'topTouchStart') {
538 + if (!state.isPressed) {
539 + state.pointerType = 'touch';
540 + const pressTarget = (state.pressTarget = context.getEventCurrentTarget(
541 + event,
542 + ));
543 +
544 + const touchEvent = getTouchFromPressEvent(nativeEvent);
545 + if (touchEvent === null) {
546 + return;
547 + }
548 + state.touchEvent = touchEvent;
549 + state.activePointerId = touchEvent.identifier;
550 + calculateResponderRegion(
551 + context,
552 + ((pressTarget: any): ReactNativeEventTarget),
553 + props,
554 + obj => {
555 + state.responderRegionOnActivation = obj;
556 + },
557 + );
558 + state.responderRegionOnDeactivation = null;
559 + state.isPressWithinResponderRegion = true;
560 +
561 + dispatchPressStartEvents(event, context, props, state);
562 + addRootEventTypes(context, state);
563 + }
564 + }
565 + },
566 + onRootEvent(
567 + event: ReactNativeResponderEvent,
568 + context: ReactNativeResponderContext,
569 + props: PressProps,
570 + state: PressState,
571 + ): void {
572 + const {nativeEvent, target, type} = event;
573 + const activePointerId = state.activePointerId;
574 +
575 + switch (type) {
576 + case 'topTouchMove': {
577 + const touchEvent = getTouchById(nativeEvent, activePointerId);
578 + if (touchEvent === null) {
579 + return;
580 + }
581 + state.touchEvent = touchEvent;
582 +
583 + updateIsPressWithinResponderRegion(
584 + target,
585 + touchEvent,
586 + context,
587 + props,
588 + state,
589 + () => {
590 + if (state.isPressWithinResponderRegion) {
591 + if (state.isPressed) {
592 + if (props.onPressMove) {
593 + dispatchEvent(
594 + event,
595 + context,
596 + state,
597 + 'pressmove',
598 + props.onPressMove,
599 + UserBlockingEvent,
600 + );
601 + }
602 + if (
603 + state.activationPosition != null &&
604 + state.longPressTimeout != null
605 + ) {
606 + const deltaX = state.activationPosition.x - touchEvent.pageX;
607 + const deltaY = state.activationPosition.y - touchEvent.pageY;
608 + if (
609 + Math.hypot(deltaX, deltaY) > 10 &&
610 + state.longPressTimeout != null
611 + ) {
612 + context.clearTimeout(state.longPressTimeout);
613 + }
614 + }
615 + } else {
616 + dispatchPressStartEvents(event, context, props, state);
617 + }
618 + } else {
619 + dispatchPressEndEvents(event, context, props, state);
620 + }
621 + },
622 + );
623 + break;
624 + }
625 + case 'topTouchEnd': {
626 + if (state.isPressed) {
627 + const touchEvent = getTouchById(nativeEvent, activePointerId);
628 + if (touchEvent === null) {
629 + return;
630 + }
631 + state.touchEvent = touchEvent;
632 + const wasLongPressed = state.isLongPressed;
633 + dispatchPressEndEvents(event, context, props, state);
634 +
635 + if (state.pressTarget !== null && props.onPress) {
636 + // If the event target isn't within the press target, check if we're still
637 + // within the responder region. The region may have changed if the
638 + // element's layout was modified after activation.
639 + updateIsPressWithinResponderRegion(
640 + target,
641 + touchEvent,
642 + context,
643 + props,
644 + state,
645 + () => {
646 + if (state.isPressWithinResponderRegion) {
647 + if (
648 + !(
649 + wasLongPressed &&
650 + props.onLongPressShouldCancelPress &&
651 + props.onLongPressShouldCancelPress()
652 + )
653 + ) {
654 + dispatchEvent(
655 + event,
656 + context,
657 + state,
658 + 'press',
659 + props.onPress,
660 + DiscreteEvent,
661 + );
662 + }
663 + }
664 + },
665 + );
666 + }
667 + }
668 + state.touchEvent = null;
669 + removeRootEventTypes(context, state);
670 + break;
671 + }
672 + case 'topTouchCancel': {
673 + dispatchCancel(event, context, props, state);
674 + }
675 + }
676 + },
677 +};
678 +
679 +export const Press = React.unstable_createEvent(PressResponder);
680 +
681 +export function usePress(props: PressProps): void {
682 + React.unstable_useEvent(Press, props);
683 +}
packages/react-native-renderer/src/ReactFabricEventEmitter.js
+11
@@ -17,7 +17,9 @@ import {registrationNameModules} from 'events/EventPluginRegistry';
17 import {batchedUpdates} from 'events/ReactGenericBatching';
18
19 import type {AnyNativeEvent} from 'events/PluginModuleType';
20 +import {enableFlareAPI} from 'shared/ReactFeatureFlags';
21 import type {TopLevelType} from 'events/TopLevelEventTypes';
22 +import {dispatchEventForResponderEventSystem} from './ReactFabricEventResponderSystem';
23
24 export {getListener, registrationNameModules as registrationNames};
25
@@ -27,7 +29,16 @@ export function dispatchEvent(
29 nativeEvent: AnyNativeEvent,
30 ) {
31 const targetFiber = (target: null | Fiber);
32 + if (enableFlareAPI) {
33 + // React Flare event system
34 + dispatchEventForResponderEventSystem(
35 + (topLevelType: any),
36 + target,
37 + (nativeEvent: any),
38 + );
39 + }
40 batchedUpdates(function() {
41 + // Heritage plugin event system
42 runExtractedPluginEventsInBatch(
43 topLevelType,
44 targetFiber,
packages/react-native-renderer/src/ReactFabricEventResponderSystem.js new
+835
@@ -0,0 +1,835 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 {EventComponent, HostComponent} from 'shared/ReactWorkTags';
11 +import type {Fiber} from 'react-reconciler/src/ReactFiber';
12 +import {
13 + batchedEventUpdates,
14 + discreteUpdates,
15 + flushDiscreteUpdatesIfNeeded,
16 +} from 'events/ReactGenericBatching';
17 +import type {
18 + ReactEventResponder,
19 + ReactEventComponentInstance,
20 +} from 'shared/ReactTypes';
21 +import type {
22 + ReactNativeEventResponderEventType,
23 + ReactNativeResponderContext,
24 + ReactNativeResponderEvent,
25 + EventPriority,
26 + ReactNativeEventTarget,
27 + ReactFaricEvent,
28 +} from './ReactNativeTypes';
29 +import {
30 + ContinuousEvent,
31 + UserBlockingEvent,
32 + DiscreteEvent,
33 +} from './ReactNativeTypes';
34 +import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
35 +import {enableUserBlockingEvents} from 'shared/ReactFeatureFlags';
36 +import warning from 'shared/warning';
37 +import invariant from 'shared/invariant';
38 +
39 +// Intentionally not named imports because Rollup would use dynamic dispatch for
40 +// CommonJS interop named imports.
41 +import * as Scheduler from 'scheduler';
42 +const {
43 + unstable_UserBlockingPriority: UserBlockingPriority,
44 + unstable_runWithPriority: runWithPriority,
45 +} = Scheduler;
46 +
47 +type EventObjectType = $Shape<PartialEventObject>;
48 +
49 +type PartialEventObject = {
50 + target: ReactNativeEventTarget,
51 + type: string,
52 +};
53 +
54 +type ResponderTimeout = {|
55 + id: TimeoutID,
56 + timers: Map<number, ResponderTimer>,
57 +|};
58 +
59 +type ResponderTimer = {|
60 + isHook: boolean,
61 + instance: ReactNativeEventComponentInstance,
62 + func: () => void,
63 + id: number,
64 + timeStamp: number,
65 +|};
66 +
67 +type EventQueue = {
68 + events: Array<EventObjectType>,
69 + eventPriority: EventPriority,
70 +};
71 +
72 +type ReactNativeEventResponder = ReactEventResponder<
73 + ReactNativeEventResponderEventType,
74 + ReactNativeResponderEvent,
75 + ReactNativeResponderContext,
76 +>;
77 +
78 +type ReactNativeEventComponentInstance = ReactEventComponentInstance<
79 + ReactNativeEventResponderEventType,
80 + ReactNativeResponderEvent,
81 + ReactNativeResponderContext,
82 +>;
83 +
84 +const {measureInWindow} = nativeFabricUIManager;
85 +
86 +const activeTimeouts: Map<number, ResponderTimeout> = new Map();
87 +const rootEventTypesToEventComponentInstances: Map<
88 + ReactNativeEventResponderEventType | string,
89 + Set<ReactNativeEventComponentInstance>,
90 +> = new Map();
91 +const targetEventTypeCached: Map<
92 + Array<ReactNativeEventResponderEventType>,
93 + Set<ReactNativeEventResponderEventType>,
94 +> = new Map();
95 +const ownershipChangeListeners: Set<
96 + ReactNativeEventComponentInstance,
97 +> = new Set();
98 +const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
99 +const eventListeners:
100 + | WeakMap
101 + | Map<
102 + $Shape<PartialEventObject>,
103 + ($Shape<PartialEventObject>) => void,
104 + > = new PossiblyWeakMap();
105 +
106 +let globalOwner = null;
107 +let continueLocalPropagation = false;
108 +
109 +let currentTimeStamp = 0;
110 +let currentTimers = new Map();
111 +let currentInstance: null | ReactNativeEventComponentInstance = null;
112 +let currentEventQueue: null | EventQueue = null;
113 +let currentTimerIDCounter = 0;
114 +let currentlyInHook = false;
115 +
116 +const eventResponderContext: ReactNativeResponderContext = {
117 + dispatchEvent(
118 + possibleEventObject: Object,
119 + listener: ($Shape<PartialEventObject>) => void,
120 + eventPriority: EventPriority,
121 + ): void {
122 + validateResponderContext();
123 + const {target, type, timeStamp} = possibleEventObject;
124 +
125 + if (target == null || type == null || timeStamp == null) {
126 + throw new Error(
127 + 'context.dispatchEvent: "target", "timeStamp", and "type" fields on event object are required.',
128 + );
129 + }
130 + const showWarning = name => {
131 + if (__DEV__) {
132 + warning(
133 + false,
134 + '%s is not available on event objects created from event responder modules (React Flare). ' +
135 + 'Try wrapping in a conditional, i.e. `if (event.type !== "press") { event.%s }`',
136 + name,
137 + name,
138 + );
139 + }
140 + };
141 + possibleEventObject.preventDefault = () => {
142 + if (__DEV__) {
143 + showWarning('preventDefault()');
144 + }
145 + };
146 + possibleEventObject.stopPropagation = () => {
147 + if (__DEV__) {
148 + showWarning('stopPropagation()');
149 + }
150 + };
151 + possibleEventObject.isDefaultPrevented = () => {
152 + if (__DEV__) {
153 + showWarning('isDefaultPrevented()');
154 + }
155 + };
156 + possibleEventObject.isPropagationStopped = () => {
157 + if (__DEV__) {
158 + showWarning('isPropagationStopped()');
159 + }
160 + };
161 + // $FlowFixMe: we don't need value, Flow thinks we do
162 + Object.defineProperty(possibleEventObject, 'nativeEvent', {
163 + get() {
164 + if (__DEV__) {
165 + showWarning('nativeEvent');
166 + }
167 + },
168 + });
169 +
170 + const eventObject = ((possibleEventObject: any): $Shape<
171 + PartialEventObject,
172 + >);
173 + const eventQueue = ((currentEventQueue: any): EventQueue);
174 + eventQueue.eventPriority = eventPriority;
175 + eventListeners.set(eventObject, listener);
176 + eventQueue.events.push(eventObject);
177 + },
178 + isTargetWithinNode(
179 + childTarget: ReactNativeEventTarget,
180 + parentTarget: ReactNativeEventTarget,
181 + ) {
182 + validateResponderContext();
183 + const childFiber = getFiberFromTarget(childTarget);
184 + const parentFiber = getFiberFromTarget(parentTarget);
185 +
186 + let node = childFiber;
187 + while (node !== null) {
188 + if (node === parentFiber) {
189 + return true;
190 + }
191 + node = node.return;
192 + }
193 + return false;
194 + },
195 + getEventCurrentTarget(
196 + event: ReactNativeResponderEvent,
197 + ): ReactNativeEventTarget {
198 + validateResponderContext();
199 + const target = event.target;
200 + let fiber = getFiberFromTarget(target);
201 + let hostComponent = target;
202 + const currentResponder = ((currentInstance: any): ReactNativeEventComponentInstance)
203 + .responder;
204 +
205 + while (fiber !== null) {
206 + const stateNode = fiber.stateNode;
207 + if (
208 + fiber.tag === EventComponent &&
209 + (stateNode === null || stateNode.responder === currentResponder)
210 + ) {
211 + break;
212 + }
213 + if (fiber.tag === HostComponent) {
214 + hostComponent = fiber.stateNode;
215 + }
216 + fiber = fiber.return;
217 + }
218 + return ((hostComponent: any): ReactNativeEventTarget);
219 + },
220 + getTargetBoundingRect(
221 + target: ReactNativeEventTarget,
222 + callback: ({
223 + left: number,
224 + right: number,
225 + top: number,
226 + bottom: number,
227 + }) => void,
228 + ) {
229 + measureInWindow(target.node, (x, y, width, height) => {
230 + callback({
231 + left: x,
232 + right: x + width,
233 + top: y,
234 + bottom: y + height,
235 + });
236 + });
237 + },
238 + addRootEventTypes(
239 + rootEventTypes: Array<ReactNativeEventResponderEventType>,
240 + ): void {
241 + validateResponderContext();
242 + for (let i = 0; i < rootEventTypes.length; i++) {
243 + const rootEventType = rootEventTypes[i];
244 + const eventComponentInstance = ((currentInstance: any): ReactNativeEventComponentInstance);
245 + registerRootEventType(rootEventType, eventComponentInstance);
246 + }
247 + },
248 + removeRootEventTypes(
249 + rootEventTypes: Array<ReactNativeEventResponderEventType>,
250 + ): void {
251 + validateResponderContext();
252 + for (let i = 0; i < rootEventTypes.length; i++) {
253 + const rootEventType = rootEventTypes[i];
254 +
255 + let rootEventComponents = rootEventTypesToEventComponentInstances.get(
256 + rootEventType,
257 + );
258 + let rootEventTypesSet = ((currentInstance: any): ReactNativeEventComponentInstance)
259 + .rootEventTypes;
260 + if (rootEventTypesSet !== null) {
261 + rootEventTypesSet.delete(rootEventType);
262 + }
263 + if (rootEventComponents !== undefined) {
264 + rootEventComponents.delete(
265 + ((currentInstance: any): ReactNativeEventComponentInstance),
266 + );
267 + }
268 + }
269 + },
270 + setTimeout(func: () => void, delay): number {
271 + validateResponderContext();
272 + if (currentTimers === null) {
273 + currentTimers = new Map();
274 + }
275 + let timeout = currentTimers.get(delay);
276 +
277 + const timerId = currentTimerIDCounter++;
278 + if (timeout === undefined) {
279 + const timers = new Map();
280 + const id = setTimeout(() => {
281 + processTimers(timers, delay);
282 + }, delay);
283 + timeout = {
284 + id,
285 + timers,
286 + };
287 + currentTimers.set(delay, timeout);
288 + }
289 + timeout.timers.set(timerId, {
290 + isHook: currentlyInHook,
291 + instance: ((currentInstance: any): ReactNativeEventComponentInstance),
292 + func,
293 + id: timerId,
294 + timeStamp: currentTimeStamp,
295 + });
296 + activeTimeouts.set(timerId, timeout);
297 + return timerId;
298 + },
299 + clearTimeout(timerId: number): void {
300 + validateResponderContext();
301 + const timeout = activeTimeouts.get(timerId);
302 +
303 + if (timeout !== undefined) {
304 + const timers = timeout.timers;
305 + timers.delete(timerId);
306 + if (timers.size === 0) {
307 + clearTimeout(timeout.id);
308 + }
309 + }
310 + },
311 + getTimeStamp(): number {
312 + validateResponderContext();
313 + return currentTimeStamp;
314 + },
315 +};
316 +
317 +function getFiberFromTarget(
318 + target: null | ReactNativeEventTarget,
319 +): Fiber | null {
320 + if (target === null) {
321 + return null;
322 + }
323 + return ((target.canonical._internalInstanceHandle: any): Fiber);
324 +}
325 +
326 +function processTimers(
327 + timers: Map<number, ResponderTimer>,
328 + delay: number,
329 +): void {
330 + const timersArr = Array.from(timers.values());
331 + currentEventQueue = createEventQueue();
332 + try {
333 + for (let i = 0; i < timersArr.length; i++) {
334 + const {isHook, instance, func, id, timeStamp} = timersArr[i];
335 + currentInstance = instance;
336 + currentTimeStamp = timeStamp + delay;
337 + currentlyInHook = isHook;
338 + try {
339 + func();
340 + } finally {
341 + activeTimeouts.delete(id);
342 + }
343 + }
344 + processEventQueue();
345 + } finally {
346 + currentTimers = null;
347 + currentInstance = null;
348 + currentEventQueue = null;
349 + currentTimeStamp = 0;
350 + }
351 +}
352 +
353 +function createFabricResponderEvent(
354 + topLevelType: ReactNativeEventResponderEventType,
355 + nativeEvent: ReactFaricEvent,
356 + target: null | ReactNativeEventTarget,
357 +): ReactNativeResponderEvent {
358 + const responderEvent = {
359 + nativeEvent,
360 + target,
361 + type: topLevelType,
362 + };
363 + if (__DEV__) {
364 + Object.freeze(responderEvent);
365 + }
366 + return responderEvent;
367 +}
368 +
369 +function validateResponderContext(): void {
370 + invariant(
371 + currentEventQueue && currentInstance,
372 + 'An event responder context was used outside of an event cycle. ' +
373 + 'Use context.setTimeout() to use asynchronous responder context outside of event cycle .',
374 + );
375 +}
376 +
377 +// TODO this function is almost an exact copy of the DOM version, we should
378 +// somehow share the logic
379 +function createEventQueue(): EventQueue {
380 + return {
381 + events: [],
382 + eventPriority: ContinuousEvent,
383 + };
384 +}
385 +
386 +// TODO this function is almost an exact copy of the DOM version, we should
387 +// somehow share the logic
388 +function processEventQueue(): void {
389 + const {events, eventPriority} = ((currentEventQueue: any): EventQueue);
390 +
391 + if (events.length === 0) {
392 + return;
393 + }
394 +
395 + switch (eventPriority) {
396 + case DiscreteEvent: {
397 + flushDiscreteUpdatesIfNeeded(currentTimeStamp);
398 + discreteUpdates(() => {
399 + batchedEventUpdates(processEvents, events);
400 + });
401 + break;
402 + }
403 + case UserBlockingEvent: {
404 + if (enableUserBlockingEvents) {
405 + runWithPriority(
406 + UserBlockingPriority,
407 + batchedEventUpdates.bind(null, processEvents, events),
408 + );
409 + } else {
410 + batchedEventUpdates(processEvents, events);
411 + }
412 + break;
413 + }
414 + case ContinuousEvent: {
415 + batchedEventUpdates(processEvents, events);
416 + break;
417 + }
418 + }
419 +}
420 +
421 +// TODO this function is almost an exact copy of the DOM version, we should
422 +// somehow share the logic
423 +function triggerOwnershipListeners(): void {
424 + const listeningInstances = Array.from(ownershipChangeListeners);
425 + const previousInstance = currentInstance;
426 + const previouslyInHook = currentlyInHook;
427 + currentEventQueue = createEventQueue();
428 + try {
429 + for (let i = 0; i < listeningInstances.length; i++) {
430 + const instance = listeningInstances[i];
431 + const {isHook, props, responder, state} = instance;
432 + currentInstance = instance;
433 + currentlyInHook = isHook;
434 + const onOwnershipChange = ((responder: any): ReactNativeEventResponder)
435 + .onOwnershipChange;
436 + if (onOwnershipChange !== undefined) {
437 + onOwnershipChange(eventResponderContext, props, state);
438 + }
439 + }
440 + processEventQueue();
441 + } finally {
442 + currentInstance = previousInstance;
443 + currentlyInHook = previouslyInHook;
444 + }
445 +}
446 +
447 +// TODO this function is almost an exact copy of the DOM version, we should
448 +// somehow share the logic
449 +function releaseOwnershipForEventComponentInstance(
450 + eventComponentInstance: ReactNativeEventComponentInstance,
451 +): boolean {
452 + if (globalOwner === eventComponentInstance) {
453 + globalOwner = null;
454 + triggerOwnershipListeners();
455 + return true;
456 + }
457 + return false;
458 +}
459 +
460 +// TODO this function is almost an exact copy of the DOM version, we should
461 +// somehow share the logic
462 +function processEvent(event: $Shape<PartialEventObject>): void {
463 + const type = event.type;
464 + const listener = ((eventListeners.get(event): any): (
465 + $Shape<PartialEventObject>,
466 + ) => void);
467 + invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
468 +}
469 +
470 +// TODO this function is almost an exact copy of the DOM version, we should
471 +// somehow share the logic
472 +function processEvents(events: Array<EventObjectType>): void {
473 + for (let i = 0, length = events.length; i < length; i++) {
474 + processEvent(events[i]);
475 + }
476 +}
477 +
478 +function getFabricTargetEventTypesSet(
479 + eventTypes: Array<ReactNativeEventResponderEventType>,
480 +): Set<ReactNativeEventResponderEventType> {
481 + let cachedSet = targetEventTypeCached.get(eventTypes);
482 +
483 + if (cachedSet === undefined) {
484 + cachedSet = new Set();
485 + for (let i = 0; i < eventTypes.length; i++) {
486 + cachedSet.add(eventTypes[i]);
487 + }
488 + targetEventTypeCached.set(eventTypes, cachedSet);
489 + }
490 + return cachedSet;
491 +}
492 +
493 +// TODO this function is almost an exact copy of the DOM version, we should
494 +// somehow share the logic
495 +function getTargetEventResponderInstances(
496 + topLevelType: ReactNativeEventResponderEventType,
497 + targetFiber: null | Fiber,
498 +): Array<ReactNativeEventComponentInstance> {
499 + const eventResponderInstances = [];
500 + let node = targetFiber;
501 + while (node !== null) {
502 + // Traverse up the fiber tree till we find event component fibers.
503 + if (node.tag === EventComponent) {
504 + const eventComponentInstance = node.stateNode;
505 + const responder = eventComponentInstance.responder;
506 + const targetEventTypes = responder.targetEventTypes;
507 + // Validate the target event type exists on the responder
508 + if (targetEventTypes !== undefined) {
509 + const targetEventTypesSet = getFabricTargetEventTypesSet(
510 + targetEventTypes,
511 + );
512 + if (targetEventTypesSet.has(topLevelType)) {
513 + eventResponderInstances.push(eventComponentInstance);
514 + }
515 + }
516 + }
517 + node = node.return;
518 + }
519 + return eventResponderInstances;
520 +}
521 +
522 +// TODO this function is almost an exact copy of the DOM version, we should
523 +// somehow share the logic
524 +function shouldSkipEventComponent(
525 + eventResponderInstance: ReactNativeEventComponentInstance,
526 + responder: ReactNativeEventResponder,
527 + propagatedEventResponders: null | Set<ReactNativeEventResponder>,
528 + isHook: boolean,
529 +): boolean {
530 + if (propagatedEventResponders !== null && !isHook) {
531 + if (propagatedEventResponders.has(responder)) {
532 + return true;
533 + }
534 + propagatedEventResponders.add(responder);
535 + }
536 + if (globalOwner && globalOwner !== eventResponderInstance) {
537 + return true;
538 + }
539 + return false;
540 +}
541 +
542 +function checkForLocalPropagationContinuation(
543 + responder: ReactNativeEventResponder,
544 + propagatedEventResponders: Set<ReactNativeEventResponder>,
545 +): void {
546 + if (continueLocalPropagation === true) {
547 + propagatedEventResponders.delete(responder);
548 + continueLocalPropagation = false;
549 + }
550 +}
551 +
552 +// TODO this function is almost an exact copy of the DOM version, we should
553 +// somehow share the logic
554 +function getRootEventResponderInstances(
555 + topLevelType: string,
556 +): Array<ReactNativeEventComponentInstance> {
557 + const eventResponderInstances = [];
558 + const rootEventInstances = rootEventTypesToEventComponentInstances.get(
559 + topLevelType,
560 + );
561 + if (rootEventInstances !== undefined) {
562 + const rootEventComponentInstances = Array.from(rootEventInstances);
563 +
564 + for (let i = 0; i < rootEventComponentInstances.length; i++) {
565 + const rootEventComponentInstance = rootEventComponentInstances[i];
566 + eventResponderInstances.push(rootEventComponentInstance);
567 + }
568 + }
569 + return eventResponderInstances;
570 +}
571 +
572 +// TODO this function is almost an exact copy of the DOM version, we should
573 +// somehow share the logic
574 +function traverseAndHandleEventResponderInstances(
575 + topLevelType: ReactNativeEventResponderEventType,
576 + targetFiber: null | Fiber,
577 + nativeEvent: ReactFaricEvent,
578 +): void {
579 + // Trigger event responders in this order:
580 + // - Capture target phase
581 + // - Bubble target phase
582 + // - Root phase
583 +
584 + const targetEventResponderInstances = getTargetEventResponderInstances(
585 + topLevelType,
586 + targetFiber,
587 + );
588 + const responderEvent = createFabricResponderEvent(
589 + topLevelType,
590 + nativeEvent,
591 + targetFiber !== null
592 + ? ((targetFiber.stateNode: any): ReactNativeEventTarget)
593 + : null,
594 + );
595 + const propagatedEventResponders: Set<ReactNativeEventResponder> = new Set();
596 + let length = targetEventResponderInstances.length;
597 + let i;
598 +
599 + // Captured and bubbled event phases have the notion of local propagation.
600 + // This means that the propgation chain can be stopped part of the the way
601 + // through processing event component instances. The major difference to other
602 + // events systems is that the stopping of propgation is localized to a single
603 + // phase, rather than both phases.
604 + if (length > 0) {
605 + // Capture target phase
606 + for (i = length; i-- > 0; ) {
607 + const targetEventResponderInstance = targetEventResponderInstances[i];
608 + const {isHook, responder, props, state} = targetEventResponderInstance;
609 + const eventListener = ((responder: any): ReactNativeEventResponder)
610 + .onEventCapture;
611 + if (eventListener !== undefined) {
612 + if (
613 + shouldSkipEventComponent(
614 + targetEventResponderInstance,
615 + ((responder: any): ReactNativeEventResponder),
616 + propagatedEventResponders,
617 + isHook,
618 + )
619 + ) {
620 + continue;
621 + }
622 + currentInstance = targetEventResponderInstance;
623 + currentlyInHook = isHook;
624 + eventListener(responderEvent, eventResponderContext, props, state);
625 + if (!isHook) {
626 + checkForLocalPropagationContinuation(
627 + ((responder: any): ReactNativeEventResponder),
628 + propagatedEventResponders,
629 + );
630 + }
631 + }
632 + }
633 + // We clean propagated event responders between phases.
634 + propagatedEventResponders.clear();
635 + // Bubble target phase
636 + for (i = 0; i < length; i++) {
637 + const targetEventResponderInstance = targetEventResponderInstances[i];
638 + const {isHook, responder, props, state} = targetEventResponderInstance;
639 + const eventListener = ((responder: any): ReactNativeEventResponder)
640 + .onEvent;
641 + if (eventListener !== undefined) {
642 + if (
643 + shouldSkipEventComponent(
644 + targetEventResponderInstance,
645 + ((responder: any): ReactNativeEventResponder),
646 + propagatedEventResponders,
647 + isHook,
648 + )
649 + ) {
650 + continue;
651 + }
652 + currentInstance = targetEventResponderInstance;
653 + currentlyInHook = isHook;
654 + eventListener(responderEvent, eventResponderContext, props, state);
655 + if (!isHook) {
656 + checkForLocalPropagationContinuation(
657 + ((responder: any): ReactNativeEventResponder),
658 + propagatedEventResponders,
659 + );
660 + }
661 + }
662 + }
663 + }
664 + // Root phase
665 + const rootEventResponderInstances = getRootEventResponderInstances(
666 + topLevelType,
667 + );
668 + length = rootEventResponderInstances.length;
669 + if (length > 0) {
670 + for (i = 0; i < length; i++) {
671 + const rootEventResponderInstance = rootEventResponderInstances[i];
672 + const {isHook, props, responder, state} = rootEventResponderInstance;
673 + const eventListener = responder.onRootEvent;
674 + if (eventListener !== undefined) {
675 + if (
676 + shouldSkipEventComponent(
677 + rootEventResponderInstance,
678 + responder,
679 + null,
680 + isHook,
681 + )
682 + ) {
683 + continue;
684 + }
685 + currentInstance = rootEventResponderInstance;
686 + currentlyInHook = isHook;
687 + eventListener(responderEvent, eventResponderContext, props, state);
688 + }
689 + }
690 + }
691 +}
692 +
693 +// TODO this function is almost an exact copy of the DOM version, we should
694 +// somehow share the logic
695 +export function dispatchEventForResponderEventSystem(
696 + topLevelType: ReactNativeEventResponderEventType,
697 + targetFiber: null | Fiber,
698 + nativeEvent: ReactFaricEvent,
699 +): void {
700 + const previousEventQueue = currentEventQueue;
701 + const previousInstance = currentInstance;
702 + const previousTimers = currentTimers;
703 + const previousTimeStamp = currentTimeStamp;
704 + const previouslyInHook = currentlyInHook;
705 + currentTimers = null;
706 + currentEventQueue = createEventQueue();
707 + // We might want to control timeStamp another way here
708 + currentTimeStamp = Date.now();
709 + try {
710 + traverseAndHandleEventResponderInstances(
711 + topLevelType,
712 + targetFiber,
713 + nativeEvent,
714 + );
715 + processEventQueue();
716 + } finally {
717 + currentTimers = previousTimers;
718 + currentInstance = previousInstance;
719 + currentEventQueue = previousEventQueue;
720 + currentTimeStamp = previousTimeStamp;
721 + currentlyInHook = previouslyInHook;
722 + }
723 +}
724 +
725 +// TODO this function is almost an exact copy of the DOM version, we should
726 +// somehow share the logic
727 +export function mountEventResponder(
728 + eventComponentInstance: ReactNativeEventComponentInstance,
729 +) {
730 + const responder = ((eventComponentInstance.responder: any): ReactNativeEventResponder);
731 + if (responder.onOwnershipChange !== undefined) {
732 + ownershipChangeListeners.add(eventComponentInstance);
733 + }
734 + const onMount = responder.onMount;
735 + if (onMount !== undefined) {
736 + let {isHook, props, state} = eventComponentInstance;
737 + currentEventQueue = createEventQueue();
738 + currentInstance = eventComponentInstance;
739 + currentlyInHook = isHook;
740 + try {
741 + onMount(eventResponderContext, props, state);
742 + processEventQueue();
743 + } finally {
744 + currentEventQueue = null;
745 + currentInstance = null;
746 + currentTimers = null;
747 + }
748 + }
749 +}
750 +
751 +// TODO this function is almost an exact copy of the DOM version, we should
752 +// somehow share the logic
753 +export function unmountEventResponder(
754 + eventComponentInstance: ReactNativeEventComponentInstance,
755 +): void {
756 + const responder = ((eventComponentInstance.responder: any): ReactNativeEventResponder);
757 + const onUnmount = responder.onUnmount;
758 + if (onUnmount !== undefined) {
759 + let {isHook, props, state} = eventComponentInstance;
760 + currentEventQueue = createEventQueue();
761 + currentInstance = eventComponentInstance;
762 + currentlyInHook = isHook;
763 + try {
764 + onUnmount(eventResponderContext, props, state);
765 + processEventQueue();
766 + } finally {
767 + currentEventQueue = null;
768 + currentInstance = null;
769 + currentTimers = null;
770 + }
771 + }
772 + try {
773 + currentEventQueue = createEventQueue();
774 + releaseOwnershipForEventComponentInstance(eventComponentInstance);
775 + processEventQueue();
776 + } finally {
777 + currentEventQueue = null;
778 + }
779 + if (responder.onOwnershipChange !== undefined) {
780 + ownershipChangeListeners.delete(eventComponentInstance);
781 + }
782 + const rootEventTypesSet = eventComponentInstance.rootEventTypes;
783 + if (rootEventTypesSet !== null) {
784 + const rootEventTypes = Array.from(rootEventTypesSet);
785 +
786 + for (let i = 0; i < rootEventTypes.length; i++) {
787 + const topLevelEventType = rootEventTypes[i];
788 + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
789 + topLevelEventType,
790 + );
791 + if (rootEventComponentInstances !== undefined) {
792 + rootEventComponentInstances.delete(eventComponentInstance);
793 + }
794 + }
795 + }
796 +}
797 +
798 +function registerRootEventType(
799 + rootEventType: ReactNativeEventResponderEventType,
800 + eventComponentInstance: ReactNativeEventComponentInstance,
801 +) {
802 + let rootEventComponentInstances = rootEventTypesToEventComponentInstances.get(
803 + rootEventType,
804 + );
805 + if (rootEventComponentInstances === undefined) {
806 + rootEventComponentInstances = new Set();
807 + rootEventTypesToEventComponentInstances.set(
808 + rootEventType,
809 + rootEventComponentInstances,
810 + );
811 + }
812 + let rootEventTypesSet = eventComponentInstance.rootEventTypes;
813 + if (rootEventTypesSet === null) {
814 + rootEventTypesSet = eventComponentInstance.rootEventTypes = new Set();
815 + }
816 + invariant(
817 + !rootEventTypesSet.has(rootEventType),
818 + 'addRootEventTypes() found a duplicate root event ' +
819 + 'type of "%s". This might be because the event type exists in the event responder "rootEventTypes" ' +
820 + 'array or because of a previous addRootEventTypes() using this root event type.',
821 + rootEventType,
822 + );
823 + rootEventTypesSet.add(rootEventType);
824 + rootEventComponentInstances.add(eventComponentInstance);
825 +}
826 +
827 +export function addRootEventTypesForComponentInstance(
828 + eventComponentInstance: ReactNativeEventComponentInstance,
829 + rootEventTypes: Array<ReactNativeEventResponderEventType>,
830 +): void {
831 + for (let i = 0; i < rootEventTypes.length; i++) {
832 + const rootEventType = rootEventTypes[i];
833 + registerRootEventType(rootEventType, eventComponentInstance);
834 + }
835 +}
packages/react-native-renderer/src/ReactFabricHostConfig.js
+35 -8
@@ -13,7 +13,9 @@ import type {
13 MeasureOnSuccessCallback,
14 NativeMethodsMixinType,
15 ReactNativeBaseComponentViewConfig,
16 - ReactNativeEventResponder,
16 + ReactNativeEventResponderEventType,
17 + ReactNativeResponderEvent,
18 + ReactNativeResponderContext,
19 } from './ReactNativeTypes';
20 import type {ReactEventComponentInstance} from 'shared/ReactTypes';
21
@@ -24,6 +26,13 @@ import invariant from 'shared/invariant';
26 import warningWithoutStack from 'shared/warningWithoutStack';
27
28 import {dispatchEvent} from './ReactFabricEventEmitter';
29 +import {
30 + addRootEventTypesForComponentInstance,
31 + mountEventResponder,
32 + unmountEventResponder,
33 +} from './ReactFabricEventResponderSystem';
34 +
35 +import {enableFlareAPI} from 'shared/ReactFeatureFlags';
36
37 // Modules provided by RN:
38 import {
@@ -56,6 +65,12 @@ const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
65 // This means that they never overlap.
66 let nextReactTag = 2;
67
68 +type ReactNativeEventComponentInstance = ReactEventComponentInstance<
69 + ReactNativeEventResponderEventType,
70 + ReactNativeResponderEvent,
71 + ReactNativeResponderContext,
72 +>;
73 +
74 type Node = Object;
75 export type Type = string;
76 export type Props = Object;
@@ -77,7 +92,6 @@ export type UpdatePayload = Object;
92
93 export type TimeoutHandle = TimeoutID;
94 export type NoTimeout = -1;
80 -export type EventResponder = ReactNativeEventResponder;
95
96 // TODO: Remove this conditional once all changes have propagated.
97 if (registerEventHandler) {
@@ -431,21 +445,34 @@ export function replaceContainerChildren(
445 ): void {}
446
447 export function mountEventComponent(
434 - eventComponentInstance: ReactEventComponentInstance<any, any, any>,
448 + eventComponentInstance: ReactNativeEventComponentInstance,
449 ) {
436 - throw new Error('Not yet implemented.');
450 + if (enableFlareAPI) {
451 + const responder = eventComponentInstance.responder;
452 + const {rootEventTypes} = responder;
453 + if (rootEventTypes !== undefined) {
454 + addRootEventTypesForComponentInstance(
455 + eventComponentInstance,
456 + rootEventTypes,
457 + );
458 + }
459 + mountEventResponder(eventComponentInstance);
460 + }
461 }
462
463 export function updateEventComponent(
440 - eventComponentInstance: ReactEventComponentInstance<any, any, any>,
464 + eventComponentInstance: ReactNativeEventComponentInstance,
465 ) {
442 - throw new Error('Not yet implemented.');
466 + // NO-OP, why might use this in the future
467 }
468
469 export function unmountEventComponent(
446 - eventComponentInstance: ReactEventComponentInstance<any, any, any>,
470 + eventComponentInstance: ReactNativeEventComponentInstance,
471 ): void {
448 - throw new Error('Not yet implemented.');
472 + if (enableFlareAPI) {
473 + // TODO stop listening to targetEventTypes
474 + unmountEventResponder(eventComponentInstance);
475 + }
476 }
477
478 export function getEventTargetChildElement(
packages/react-native-renderer/src/ReactNativeHostConfig.js
+12 -5
@@ -9,7 +9,9 @@
9
10 import type {
11 ReactNativeBaseComponentViewConfig,
12 - ReactNativeEventResponder,
12 + ReactNativeEventResponderEventType,
13 + ReactNativeResponderEvent,
14 + ReactNativeResponderContext,
15 } from './ReactNativeTypes';
16 import type {ReactEventComponentInstance} from 'shared/ReactTypes';
17
@@ -32,6 +34,12 @@ import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
34
35 const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
36
37 +type ReactNativeEventComponentInstance = ReactEventComponentInstance<
38 + ReactNativeEventResponderEventType,
39 + ReactNativeResponderEvent,
40 + ReactNativeResponderContext,
41 +>;
42 +
43 export type Type = string;
44 export type Props = Object;
45 export type Container = number;
@@ -51,7 +59,6 @@ export type ChildSet = void; // Unused
59
60 export type TimeoutHandle = TimeoutID;
61 export type NoTimeout = -1;
54 -export type EventResponder = ReactNativeEventResponder;
62
63 const UPDATE_SIGNAL = {};
64 if (__DEV__) {
@@ -490,19 +497,19 @@ export function unhideTextInstance(
497 }
498
499 export function mountEventComponent(
493 - eventComponentInstance: ReactEventComponentInstance<any, any, any>,
500 + eventComponentInstance: ReactNativeEventComponentInstance,
501 ) {
502 throw new Error('Not yet implemented.');
503 }
504
505 export function updateEventComponent(
499 - eventComponentInstance: ReactEventComponentInstance<any, any, any>,
506 + eventComponentInstance: ReactNativeEventComponentInstance,
507 ) {
508 throw new Error('Not yet implemented.');
509 }
510
511 export function unmountEventComponent(
505 - eventComponentInstance: ReactEventComponentInstance<any, any, any>,
512 + eventComponentInstance: ReactNativeEventComponentInstance,
513 ): void {
514 throw new Error('Not yet implemented.');
515 }
packages/react-native-renderer/src/ReactNativeTypes.js
+93 -3
@@ -154,9 +154,99 @@ export type ReactFabricType = {
154 callback: ?Function,
155 ): any,
156 unmountComponentAtNode(containerTag: number): any,
157 -
157 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: SecretInternalsFabricType,
158 };
159
161 -// TODO will be addressed with upcoming React Flare support
162 -export type ReactNativeEventResponder = any;
160 +export type ReactNativeEventResponderEventType =
161 + | 'topMouseDown'
162 + | 'topMouseMove'
163 + | 'topMouseUp'
164 + | 'topScroll'
165 + | 'topSelectionChange'
166 + | 'topTouchCancel'
167 + | 'topTouchEnd'
168 + | 'topTouchMove'
169 + | 'topTouchStart';
170 +
171 +export type ReactNativeEventTarget = {
172 + node: Object,
173 + canonical: {
174 + _nativeTag: number,
175 + viewConfig: ReactNativeBaseComponentViewConfig<>,
176 + currentProps: Object,
177 + _internalInstanceHandle: Object,
178 + },
179 +};
180 +
181 +export type ReactFaricEventTouch = {
182 + identifier: number,
183 + locationX: number,
184 + locationY: number,
185 + pageX: number,
186 + pageY: number,
187 + screenX: number,
188 + screenY: number,
189 + target: number,
190 + timestamp: number,
191 + force: number,
192 +};
193 +
194 +export type ReactFaricEvent = {
195 + touches: Array<ReactFaricEventTouch>,
196 + changedTouches: Array<ReactFaricEventTouch>,
197 + targetTouches: Array<ReactFaricEventTouch>,
198 + target: number,
199 +};
200 +
201 +export type ReactNativeResponderEvent = {
202 + nativeEvent: ReactFaricEvent,
203 + target: null | ReactNativeEventTarget,
204 + type: ReactNativeEventResponderEventType,
205 +};
206 +
207 +export type ReactNativeResponderContext = {
208 + dispatchEvent: (
209 + eventObject: Object,
210 + listener: (Object) => void,
211 + eventPriority: EventPriority,
212 + ) => void,
213 + isTargetWithinNode: (
214 + childTarget: ReactNativeEventTarget,
215 + parentTarget: ReactNativeEventTarget,
216 + ) => boolean,
217 + getTargetBoundingRect(
218 + target: ReactNativeEventTarget,
219 + cb: ({
220 + left: number,
221 + right: number,
222 + top: number,
223 + bottom: number,
224 + }) => void,
225 + ): void,
226 + addRootEventTypes: (
227 + rootEventTypes: Array<ReactNativeEventResponderEventType>,
228 + ) => void,
229 + removeRootEventTypes: (
230 + rootEventTypes: Array<ReactNativeEventResponderEventType>,
231 + ) => void,
232 + getEventCurrentTarget(
233 + event: ReactNativeResponderEvent,
234 + ): ReactNativeEventTarget,
235 + setTimeout: (func: () => void, timeout: number) => number,
236 + clearTimeout: (timerId: number) => void,
237 + getTimeStamp: () => number,
238 +};
239 +
240 +export type PointerType =
241 + | ''
242 + | 'mouse'
243 + | 'keyboard'
244 + | 'pen'
245 + | 'touch'
246 + | 'trackpad';
247 +
248 +export opaque type EventPriority = 0 | 1 | 2;
249 +
250 +export const DiscreteEvent: EventPriority = 0;
251 +export const UserBlockingEvent: EventPriority = 1;
252 +export const ContinuousEvent: EventPriority = 2;
packages/shared/ReactDOMTypes.js
+1 -1
@@ -55,7 +55,7 @@ export type ReactDOMResponderContext = {
55 listener: (Object) => void,
56 eventPriority: EventPriority,
57 ) => void,
58 - isTargetWithinElement: (
58 + isTargetWithinNode: (
59 childTarget: Element | Document,
60 parentTarget: Element | Document,
61 ) => boolean,