@office-iss/react-native-win32 0.67.0-preview.2 → 0.67.0-preview.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,962 @@
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 strict-local
8
+ * @format
9
+ */
10
+
11
+ import {isHoverEnabled} from './HoverState';
12
+ import invariant from 'invariant';
13
+ import SoundManager from '../Components/Sound/SoundManager';
14
+ import {normalizeRect, type RectOrSize} from '../StyleSheet/Rect';
15
+ import type {
16
+ BlurEvent,
17
+ FocusEvent,
18
+ PressEvent,
19
+ MouseEvent,
20
+ KeyEvent, // [Windows]
21
+ } from '../Types/CoreEventTypes';
22
+ import PressabilityPerformanceEventEmitter from './PressabilityPerformanceEventEmitter.js';
23
+ import {type PressabilityTouchSignal as TouchSignal} from './PressabilityTypes.js';
24
+ import Platform from '../Utilities/Platform';
25
+ import UIManager from '../ReactNative/UIManager';
26
+ import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
27
+ import * as React from 'react';
28
+
29
+ export type PressabilityConfig = $ReadOnly<{|
30
+ /**
31
+ * Whether a press gesture can be interrupted by a parent gesture such as a
32
+ * scroll event. Defaults to true.
33
+ */
34
+ cancelable?: ?boolean,
35
+
36
+ /**
37
+ * Whether to disable initialization of the press gesture.
38
+ */
39
+ disabled?: ?boolean,
40
+
41
+ /**
42
+ * Amount to extend the `VisualRect` by to create `HitRect`.
43
+ */
44
+ hitSlop?: ?RectOrSize,
45
+
46
+ /**
47
+ * Amount to extend the `HitRect` by to create `PressRect`.
48
+ */
49
+ pressRectOffset?: ?RectOrSize,
50
+
51
+ /**
52
+ * Whether to disable the systemm sound when `onPress` fires on Android.
53
+ **/
54
+ android_disableSound?: ?boolean,
55
+
56
+ /**
57
+ * Duration to wait after hover in before calling `onHoverIn`.
58
+ */
59
+ delayHoverIn?: ?number,
60
+
61
+ /**
62
+ * Duration to wait after hover out before calling `onHoverOut`.
63
+ */
64
+ delayHoverOut?: ?number,
65
+
66
+ /**
67
+ * Duration (in addition to `delayPressIn`) after which a press gesture is
68
+ * considered a long press gesture. Defaults to 500 (milliseconds).
69
+ */
70
+ delayLongPress?: ?number,
71
+
72
+ /**
73
+ * Duration to wait after press down before calling `onPressIn`.
74
+ */
75
+ delayPressIn?: ?number,
76
+
77
+ /**
78
+ * Duration to wait after letting up before calling `onPressOut`.
79
+ */
80
+ delayPressOut?: ?number,
81
+
82
+ /**
83
+ * Minimum duration to wait between calling `onPressIn` and `onPressOut`.
84
+ */
85
+ minPressDuration?: ?number,
86
+
87
+ /**
88
+ * Called after the element loses focus.
89
+ */
90
+ onBlur?: ?(event: BlurEvent) => mixed,
91
+
92
+ /**
93
+ * Called after the element is focused.
94
+ */
95
+ onFocus?: ?(event: FocusEvent) => mixed,
96
+
97
+ /*
98
+ * Called after a key down event is detected.
99
+ */
100
+ onKeyDown?: ?(event: KeyEvent) => mixed,
101
+
102
+ /*
103
+ * Called after a key up event is detected.
104
+ */
105
+ onKeyUp?: ?(event: KeyEvent) => mixed,
106
+
107
+ /**
108
+ * Called when the hover is activated to provide visual feedback.
109
+ */
110
+ onHoverIn?: ?(event: MouseEvent) => mixed,
111
+
112
+ /**
113
+ * Called when the hover is deactivated to undo visual feedback.
114
+ */
115
+ onHoverOut?: ?(event: MouseEvent) => mixed,
116
+
117
+ /**
118
+ * Called when a long press gesture has been triggered.
119
+ */
120
+ onLongPress?: ?(event: PressEvent) => mixed,
121
+
122
+ /**
123
+ * Called when a press gestute has been triggered.
124
+ */
125
+ onPress?: ?(event: PressEvent) => mixed,
126
+
127
+ /**
128
+ * Called when the press is activated to provide visual feedback.
129
+ */
130
+ onPressIn?: ?(event: PressEvent) => mixed,
131
+
132
+ /**
133
+ * Called when the press location moves. (This should rarely be used.)
134
+ */
135
+ onPressMove?: ?(event: PressEvent) => mixed,
136
+
137
+ /**
138
+ * Called when the press is deactivated to undo visual feedback.
139
+ */
140
+ onPressOut?: ?(event: PressEvent) => mixed,
141
+
142
+ /**
143
+ * Returns whether a long press gesture should cancel the press gesture.
144
+ * Defaults to true.
145
+ */
146
+ onLongPressShouldCancelPress_DEPRECATED?: ?() => boolean,
147
+
148
+ /**
149
+ * If `cancelable` is set, this will be ignored.
150
+ *
151
+ * Returns whether to yield to a lock termination request (e.g. if a native
152
+ * scroll gesture attempts to steal the responder lock).
153
+ */
154
+ onResponderTerminationRequest_DEPRECATED?: ?() => boolean,
155
+
156
+ /**
157
+ * If `disabled` is set, this will be ignored.
158
+ *
159
+ * Returns whether to start a press gesture.
160
+ *
161
+ * @deprecated
162
+ */
163
+ onStartShouldSetResponder_DEPRECATED?: ?() => boolean,
164
+
165
+ // [Windows
166
+ /**
167
+ * Raw handler for onMouseEnter that will be preferred if set over hover
168
+ * events. This is to preserve compatibility with pre-0.62 behavior which
169
+ * allowed attaching mouse event handlers to Touchables
170
+ */
171
+ onMouseEnter?: ?(event: MouseEvent) => mixed,
172
+
173
+ /**
174
+ * Raw handler for onMouseLeave that will be preferred if set over hover
175
+ * events. This is to preserve compatibility with pre-0.62 behavior which
176
+ * allowed attaching mouse event handlers to Touchables
177
+ */
178
+ onMouseLeave?: ?(event: MouseEvent) => mixed,
179
+ // Windows]
180
+ |}>;
181
+
182
+ export type EventHandlers = $ReadOnly<{|
183
+ onBlur: (event: BlurEvent) => void,
184
+ onClick: (event: PressEvent) => void,
185
+ onFocus: (event: FocusEvent) => void,
186
+ onMouseEnter?: (event: MouseEvent) => void,
187
+ onMouseLeave?: (event: MouseEvent) => void,
188
+ onResponderGrant: (event: PressEvent) => void,
189
+ onResponderMove: (event: PressEvent) => void,
190
+ onResponderRelease: (event: PressEvent) => void,
191
+ onResponderTerminate: (event: PressEvent) => void,
192
+ onResponderTerminationRequest: () => boolean,
193
+ onStartShouldSetResponder: () => boolean,
194
+ // [Windows
195
+ onKeyUp: (event: KeyEvent) => void,
196
+ onKeyDown: (event: KeyEvent) => void,
197
+ // Windows]
198
+ |}>;
199
+
200
+ type TouchState =
201
+ | 'NOT_RESPONDER'
202
+ | 'RESPONDER_INACTIVE_PRESS_IN'
203
+ | 'RESPONDER_INACTIVE_PRESS_OUT'
204
+ | 'RESPONDER_ACTIVE_PRESS_IN'
205
+ | 'RESPONDER_ACTIVE_PRESS_OUT'
206
+ | 'RESPONDER_ACTIVE_LONG_PRESS_IN'
207
+ | 'RESPONDER_ACTIVE_LONG_PRESS_OUT'
208
+ | 'ERROR';
209
+
210
+ const Transitions = Object.freeze({
211
+ NOT_RESPONDER: {
212
+ DELAY: 'ERROR',
213
+ RESPONDER_GRANT: 'RESPONDER_INACTIVE_PRESS_IN',
214
+ RESPONDER_RELEASE: 'ERROR',
215
+ RESPONDER_TERMINATED: 'ERROR',
216
+ ENTER_PRESS_RECT: 'ERROR',
217
+ LEAVE_PRESS_RECT: 'ERROR',
218
+ LONG_PRESS_DETECTED: 'ERROR',
219
+ },
220
+ RESPONDER_INACTIVE_PRESS_IN: {
221
+ DELAY: 'RESPONDER_ACTIVE_PRESS_IN',
222
+ RESPONDER_GRANT: 'ERROR',
223
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
224
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
225
+ ENTER_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_IN',
226
+ LEAVE_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_OUT',
227
+ LONG_PRESS_DETECTED: 'ERROR',
228
+ },
229
+ RESPONDER_INACTIVE_PRESS_OUT: {
230
+ DELAY: 'RESPONDER_ACTIVE_PRESS_OUT',
231
+ RESPONDER_GRANT: 'ERROR',
232
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
233
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
234
+ ENTER_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_IN',
235
+ LEAVE_PRESS_RECT: 'RESPONDER_INACTIVE_PRESS_OUT',
236
+ LONG_PRESS_DETECTED: 'ERROR',
237
+ },
238
+ RESPONDER_ACTIVE_PRESS_IN: {
239
+ DELAY: 'ERROR',
240
+ RESPONDER_GRANT: 'ERROR',
241
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
242
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
243
+ ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_IN',
244
+ LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_OUT',
245
+ LONG_PRESS_DETECTED: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
246
+ },
247
+ RESPONDER_ACTIVE_PRESS_OUT: {
248
+ DELAY: 'ERROR',
249
+ RESPONDER_GRANT: 'ERROR',
250
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
251
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
252
+ ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_IN',
253
+ LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_PRESS_OUT',
254
+ LONG_PRESS_DETECTED: 'ERROR',
255
+ },
256
+ RESPONDER_ACTIVE_LONG_PRESS_IN: {
257
+ DELAY: 'ERROR',
258
+ RESPONDER_GRANT: 'ERROR',
259
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
260
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
261
+ ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
262
+ LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT',
263
+ LONG_PRESS_DETECTED: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
264
+ },
265
+ RESPONDER_ACTIVE_LONG_PRESS_OUT: {
266
+ DELAY: 'ERROR',
267
+ RESPONDER_GRANT: 'ERROR',
268
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
269
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
270
+ ENTER_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_IN',
271
+ LEAVE_PRESS_RECT: 'RESPONDER_ACTIVE_LONG_PRESS_OUT',
272
+ LONG_PRESS_DETECTED: 'ERROR',
273
+ },
274
+ ERROR: {
275
+ DELAY: 'NOT_RESPONDER',
276
+ RESPONDER_GRANT: 'RESPONDER_INACTIVE_PRESS_IN',
277
+ RESPONDER_RELEASE: 'NOT_RESPONDER',
278
+ RESPONDER_TERMINATED: 'NOT_RESPONDER',
279
+ ENTER_PRESS_RECT: 'NOT_RESPONDER',
280
+ LEAVE_PRESS_RECT: 'NOT_RESPONDER',
281
+ LONG_PRESS_DETECTED: 'NOT_RESPONDER',
282
+ },
283
+ });
284
+
285
+ const isActiveSignal = signal =>
286
+ signal === 'RESPONDER_ACTIVE_PRESS_IN' ||
287
+ signal === 'RESPONDER_ACTIVE_LONG_PRESS_IN';
288
+
289
+ const isActivationSignal = signal =>
290
+ signal === 'RESPONDER_ACTIVE_PRESS_OUT' ||
291
+ signal === 'RESPONDER_ACTIVE_PRESS_IN';
292
+
293
+ const isPressInSignal = signal =>
294
+ signal === 'RESPONDER_INACTIVE_PRESS_IN' ||
295
+ signal === 'RESPONDER_ACTIVE_PRESS_IN' ||
296
+ signal === 'RESPONDER_ACTIVE_LONG_PRESS_IN';
297
+
298
+ const isTerminalSignal = signal =>
299
+ signal === 'RESPONDER_TERMINATED' || signal === 'RESPONDER_RELEASE';
300
+
301
+ const DEFAULT_LONG_PRESS_DELAY_MS = 500;
302
+ const DEFAULT_PRESS_RECT_OFFSETS = {
303
+ bottom: 30,
304
+ left: 20,
305
+ right: 20,
306
+ top: 20,
307
+ };
308
+ const DEFAULT_MIN_PRESS_DURATION = 130;
309
+
310
+ /**
311
+ * Pressability implements press handling capabilities.
312
+ *
313
+ * =========================== Pressability Tutorial ===========================
314
+ *
315
+ * The `Pressability` class helps you create press interactions by analyzing the
316
+ * geometry of elements and observing when another responder (e.g. ScrollView)
317
+ * has stolen the touch lock. It offers hooks for your component to provide
318
+ * interaction feedback to the user:
319
+ *
320
+ * - When a press has activated (e.g. highlight an element)
321
+ * - When a press has deactivated (e.g. un-highlight an element)
322
+ * - When a press sould trigger an action, meaning it activated and deactivated
323
+ * while within the geometry of the element without the lock being stolen.
324
+ *
325
+ * A high quality interaction isn't as simple as you might think. There should
326
+ * be a slight delay before activation. Moving your finger beyond an element's
327
+ * bounds should trigger deactivation, but moving the same finger back within an
328
+ * element's bounds should trigger reactivation.
329
+ *
330
+ * In order to use `Pressability`, do the following:
331
+ *
332
+ * 1. Instantiate `Pressability` and store it on your component's state.
333
+ *
334
+ * state = {
335
+ * pressability: new Pressability({
336
+ * // ...
337
+ * }),
338
+ * };
339
+ *
340
+ * 2. Choose the rendered component who should collect the press events. On that
341
+ * element, spread `pressability.getEventHandlers()` into its props.
342
+ *
343
+ * return (
344
+ * <View {...this.state.pressability.getEventHandlers()} />
345
+ * );
346
+ *
347
+ * 3. Reset `Pressability` when your component unmounts.
348
+ *
349
+ * componentWillUnmount() {
350
+ * this.state.pressability.reset();
351
+ * }
352
+ *
353
+ * ==================== Pressability Implementation Details ====================
354
+ *
355
+ * `Pressability` only assumes that there exists a `HitRect` node. The `PressRect`
356
+ * is an abstract box that is extended beyond the `HitRect`.
357
+ *
358
+ * # Geometry
359
+ *
360
+ * ┌────────────────────────┐
361
+ * │ ┌──────────────────┐ │ - Presses start anywhere within `HitRect`, which
362
+ * │ │ ┌────────────┐ │ │ is expanded via the prop `hitSlop`.
363
+ * │ │ │ VisualRect │ │ │
364
+ * │ │ └────────────┘ │ │ - When pressed down for sufficient amount of time
365
+ * │ │ HitRect │ │ before letting up, `VisualRect` activates for
366
+ * │ └──────────────────┘ │ as long as the press stays within `PressRect`.
367
+ * │ PressRect o │
368
+ * └────────────────────│───┘
369
+ * Out Region └────── `PressRect`, which is expanded via the prop
370
+ * `pressRectOffset`, allows presses to move
371
+ * beyond `HitRect` while maintaining activation
372
+ * and being eligible for a "press".
373
+ *
374
+ * # State Machine
375
+ *
376
+ * ┌───────────────┐ ◀──── RESPONDER_RELEASE
377
+ * │ NOT_RESPONDER │
378
+ * └───┬───────────┘ ◀──── RESPONDER_TERMINATED
379
+ * │
380
+ * │ RESPONDER_GRANT (HitRect)
381
+ * │
382
+ * ▼
383
+ * ┌─────────────────────┐ ┌───────────────────┐ ┌───────────────────┐
384
+ * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ T + DELAY │ RESPONDER_ACTIVE_ │
385
+ * │ PRESS_IN ├────────▶ │ PRESS_IN ├────────────▶ │ LONG_PRESS_IN │
386
+ * └─┬───────────────────┘ └─┬─────────────────┘ └─┬─────────────────┘
387
+ * │ ▲ │ ▲ │ ▲
388
+ * │LEAVE_ │ │LEAVE_ │ │LEAVE_ │
389
+ * │PRESS_RECT │ENTER_ │PRESS_RECT │ENTER_ │PRESS_RECT │ENTER_
390
+ * │ │PRESS_RECT │ │PRESS_RECT │ │PRESS_RECT
391
+ * ▼ │ ▼ │ ▼ │
392
+ * ┌─────────────┴───────┐ ┌─────────────┴─────┐ ┌─────────────┴─────┐
393
+ * │ RESPONDER_INACTIVE_ │ DELAY │ RESPONDER_ACTIVE_ │ │ RESPONDER_ACTIVE_ │
394
+ * │ PRESS_OUT ├────────▶ │ PRESS_OUT │ │ LONG_PRESS_OUT │
395
+ * └─────────────────────┘ └───────────────────┘ └───────────────────┘
396
+ *
397
+ * T + DELAY => LONG_PRESS_DELAY + DELAY
398
+ *
399
+ * Not drawn are the side effects of each transition. The most important side
400
+ * effect is the invocation of `onPress` and `onLongPress` that occur when a
401
+ * responder is release while in the "press in" states.
402
+ */
403
+ export default class Pressability {
404
+ _config: PressabilityConfig;
405
+ _eventHandlers: ?EventHandlers = null;
406
+ _hoverInDelayTimeout: ?TimeoutID = null;
407
+ _hoverOutDelayTimeout: ?TimeoutID = null;
408
+ _isHovered: boolean = false;
409
+ _longPressDelayTimeout: ?TimeoutID = null;
410
+ _pressDelayTimeout: ?TimeoutID = null;
411
+ _pressOutDelayTimeout: ?TimeoutID = null;
412
+ _responderID: ?number | React.ElementRef<HostComponent<mixed>> = null;
413
+ _responderRegion: ?$ReadOnly<{|
414
+ bottom: number,
415
+ left: number,
416
+ right: number,
417
+ top: number,
418
+ |}> = null;
419
+ _touchActivatePosition: ?$ReadOnly<{|
420
+ pageX: number,
421
+ pageY: number,
422
+ |}>;
423
+ _touchActivateTime: ?number;
424
+ _touchState: TouchState = 'NOT_RESPONDER';
425
+
426
+ constructor(config: PressabilityConfig) {
427
+ this.configure(config);
428
+ }
429
+
430
+ configure(config: PressabilityConfig): void {
431
+ this._config = config;
432
+ }
433
+
434
+ /**
435
+ * Resets any pending timers. This should be called on unmount.
436
+ */
437
+ reset(): void {
438
+ this._cancelHoverInDelayTimeout();
439
+ this._cancelHoverOutDelayTimeout();
440
+ this._cancelLongPressDelayTimeout();
441
+ this._cancelPressDelayTimeout();
442
+ this._cancelPressOutDelayTimeout();
443
+
444
+ // Ensure that, if any async event handlers are fired after unmount
445
+ // due to a race, we don't call any configured callbacks.
446
+ this._config = Object.freeze({});
447
+ }
448
+
449
+ /**
450
+ * Returns a set of props to spread into the interactive element.
451
+ */
452
+ getEventHandlers(): EventHandlers {
453
+ if (this._eventHandlers == null) {
454
+ this._eventHandlers = this._createEventHandlers();
455
+ }
456
+ return this._eventHandlers;
457
+ }
458
+
459
+ _createEventHandlers(): EventHandlers {
460
+ const focusEventHandlers = {
461
+ onBlur: (event: BlurEvent): void => {
462
+ const {onBlur} = this._config;
463
+ if (onBlur != null) {
464
+ onBlur(event);
465
+ }
466
+ },
467
+ onFocus: (event: FocusEvent): void => {
468
+ const {onFocus} = this._config;
469
+ if (onFocus != null) {
470
+ onFocus(event);
471
+ }
472
+ },
473
+ };
474
+
475
+ const responderEventHandlers = {
476
+ onStartShouldSetResponder: (): boolean => {
477
+ const {disabled} = this._config;
478
+ if (disabled == null) {
479
+ const {onStartShouldSetResponder_DEPRECATED} = this._config;
480
+ return onStartShouldSetResponder_DEPRECATED == null
481
+ ? true
482
+ : onStartShouldSetResponder_DEPRECATED();
483
+ }
484
+ return !disabled;
485
+ },
486
+
487
+ onResponderGrant: (event: PressEvent): void => {
488
+ event.persist();
489
+
490
+ this._cancelPressOutDelayTimeout();
491
+
492
+ this._responderID = event.currentTarget;
493
+ this._touchState = 'NOT_RESPONDER';
494
+ this._receiveSignal('RESPONDER_GRANT', event);
495
+
496
+ const delayPressIn = normalizeDelay(this._config.delayPressIn);
497
+ if (delayPressIn > 0) {
498
+ this._pressDelayTimeout = setTimeout(() => {
499
+ this._receiveSignal('DELAY', event);
500
+ }, delayPressIn);
501
+ } else {
502
+ this._receiveSignal('DELAY', event);
503
+ }
504
+
505
+ const delayLongPress = normalizeDelay(
506
+ this._config.delayLongPress,
507
+ 10,
508
+ DEFAULT_LONG_PRESS_DELAY_MS - delayPressIn,
509
+ );
510
+ this._longPressDelayTimeout = setTimeout(() => {
511
+ this._handleLongPress(event);
512
+ }, delayLongPress + delayPressIn);
513
+ },
514
+
515
+ onResponderMove: (event: PressEvent): void => {
516
+ const {onPressMove} = this._config;
517
+ if (onPressMove != null) {
518
+ onPressMove(event);
519
+ }
520
+
521
+ // Region may not have finished being measured, yet.
522
+ const responderRegion = this._responderRegion;
523
+ if (responderRegion == null) {
524
+ return;
525
+ }
526
+
527
+ const touch = getTouchFromPressEvent(event);
528
+ if (touch == null) {
529
+ this._cancelLongPressDelayTimeout();
530
+ this._receiveSignal('LEAVE_PRESS_RECT', event);
531
+ return;
532
+ }
533
+
534
+ if (this._touchActivatePosition != null) {
535
+ const deltaX = this._touchActivatePosition.pageX - touch.pageX;
536
+ const deltaY = this._touchActivatePosition.pageY - touch.pageY;
537
+ if (Math.hypot(deltaX, deltaY) > 10) {
538
+ this._cancelLongPressDelayTimeout();
539
+ }
540
+ }
541
+
542
+ if (this._isTouchWithinResponderRegion(touch, responderRegion)) {
543
+ this._receiveSignal('ENTER_PRESS_RECT', event);
544
+ } else {
545
+ this._cancelLongPressDelayTimeout();
546
+ this._receiveSignal('LEAVE_PRESS_RECT', event);
547
+ }
548
+ },
549
+
550
+ onResponderRelease: (event: PressEvent): void => {
551
+ this._receiveSignal('RESPONDER_RELEASE', event);
552
+ },
553
+
554
+ onResponderTerminate: (event: PressEvent): void => {
555
+ this._receiveSignal('RESPONDER_TERMINATED', event);
556
+ },
557
+
558
+ onResponderTerminationRequest: (): boolean => {
559
+ const {cancelable} = this._config;
560
+ if (cancelable == null) {
561
+ const {onResponderTerminationRequest_DEPRECATED} = this._config;
562
+ return onResponderTerminationRequest_DEPRECATED == null
563
+ ? true
564
+ : onResponderTerminationRequest_DEPRECATED();
565
+ }
566
+ return cancelable;
567
+ },
568
+
569
+ onClick: (event: PressEvent): void => {
570
+ const {onPress, disabled} = this._config;
571
+ if (onPress != null && disabled !== true) {
572
+ onPress(event);
573
+ }
574
+ },
575
+ };
576
+
577
+ if (process.env.NODE_ENV === 'test') {
578
+ // We are setting this in order to find this node in ReactNativeTestTools
579
+ responderEventHandlers.onStartShouldSetResponder.testOnly_pressabilityConfig = () =>
580
+ this._config;
581
+ }
582
+
583
+ const mouseEventHandlers =
584
+ Platform.OS === 'ios' || Platform.OS === 'android'
585
+ ? null
586
+ : {
587
+ onMouseEnter: (event: MouseEvent): void => {
588
+ // [Windows Add attached raw mouse event handler for compat
589
+ if (this._config.onMouseEnter) {
590
+ this._config.onMouseEnter(event);
591
+ }
592
+ // Windows]
593
+
594
+ if (isHoverEnabled()) {
595
+ this._isHovered = true;
596
+ this._cancelHoverOutDelayTimeout();
597
+ const {onHoverIn} = this._config;
598
+ if (onHoverIn != null) {
599
+ const delayHoverIn = normalizeDelay(
600
+ this._config.delayHoverIn,
601
+ );
602
+ if (delayHoverIn > 0) {
603
+ event.persist();
604
+ this._hoverInDelayTimeout = setTimeout(() => {
605
+ onHoverIn(event);
606
+ }, delayHoverIn);
607
+ } else {
608
+ onHoverIn(event);
609
+ }
610
+ }
611
+ }
612
+ },
613
+
614
+ onMouseLeave: (event: MouseEvent): void => {
615
+ // [Windows Add attached raw mouse event handler for compat
616
+ if (this._config.onMouseLeave) {
617
+ this._config.onMouseLeave(event);
618
+ }
619
+ // Windows]
620
+
621
+ if (this._isHovered) {
622
+ this._isHovered = false;
623
+ this._cancelHoverInDelayTimeout();
624
+ const {onHoverOut} = this._config;
625
+ if (onHoverOut != null) {
626
+ const delayHoverOut = normalizeDelay(
627
+ this._config.delayHoverOut,
628
+ );
629
+ if (delayHoverOut > 0) {
630
+ event.persist();
631
+ this._hoverInDelayTimeout = setTimeout(() => {
632
+ onHoverOut(event);
633
+ }, delayHoverOut);
634
+ } else {
635
+ onHoverOut(event);
636
+ }
637
+ }
638
+ }
639
+ },
640
+ };
641
+
642
+ // [Windows
643
+ const keyboardEventHandlers = {
644
+ onKeyUp: (event: KeyEvent): void => {
645
+ const {onKeyUp} = this._config;
646
+ onKeyUp && onKeyUp(event);
647
+
648
+ if (
649
+ (event.nativeEvent.code === 'Space' ||
650
+ event.nativeEvent.code === 'Enter' ||
651
+ event.nativeEvent.code === 'GamepadA') &&
652
+ event.defaultPrevented != true
653
+ ) {
654
+ const {onPressOut, onPress} = this._config;
655
+ // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead
656
+ onPressOut && onPressOut(event);
657
+ // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead
658
+ onPress && onPress(event);
659
+ }
660
+ },
661
+ onKeyDown: (event: KeyEvent): void => {
662
+ const {onKeyDown} = this._config;
663
+ onKeyDown && onKeyDown(event);
664
+
665
+ if (
666
+ (event.nativeEvent.code === 'Space' ||
667
+ event.nativeEvent.code === 'Enter' ||
668
+ event.nativeEvent.code === 'GamepadA') &&
669
+ event.defaultPrevented != true
670
+ ) {
671
+ const {onPressIn} = this._config;
672
+ // $FlowFixMe: PressEvents don't mesh with keyboarding APIs. Keep legacy behavior of passing KeyEvents instead
673
+ onPressIn && onPressIn(event);
674
+ }
675
+ },
676
+ };
677
+ // Windows]
678
+
679
+ return {
680
+ ...focusEventHandlers,
681
+ ...responderEventHandlers,
682
+ ...mouseEventHandlers,
683
+ ...keyboardEventHandlers, // [Windows]
684
+ };
685
+ }
686
+
687
+ /**
688
+ * Receives a state machine signal, performs side effects of the transition
689
+ * and stores the new state. Validates the transition as well.
690
+ */
691
+ _receiveSignal(signal: TouchSignal, event: PressEvent): void {
692
+ // Especially on iOS, not all events have timestamps associated.
693
+ // For telemetry purposes, this doesn't matter too much, as long as *some* do.
694
+ // Since the native timestamp is integral for logging telemetry, just skip
695
+ // events if they don't have a timestamp attached.
696
+ if (event.nativeEvent.timestamp != null) {
697
+ PressabilityPerformanceEventEmitter.emitEvent(() => {
698
+ return {
699
+ signal,
700
+ nativeTimestamp: event.nativeEvent.timestamp,
701
+ };
702
+ });
703
+ }
704
+
705
+ const prevState = this._touchState;
706
+ const nextState = Transitions[prevState]?.[signal];
707
+ if (this._responderID == null && signal === 'RESPONDER_RELEASE') {
708
+ return;
709
+ }
710
+ invariant(
711
+ nextState != null && nextState !== 'ERROR',
712
+ 'Pressability: Invalid signal `%s` for state `%s` on responder: %s',
713
+ signal,
714
+ prevState,
715
+ typeof this._responderID === 'number'
716
+ ? this._responderID
717
+ : '<<host component>>',
718
+ );
719
+ if (prevState !== nextState) {
720
+ this._performTransitionSideEffects(prevState, nextState, signal, event);
721
+ this._touchState = nextState;
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Performs a transition between touchable states and identify any activations
727
+ * or deactivations (and callback invocations).
728
+ */
729
+ _performTransitionSideEffects(
730
+ prevState: TouchState,
731
+ nextState: TouchState,
732
+ signal: TouchSignal,
733
+ event: PressEvent,
734
+ ): void {
735
+ if (isTerminalSignal(signal)) {
736
+ this._touchActivatePosition = null;
737
+ this._cancelLongPressDelayTimeout();
738
+ }
739
+
740
+ const isInitialTransition =
741
+ prevState === 'NOT_RESPONDER' &&
742
+ nextState === 'RESPONDER_INACTIVE_PRESS_IN';
743
+
744
+ const isActivationTransition =
745
+ !isActivationSignal(prevState) && isActivationSignal(nextState);
746
+
747
+ if (isInitialTransition || isActivationTransition) {
748
+ this._measureResponderRegion();
749
+ }
750
+
751
+ if (isPressInSignal(prevState) && signal === 'LONG_PRESS_DETECTED') {
752
+ const {onLongPress} = this._config;
753
+ if (onLongPress != null) {
754
+ onLongPress(event);
755
+ }
756
+ }
757
+
758
+ const isPrevActive = isActiveSignal(prevState);
759
+ const isNextActive = isActiveSignal(nextState);
760
+
761
+ if (!isPrevActive && isNextActive) {
762
+ this._activate(event);
763
+ } else if (isPrevActive && !isNextActive) {
764
+ this._deactivate(event);
765
+ }
766
+
767
+ if (isPressInSignal(prevState) && signal === 'RESPONDER_RELEASE') {
768
+ // If we never activated (due to delays), activate and deactivate now.
769
+ if (!isNextActive && !isPrevActive) {
770
+ this._activate(event);
771
+ this._deactivate(event);
772
+ }
773
+ const {onLongPress, onPress, android_disableSound} = this._config;
774
+ if (onPress != null) {
775
+ const isPressCanceledByLongPress =
776
+ onLongPress != null &&
777
+ prevState === 'RESPONDER_ACTIVE_LONG_PRESS_IN' &&
778
+ this._shouldLongPressCancelPress();
779
+ if (!isPressCanceledByLongPress) {
780
+ if (Platform.OS === 'android' && android_disableSound !== true) {
781
+ SoundManager.playTouchSound();
782
+ }
783
+ onPress(event);
784
+ }
785
+ }
786
+ }
787
+
788
+ this._cancelPressDelayTimeout();
789
+ }
790
+
791
+ _activate(event: PressEvent): void {
792
+ const {onPressIn} = this._config;
793
+ const {pageX, pageY} = getTouchFromPressEvent(event);
794
+ this._touchActivatePosition = {pageX, pageY};
795
+ this._touchActivateTime = Date.now();
796
+ if (onPressIn != null) {
797
+ onPressIn(event);
798
+ }
799
+ }
800
+
801
+ _deactivate(event: PressEvent): void {
802
+ const {onPressOut} = this._config;
803
+ if (onPressOut != null) {
804
+ const minPressDuration = normalizeDelay(
805
+ this._config.minPressDuration,
806
+ 0,
807
+ DEFAULT_MIN_PRESS_DURATION,
808
+ );
809
+ const pressDuration = Date.now() - (this._touchActivateTime ?? 0);
810
+ const delayPressOut = Math.max(
811
+ minPressDuration - pressDuration,
812
+ normalizeDelay(this._config.delayPressOut),
813
+ );
814
+ if (delayPressOut > 0) {
815
+ event.persist();
816
+ this._pressOutDelayTimeout = setTimeout(() => {
817
+ onPressOut(event);
818
+ }, delayPressOut);
819
+ } else {
820
+ onPressOut(event);
821
+ }
822
+ }
823
+ this._touchActivateTime = null;
824
+ }
825
+
826
+ _measureResponderRegion(): void {
827
+ if (this._responderID == null) {
828
+ return;
829
+ }
830
+
831
+ if (typeof this._responderID === 'number') {
832
+ UIManager.measure(this._responderID, this._measureCallback);
833
+ } else {
834
+ this._responderID.measure(this._measureCallback);
835
+ }
836
+ }
837
+
838
+ _measureCallback = (left, top, width, height, pageX, pageY) => {
839
+ if (!left && !top && !width && !height && !pageX && !pageY) {
840
+ return;
841
+ }
842
+ this._responderRegion = {
843
+ bottom: pageY + height,
844
+ left: pageX,
845
+ right: pageX + width,
846
+ top: pageY,
847
+ };
848
+ };
849
+
850
+ _isTouchWithinResponderRegion(
851
+ touch: $PropertyType<PressEvent, 'nativeEvent'>,
852
+ responderRegion: $ReadOnly<{|
853
+ bottom: number,
854
+ left: number,
855
+ right: number,
856
+ top: number,
857
+ |}>,
858
+ ): boolean {
859
+ const hitSlop = normalizeRect(this._config.hitSlop);
860
+ const pressRectOffset = normalizeRect(this._config.pressRectOffset);
861
+
862
+ let regionBottom = responderRegion.bottom;
863
+ let regionLeft = responderRegion.left;
864
+ let regionRight = responderRegion.right;
865
+ let regionTop = responderRegion.top;
866
+
867
+ if (hitSlop != null) {
868
+ if (hitSlop.bottom != null) {
869
+ regionBottom += hitSlop.bottom;
870
+ }
871
+ if (hitSlop.left != null) {
872
+ regionLeft -= hitSlop.left;
873
+ }
874
+ if (hitSlop.right != null) {
875
+ regionRight += hitSlop.right;
876
+ }
877
+ if (hitSlop.top != null) {
878
+ regionTop -= hitSlop.top;
879
+ }
880
+ }
881
+
882
+ regionBottom +=
883
+ pressRectOffset?.bottom ?? DEFAULT_PRESS_RECT_OFFSETS.bottom;
884
+ regionLeft -= pressRectOffset?.left ?? DEFAULT_PRESS_RECT_OFFSETS.left;
885
+ regionRight += pressRectOffset?.right ?? DEFAULT_PRESS_RECT_OFFSETS.right;
886
+ regionTop -= pressRectOffset?.top ?? DEFAULT_PRESS_RECT_OFFSETS.top;
887
+
888
+ return (
889
+ touch.pageX > regionLeft &&
890
+ touch.pageX < regionRight &&
891
+ touch.pageY > regionTop &&
892
+ touch.pageY < regionBottom
893
+ );
894
+ }
895
+
896
+ _handleLongPress(event: PressEvent): void {
897
+ if (
898
+ this._touchState === 'RESPONDER_ACTIVE_PRESS_IN' ||
899
+ this._touchState === 'RESPONDER_ACTIVE_LONG_PRESS_IN'
900
+ ) {
901
+ this._receiveSignal('LONG_PRESS_DETECTED', event);
902
+ }
903
+ }
904
+
905
+ _shouldLongPressCancelPress(): boolean {
906
+ return (
907
+ this._config.onLongPressShouldCancelPress_DEPRECATED == null ||
908
+ this._config.onLongPressShouldCancelPress_DEPRECATED()
909
+ );
910
+ }
911
+
912
+ _cancelHoverInDelayTimeout(): void {
913
+ if (this._hoverInDelayTimeout != null) {
914
+ clearTimeout(this._hoverInDelayTimeout);
915
+ this._hoverInDelayTimeout = null;
916
+ }
917
+ }
918
+
919
+ _cancelHoverOutDelayTimeout(): void {
920
+ if (this._hoverOutDelayTimeout != null) {
921
+ clearTimeout(this._hoverOutDelayTimeout);
922
+ this._hoverOutDelayTimeout = null;
923
+ }
924
+ }
925
+
926
+ _cancelLongPressDelayTimeout(): void {
927
+ if (this._longPressDelayTimeout != null) {
928
+ clearTimeout(this._longPressDelayTimeout);
929
+ this._longPressDelayTimeout = null;
930
+ }
931
+ }
932
+
933
+ _cancelPressDelayTimeout(): void {
934
+ if (this._pressDelayTimeout != null) {
935
+ clearTimeout(this._pressDelayTimeout);
936
+ this._pressDelayTimeout = null;
937
+ }
938
+ }
939
+
940
+ _cancelPressOutDelayTimeout(): void {
941
+ if (this._pressOutDelayTimeout != null) {
942
+ clearTimeout(this._pressOutDelayTimeout);
943
+ this._pressOutDelayTimeout = null;
944
+ }
945
+ }
946
+ }
947
+
948
+ function normalizeDelay(delay: ?number, min = 0, fallback = 0): number {
949
+ return Math.max(min, delay ?? fallback);
950
+ }
951
+
952
+ const getTouchFromPressEvent = (event: PressEvent) => {
953
+ const {changedTouches, touches} = event.nativeEvent;
954
+
955
+ if (touches != null && touches.length > 0) {
956
+ return touches[0];
957
+ }
958
+ if (changedTouches != null && changedTouches.length > 0) {
959
+ return changedTouches[0];
960
+ }
961
+ return event.nativeEvent;
962
+ };