[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,