[react-ui] usePress from useKeyboard and useTap (#16772)
This implements 'usePress' in user-space as a combination of 'useKeyboard' and 'useTap'. The existing 'usePress' API is preserved for now. The previous 'usePress' implementation is moved to 'PressLegacy'.
Nicolas Gallagher committed
Sep 16, 2019 at 14:36 UTC
3af05de1aaed309f8146bc53f9a4b4d785abdd3f
26 files changed
+2364
-1438
packages/react-ui/accessibility/src/FocusGrid.js
+2
-2
@@ -7,11 +7,11 @@
7
* @flow
8
*/
9
10
-import type {KeyboardEvent} from 'react-ui/events/src/dom/Keyboard';
10
+import type {KeyboardEvent} from 'react-ui/events/keyboard';
11
12
import React from 'react';
13
import {tabFocusableImpl} from './TabbableScope';
14
-import {useKeyboard} from '../../events/keyboard';
14
+import {useKeyboard} from 'react-ui/events/keyboard';
15
16
type GridComponentProps = {
17
children: React.Node,
packages/react-ui/accessibility/src/ReactTabFocus.js
+3
-2
@@ -8,16 +8,17 @@
8
*/
9
10
import type {ReactScopeMethods} from 'shared/ReactTypes';
11
-import type {KeyboardEvent} from 'react-ui/events/src/dom/Keyboard';
11
+import type {KeyboardEvent} from 'react-ui/events/keyboard';
12
13
import React from 'react';
14
import {TabbableScope} from './TabbableScope';
15
-import {useKeyboard} from '../../events/keyboard';
15
+import {useKeyboard} from 'react-ui/events/keyboard';
16
17
type TabFocusControllerProps = {
18
children: React.Node,
19
contain?: boolean,
20
};
21
+
22
const {useRef} = React;
23
24
function getTabbableNodes(scope: ReactScopeMethods) {
packages/react-ui/events/press-legacy.js
new
+12
@@ -0,0 +1,12 @@
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
+'use strict';
11
+
12
+module.exports = require('./src/dom/PressLegacy');
packages/react-ui/events/src/dom/Keyboard.js
+7
-4
@@ -27,6 +27,7 @@ type KeyboardProps = {|
27
onClick?: (e: KeyboardEvent) => ?boolean,
28
onKeyDown?: (e: KeyboardEvent) => ?boolean,
29
onKeyUp?: (e: KeyboardEvent) => ?boolean,
30
+ preventClick?: boolean,
31
preventKeys?: PreventKeysArray,
32
|};
33
@@ -256,6 +257,12 @@ const keyboardResponderImpl = {
257
);
258
}
259
} else if (type === 'click' && isVirtualClick(event)) {
260
+ if (props.preventClick !== false) {
261
+ // 'click' occurs before or after 'keyup', and may need native
262
+ // behavior prevented
263
+ nativeEvent.preventDefault();
264
+ state.defaultPrevented = true;
265
+ }
266
const onClick = props.onClick;
267
if (onClick != null) {
268
dispatchKeyboardEvent(
@@ -266,10 +273,6 @@ const keyboardResponderImpl = {
273
state.defaultPrevented,
274
);
275
}
269
- if (state.defaultPrevented && !nativeEvent.defaultPrevented) {
270
- // 'click' occurs before 'keyup' and may need native behavior prevented
271
- nativeEvent.preventDefault();
272
- }
276
} else if (type === 'keyup') {
277
state.isActive = false;
278
const onKeyUp = props.onKeyUp;
packages/react-ui/events/src/dom/Press.js
+127
-807
@@ -7,858 +7,178 @@
7
* @flow
8
*/
9
10
-import type {
11
- ReactDOMResponderEvent,
12
- ReactDOMResponderContext,
13
- PointerType,
14
-} from 'shared/ReactDOMTypes';
15
-import type {
16
- EventPriority,
17
- ReactEventResponderListener,
18
-} from 'shared/ReactTypes';
10
+import type {PointerType} from 'shared/ReactDOMTypes';
11
12
import React from 'react';
21
-import {DiscreteEvent, UserBlockingEvent} from 'shared/ReactTypes';
13
+import {useTap} from 'react-ui/events/tap';
14
+import {useKeyboard} from 'react-ui/events/keyboard';
15
23
-type PressProps = {|
24
- disabled: boolean,
25
- pressRetentionOffset: {
26
- top: number,
27
- right: number,
28
- bottom: number,
29
- left: number,
30
- },
31
- preventDefault: boolean,
32
- onPress: (e: PressEvent) => void,
33
- onPressChange: boolean => void,
34
- onPressEnd: (e: PressEvent) => void,
35
- onPressMove: (e: PressEvent) => void,
36
- onPressStart: (e: PressEvent) => void,
37
-|};
16
+const emptyObject = {};
17
39
-type PressState = {
40
- activationPosition: null | $ReadOnly<{|
41
- x: number,
42
- y: number,
43
- |}>,
44
- addedRootEvents: boolean,
45
- buttons: 0 | 1 | 4,
46
- isActivePressed: boolean,
47
- isActivePressStart: boolean,
48
- isPressed: boolean,
49
- isPressWithinResponderRegion: boolean,
50
- pointerType: PointerType,
51
- pressTarget: null | Element | Document,
52
- responderRegionOnActivation: null | $ReadOnly<{|
53
- bottom: number,
54
- left: number,
55
- right: number,
56
- top: number,
57
- |}>,
58
- responderRegionOnDeactivation: null | $ReadOnly<{|
59
- bottom: number,
60
- left: number,
61
- right: number,
62
- top: number,
63
- |}>,
64
- ignoreEmulatedMouseEvents: boolean,
65
- activePointerId: null | number,
66
- shouldPreventClick: boolean,
67
- touchEvent: null | Touch,
68
-};
18
+type PressProps = $ReadOnly<{|
19
+ disabled?: boolean,
20
+ preventDefault?: boolean,
21
+ onPress?: (e: PressEvent) => void,
22
+ onPressChange?: boolean => void,
23
+ onPressEnd?: (e: PressEvent) => void,
24
+ onPressMove?: (e: PressEvent) => void,
25
+ onPressStart?: (e: PressEvent) => void,
26
+|}>;
27
28
type PressEventType =
71
- | 'press'
72
- | 'pressmove'
29
| 'pressstart'
30
+ | 'presschange'
31
+ | 'pressmove'
32
| 'pressend'
75
- | 'presschange';
33
+ | 'press';
34
35
type PressEvent = {|
36
altKey: boolean,
79
- buttons: 0 | 1 | 4,
80
- clientX: null | number,
81
- clientY: null | number,
37
+ buttons: null | 0 | 1 | 4,
38
ctrlKey: boolean,
39
defaultPrevented: boolean,
40
+ key: null | string,
41
metaKey: boolean,
85
- pageX: null | number,
86
- pageY: null | number,
42
+ pageX: number,
43
+ pageY: number,
44
pointerType: PointerType,
88
- screenX: null | number,
89
- screenY: null | number,
45
shiftKey: boolean,
91
- target: Element | Document,
46
+ target: null | Element,
47
timeStamp: number,
48
type: PressEventType,
94
- x: null | number,
95
- y: null | number,
49
+ x: number,
50
+ y: number,
51
|};
52
98
-const hasPointerEvents =
99
- typeof window !== 'undefined' && window.PointerEvent !== undefined;
100
-
101
-const isMac =
102
- typeof window !== 'undefined' && window.navigator != null
103
- ? /^Mac/.test(window.navigator.platform)
104
- : false;
105
-
106
-const DEFAULT_PRESS_RETENTION_OFFSET = {
107
- bottom: 20,
108
- top: 20,
109
- left: 20,
110
- right: 20,
111
-};
112
-
113
-const targetEventTypes = hasPointerEvents
114
- ? ['keydown_active', 'pointerdown', 'click_active']
115
- : ['keydown_active', 'touchstart', 'mousedown', 'click_active'];
116
-
117
-const rootEventTypes = hasPointerEvents
118
- ? ['pointerup', 'pointermove', 'pointercancel', 'click', 'keyup', 'scroll']
119
- : [
120
- 'click',
121
- 'keyup',
122
- 'scroll',
123
- 'mousemove',
124
- 'touchmove',
125
- 'touchcancel',
126
- // Used as a 'cancel' signal for mouse interactions
127
- 'dragstart',
128
- 'mouseup',
129
- 'touchend',
130
- ];
131
-
132
-function isFunction(obj): boolean {
133
- return typeof obj === 'function';
134
-}
135
-
136
-function createPressEvent(
137
- context: ReactDOMResponderContext,
138
- type: PressEventType,
139
- target: Element | Document,
140
- pointerType: PointerType,
141
- event: ?ReactDOMResponderEvent,
142
- touchEvent: null | Touch,
143
- defaultPrevented: boolean,
144
- state: PressState,
145
-): PressEvent {
146
- const timeStamp = context.getTimeStamp();
147
- let clientX = null;
148
- let clientY = null;
149
- let pageX = null;
150
- let pageY = null;
151
- let screenX = null;
152
- let screenY = null;
153
- let altKey = false;
154
- let ctrlKey = false;
155
- let metaKey = false;
156
- let shiftKey = false;
157
-
158
- if (event) {
159
- const nativeEvent = (event.nativeEvent: any);
160
- ({altKey, ctrlKey, metaKey, shiftKey} = nativeEvent);
161
- // Only check for one property, checking for all of them is costly. We can assume
162
- // if clientX exists, so do the rest.
163
- let eventObject;
164
- eventObject = (touchEvent: any) || (nativeEvent: any);
165
- if (eventObject) {
166
- ({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject);
167
- }
168
- }
53
+function createGestureState(e: any, type: PressEventType): PressEvent {
54
return {
170
- altKey,
171
- buttons: state.buttons,
172
- clientX,
173
- clientY,
174
- ctrlKey,
175
- defaultPrevented,
176
- metaKey,
177
- pageX,
178
- pageY,
179
- pointerType,
180
- screenX,
181
- screenY,
182
- shiftKey,
183
- target,
184
- timeStamp,
55
+ altKey: e.altKey,
56
+ buttons: e.buttons,
57
+ ctrlKey: e.ctrlKey,
58
+ defaultPrevented: e.defaultPrevented,
59
+ key: e.key,
60
+ metaKey: e.metaKey,
61
+ pageX: e.pageX,
62
+ pageY: e.pageX,
63
+ pointerType: e.pointerType,
64
+ shiftKey: e.shiftKey,
65
+ target: e.target,
66
+ timeStamp: e.timeStamp,
67
type,
186
- x: clientX,
187
- y: clientY,
68
+ x: e.x,
69
+ y: e.y,
70
};
71
}
72
191
-function dispatchEvent(
192
- event: ?ReactDOMResponderEvent,
193
- listener: any => void,
194
- context: ReactDOMResponderContext,
195
- state: PressState,
196
- name: PressEventType,
197
- eventPriority: EventPriority,
198
-): void {
199
- const target = ((state.pressTarget: any): Element | Document);
200
- const pointerType = state.pointerType;
201
- const defaultPrevented =
202
- (event != null && event.nativeEvent.defaultPrevented === true) ||
203
- (name === 'press' && state.shouldPreventClick);
204
- const touchEvent = state.touchEvent;
205
- const syntheticEvent = createPressEvent(
206
- context,
207
- name,
208
- target,
209
- pointerType,
210
- event,
211
- touchEvent,
212
- defaultPrevented,
213
- state,
214
- );
215
- context.dispatchEvent(syntheticEvent, listener, eventPriority);
216
-}
217
-
218
-function dispatchPressChangeEvent(
219
- context: ReactDOMResponderContext,
220
- props: PressProps,
221
- state: PressState,
222
-): void {
223
- const onPressChange = props.onPressChange;
224
- if (isFunction(onPressChange)) {
225
- const bool = state.isActivePressed;
226
- context.dispatchEvent(bool, onPressChange, DiscreteEvent);
227
- }
228
-}
229
-
230
-function dispatchPressStartEvents(
231
- event: ReactDOMResponderEvent,
232
- context: ReactDOMResponderContext,
233
- props: PressProps,
234
- state: PressState,
235
-): void {
236
- state.isPressed = true;
237
-
238
- if (!state.isActivePressStart) {
239
- state.isActivePressStart = true;
240
- const nativeEvent: any = event.nativeEvent;
241
- const {clientX: x, clientY: y} = state.touchEvent || nativeEvent;
242
- const wasActivePressed = state.isActivePressed;
243
- state.isActivePressed = true;
244
- if (x !== undefined && y !== undefined) {
245
- state.activationPosition = {x, y};
246
- }
247
- const onPressStart = props.onPressStart;
248
-
249
- if (isFunction(onPressStart)) {
250
- dispatchEvent(
251
- event,
252
- onPressStart,
253
- context,
254
- state,
255
- 'pressstart',
256
- DiscreteEvent,
257
- );
258
- }
259
- if (!wasActivePressed) {
260
- dispatchPressChangeEvent(context, props, state);
261
- }
262
- }
263
-}
264
-
265
-function dispatchPressEndEvents(
266
- event: ?ReactDOMResponderEvent,
267
- context: ReactDOMResponderContext,
268
- props: PressProps,
269
- state: PressState,
270
-): void {
271
- state.isActivePressStart = false;
272
- state.isPressed = false;
273
-
274
- if (state.isActivePressed) {
275
- state.isActivePressed = false;
276
- const onPressEnd = props.onPressEnd;
277
-
278
- if (isFunction(onPressEnd)) {
279
- dispatchEvent(
280
- event,
281
- onPressEnd,
282
- context,
283
- state,
284
- 'pressend',
285
- DiscreteEvent,
286
- );
287
- }
288
- dispatchPressChangeEvent(context, props, state);
289
- }
290
-
291
- state.responderRegionOnDeactivation = null;
292
-}
293
-
294
-function dispatchCancel(
295
- event: ReactDOMResponderEvent,
296
- context: ReactDOMResponderContext,
297
- props: PressProps,
298
- state: PressState,
299
-): void {
300
- state.touchEvent = null;
301
- if (state.isPressed) {
302
- state.ignoreEmulatedMouseEvents = false;
303
- dispatchPressEndEvents(event, context, props, state);
304
- }
305
- removeRootEventTypes(context, state);
306
-}
307
-
308
-function isValidKeyboardEvent(nativeEvent: Object): boolean {
309
- const {key, target} = nativeEvent;
310
- const {tagName, isContentEditable} = target;
311
- // Accessibility for keyboards. Space and Enter only.
312
- // "Spacebar" is for IE 11
73
+function isValidKey(e): boolean {
74
+ const {key, target} = e;
75
+ const {tagName, isContentEditable} = (target: any);
76
return (
314
- (key === 'Enter' || key === ' ' || key === 'Spacebar') &&
77
+ (key === 'Enter' || key === ' ') &&
78
(tagName !== 'INPUT' &&
79
tagName !== 'TEXTAREA' &&
80
isContentEditable !== true)
81
);
82
}
83
321
-// TODO: account for touch hit slop
322
-function calculateResponderRegion(
323
- context: ReactDOMResponderContext,
324
- target: Element,
325
- props: PressProps,
326
-) {
327
- const pressRetentionOffset = context.objectAssign(
328
- {},
329
- DEFAULT_PRESS_RETENTION_OFFSET,
330
- props.pressRetentionOffset,
331
- );
332
-
333
- let {left, right, bottom, top} = target.getBoundingClientRect();
334
-
335
- if (pressRetentionOffset) {
336
- if (pressRetentionOffset.bottom != null) {
337
- bottom += pressRetentionOffset.bottom;
338
- }
339
- if (pressRetentionOffset.left != null) {
340
- left -= pressRetentionOffset.left;
341
- }
342
- if (pressRetentionOffset.right != null) {
343
- right += pressRetentionOffset.right;
344
- }
345
- if (pressRetentionOffset.top != null) {
346
- top -= pressRetentionOffset.top;
347
- }
348
- }
349
-
350
- return {
351
- bottom,
352
- top,
353
- left,
354
- right,
355
- };
356
-}
357
-
358
-function getTouchFromPressEvent(nativeEvent: TouchEvent): null | Touch {
359
- const targetTouches = nativeEvent.targetTouches;
360
- if (targetTouches.length > 0) {
361
- return targetTouches[0];
362
- }
363
- return null;
364
-}
365
-
366
-function unmountResponder(
367
- context: ReactDOMResponderContext,
368
- props: PressProps,
369
- state: PressState,
370
-): void {
371
- if (state.isPressed) {
372
- removeRootEventTypes(context, state);
373
- dispatchPressEndEvents(null, context, props, state);
374
- }
375
-}
376
-
377
-function addRootEventTypes(
378
- context: ReactDOMResponderContext,
379
- state: PressState,
380
-): void {
381
- if (!state.addedRootEvents) {
382
- state.addedRootEvents = true;
383
- context.addRootEventTypes(rootEventTypes);
384
- }
385
-}
386
-
387
-function removeRootEventTypes(
388
- context: ReactDOMResponderContext,
389
- state: PressState,
390
-): void {
391
- if (state.addedRootEvents) {
392
- state.addedRootEvents = false;
393
- context.removeRootEventTypes(rootEventTypes);
394
- }
395
-}
396
-
397
-function getTouchById(
398
- nativeEvent: TouchEvent,
399
- pointerId: null | number,
400
-): null | Touch {
401
- const changedTouches = nativeEvent.changedTouches;
402
- for (let i = 0; i < changedTouches.length; i++) {
403
- const touch = changedTouches[i];
404
- if (touch.identifier === pointerId) {
405
- return touch;
406
- }
407
- }
408
- return null;
409
-}
410
-
411
-function getTouchTarget(context: ReactDOMResponderContext, touchEvent: Touch) {
412
- const doc = context.getActiveDocument();
413
- return doc.elementFromPoint(touchEvent.clientX, touchEvent.clientY);
414
-}
415
-
416
-function updateIsPressWithinResponderRegion(
417
- nativeEventOrTouchEvent: Event | Touch,
418
- context: ReactDOMResponderContext,
419
- props: PressProps,
420
- state: PressState,
421
-): void {
422
- // Calculate the responder region we use for deactivation if not
423
- // already done during move event.
424
- if (state.responderRegionOnDeactivation == null) {
425
- state.responderRegionOnDeactivation = calculateResponderRegion(
426
- context,
427
- ((state.pressTarget: any): Element),
428
- props,
429
- );
430
- }
431
- const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
432
- let left, top, right, bottom;
433
-
434
- if (responderRegionOnActivation != null) {
435
- left = responderRegionOnActivation.left;
436
- top = responderRegionOnActivation.top;
437
- right = responderRegionOnActivation.right;
438
- bottom = responderRegionOnActivation.bottom;
439
-
440
- if (responderRegionOnDeactivation != null) {
441
- left = Math.min(left, responderRegionOnDeactivation.left);
442
- top = Math.min(top, responderRegionOnDeactivation.top);
443
- right = Math.max(right, responderRegionOnDeactivation.right);
444
- bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
445
- }
446
- }
447
- const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any);
448
-
449
- state.isPressWithinResponderRegion =
450
- left != null &&
451
- right != null &&
452
- top != null &&
453
- bottom != null &&
454
- x !== null &&
455
- y !== null &&
456
- (x >= left && x <= right && y >= top && y <= bottom);
457
-}
458
-
459
-// After some investigation work, screen reader virtual
460
-// clicks (NVDA, Jaws, VoiceOver) do not have co-ords associated with the click
461
-// event and "detail" is always 0 (where normal clicks are > 0)
462
-function isScreenReaderVirtualClick(nativeEvent): boolean {
463
- return (
464
- nativeEvent.detail === 0 &&
465
- nativeEvent.screenX === 0 &&
466
- nativeEvent.screenY === 0 &&
467
- nativeEvent.clientX === 0 &&
468
- nativeEvent.clientY === 0
469
- );
470
-}
471
-
472
-function targetIsDocument(target: null | Node): boolean {
473
- // When target is null, it is the root
474
- return target === null || target.nodeType === 9;
475
-}
476
-
477
-const pressResponderImpl = {
478
- targetEventTypes,
479
- getInitialState(): PressState {
480
- return {
481
- activationPosition: null,
482
- addedRootEvents: false,
483
- buttons: 0,
484
- isActivePressed: false,
485
- isActivePressStart: false,
486
- isPressed: false,
487
- isPressWithinResponderRegion: true,
488
- pointerType: '',
489
- pressTarget: null,
490
- responderRegionOnActivation: null,
491
- responderRegionOnDeactivation: null,
492
- ignoreEmulatedMouseEvents: false,
493
- activePointerId: null,
494
- shouldPreventClick: false,
495
- touchEvent: null,
496
- };
497
- },
498
- onEvent(
499
- event: ReactDOMResponderEvent,
500
- context: ReactDOMResponderContext,
501
- props: PressProps,
502
- state: PressState,
503
- ): void {
504
- const {pointerType, type} = event;
505
-
506
- if (props.disabled) {
507
- removeRootEventTypes(context, state);
508
- dispatchPressEndEvents(event, context, props, state);
509
- state.ignoreEmulatedMouseEvents = false;
510
- return;
511
- }
512
- const nativeEvent: any = event.nativeEvent;
513
- const isPressed = state.isPressed;
514
-
515
- switch (type) {
516
- // START
517
- case 'pointerdown':
518
- case 'keydown':
519
- case 'mousedown':
520
- case 'touchstart': {
521
- if (!isPressed) {
522
- const isTouchEvent = type === 'touchstart';
523
- const isPointerEvent = type === 'pointerdown';
524
- const isKeyboardEvent = pointerType === 'keyboard';
525
- const isMouseEvent = pointerType === 'mouse';
526
-
527
- // Ignore emulated mouse events
528
- if (type === 'mousedown' && state.ignoreEmulatedMouseEvents) {
529
- return;
530
- }
531
-
532
- state.shouldPreventClick = false;
533
- if (isTouchEvent) {
534
- state.ignoreEmulatedMouseEvents = true;
535
- } else if (isKeyboardEvent) {
536
- // Ignore unrelated key events
537
- if (isValidKeyboardEvent(nativeEvent)) {
538
- const {
539
- altKey,
540
- ctrlKey,
541
- metaKey,
542
- shiftKey,
543
- } = (nativeEvent: MouseEvent);
544
- if (nativeEvent.key === ' ') {
545
- nativeEvent.preventDefault();
546
- } else if (
547
- props.preventDefault !== false &&
548
- !shiftKey &&
549
- !metaKey &&
550
- !ctrlKey &&
551
- !altKey
552
- ) {
553
- state.shouldPreventClick = true;
554
- }
555
- } else {
556
- return;
557
- }
558
- }
559
-
560
- // We set these here, before the button check so we have this
561
- // data around for handling of the context menu
562
- state.pointerType = pointerType;
563
- const pressTarget = (state.pressTarget = context.getResponderNode());
564
- if (isPointerEvent) {
565
- state.activePointerId = nativeEvent.pointerId;
566
- } else if (isTouchEvent) {
567
- const touchEvent = getTouchFromPressEvent(nativeEvent);
568
- if (touchEvent === null) {
569
- return;
570
- }
571
- state.touchEvent = touchEvent;
572
- state.activePointerId = touchEvent.identifier;
573
- }
574
-
575
- // Ignore any device buttons except primary/middle and touch/pen contact.
576
- // Additionally we ignore primary-button + ctrl-key with Macs as that
577
- // acts like right-click and opens the contextmenu.
578
- if (
579
- nativeEvent.buttons === 2 ||
580
- nativeEvent.buttons > 4 ||
581
- (isMac && isMouseEvent && nativeEvent.ctrlKey)
582
- ) {
583
- return;
584
- }
585
- // Exclude document targets
586
- if (!targetIsDocument(pressTarget)) {
587
- state.responderRegionOnActivation = calculateResponderRegion(
588
- context,
589
- ((pressTarget: any): Element),
590
- props,
591
- );
592
- }
593
- state.responderRegionOnDeactivation = null;
594
- state.isPressWithinResponderRegion = true;
595
- state.buttons = nativeEvent.buttons;
596
- dispatchPressStartEvents(event, context, props, state);
597
- addRootEventTypes(context, state);
598
- } else {
599
- // Prevent spacebar press from scrolling the window
600
- if (isValidKeyboardEvent(nativeEvent) && nativeEvent.key === ' ') {
601
- nativeEvent.preventDefault();
602
- }
84
+/**
85
+ * The lack of built-in composition for gesture responders means we have to
86
+ * selectively ignore callbacks from useKeyboard or useTap if the other is
87
+ * active.
88
+ */
89
+export function usePress(props: PressProps) {
90
+ const safeProps = props || emptyObject;
91
+ const {
92
+ disabled,
93
+ preventDefault,
94
+ onPress,
95
+ onPressChange,
96
+ onPressEnd,
97
+ onPressMove,
98
+ onPressStart,
99
+ } = safeProps;
100
+
101
+ const [active, updateActive] = React.useState(null);
102
+
103
+ const tap = useTap({
104
+ disabled: disabled || active === 'keyboard',
105
+ preventDefault,
106
+ onTapStart(e) {
107
+ if (active == null) {
108
+ updateActive('tap');
109
+ if (onPressStart != null) {
110
+ onPressStart(createGestureState(e, 'pressstart'));
111
}
604
- break;
112
}
606
-
607
- case 'click': {
608
- if (state.shouldPreventClick) {
609
- nativeEvent.preventDefault();
113
+ },
114
+ onTapChange: onPressChange,
115
+ onTapUpdate(e) {
116
+ if (active === 'tap') {
117
+ if (onPressMove != null) {
118
+ onPressMove(createGestureState(e, 'pressmove'));
119
}
611
- const onPress = props.onPress;
612
-
613
- if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) {
614
- state.pointerType = 'keyboard';
615
- state.pressTarget = context.getResponderNode();
616
- const preventDefault = props.preventDefault;
617
-
618
- if (preventDefault !== false) {
619
- nativeEvent.preventDefault();
620
- }
621
- dispatchEvent(event, onPress, context, state, 'press', DiscreteEvent);
622
- }
623
- break;
120
}
625
- }
626
- },
627
- onRootEvent(
628
- event: ReactDOMResponderEvent,
629
- context: ReactDOMResponderContext,
630
- props: PressProps,
631
- state: PressState,
632
- ): void {
633
- let {pointerType, target, type} = event;
634
-
635
- const nativeEvent: any = event.nativeEvent;
636
- const isPressed = state.isPressed;
637
- const activePointerId = state.activePointerId;
638
- const previousPointerType = state.pointerType;
639
-
640
- switch (type) {
641
- // MOVE
642
- case 'pointermove':
643
- case 'mousemove':
644
- case 'touchmove': {
645
- let touchEvent;
646
- // Ignore emulated events (pointermove will dispatch touch and mouse events)
647
- // Ignore pointermove events during a keyboard press.
648
- if (previousPointerType !== pointerType) {
649
- return;
650
- }
651
- if (
652
- type === 'pointermove' &&
653
- activePointerId !== nativeEvent.pointerId
654
- ) {
655
- return;
656
- } else if (type === 'touchmove') {
657
- touchEvent = getTouchById(nativeEvent, activePointerId);
658
- if (touchEvent === null) {
659
- return;
660
- }
661
- state.touchEvent = touchEvent;
121
+ },
122
+ onTapEnd(e) {
123
+ if (active === 'tap') {
124
+ if (onPressEnd != null) {
125
+ onPressEnd(createGestureState(e, 'pressend'));
126
}
663
- const pressTarget = state.pressTarget;
664
-
665
- if (pressTarget !== null && !targetIsDocument(pressTarget)) {
666
- if (
667
- pointerType === 'mouse' &&
668
- context.isTargetWithinNode(target, pressTarget)
669
- ) {
670
- state.isPressWithinResponderRegion = true;
671
- } else {
672
- // Calculate the responder region we use for deactivation, as the
673
- // element dimensions may have changed since activation.
674
- updateIsPressWithinResponderRegion(
675
- touchEvent || nativeEvent,
676
- context,
677
- props,
678
- state,
679
- );
680
- }
681
- }
682
-
683
- if (state.isPressWithinResponderRegion) {
684
- if (isPressed) {
685
- const onPressMove = props.onPressMove;
686
-
687
- if (isFunction(onPressMove)) {
688
- dispatchEvent(
689
- event,
690
- onPressMove,
691
- context,
692
- state,
693
- 'pressmove',
694
- UserBlockingEvent,
695
- );
696
- }
697
- } else {
698
- dispatchPressStartEvents(event, context, props, state);
699
- }
700
- } else {
701
- dispatchPressEndEvents(event, context, props, state);
127
+ if (onPress != null && e.buttons !== 4) {
128
+ onPress(createGestureState(e, 'press'));
129
}
703
- break;
130
+ updateActive(null);
131
}
705
-
706
- // END
707
- case 'pointerup':
708
- case 'keyup':
709
- case 'mouseup':
710
- case 'touchend': {
711
- if (isPressed) {
712
- const buttons = state.buttons;
713
- let isKeyboardEvent = false;
714
- let touchEvent;
715
- if (
716
- type === 'pointerup' &&
717
- activePointerId !== nativeEvent.pointerId
718
- ) {
719
- return;
720
- } else if (type === 'touchend') {
721
- touchEvent = getTouchById(nativeEvent, activePointerId);
722
- if (touchEvent === null) {
723
- return;
724
- }
725
- state.touchEvent = touchEvent;
726
- target = getTouchTarget(context, touchEvent);
727
- } else if (type === 'keyup') {
728
- // Ignore unrelated keyboard events
729
- if (!isValidKeyboardEvent(nativeEvent)) {
730
- return;
731
- }
732
- isKeyboardEvent = true;
733
- removeRootEventTypes(context, state);
734
- } else if (buttons === 4) {
735
- // Remove the root events here as no 'click' event is dispatched when this 'button' is pressed.
736
- removeRootEventTypes(context, state);
737
- }
738
-
739
- // Determine whether to call preventDefault on subsequent native events.
740
- if (
741
- context.isTargetWithinResponder(target) &&
742
- context.isTargetWithinHostComponent(target, 'a')
743
- ) {
744
- const {
745
- altKey,
746
- ctrlKey,
747
- metaKey,
748
- shiftKey,
749
- } = (nativeEvent: MouseEvent);
750
- // Check "open in new window/tab" and "open context menu" key modifiers
751
- const preventDefault = props.preventDefault;
752
-
753
- if (
754
- preventDefault !== false &&
755
- !shiftKey &&
756
- !metaKey &&
757
- !ctrlKey &&
758
- !altKey
759
- ) {
760
- state.shouldPreventClick = true;
761
- }
762
- }
763
-
764
- const pressTarget = state.pressTarget;
765
- dispatchPressEndEvents(event, context, props, state);
766
- const onPress = props.onPress;
767
-
768
- if (pressTarget !== null && isFunction(onPress)) {
769
- if (
770
- !isKeyboardEvent &&
771
- pressTarget !== null &&
772
- !targetIsDocument(pressTarget)
773
- ) {
774
- if (
775
- pointerType === 'mouse' &&
776
- context.isTargetWithinNode(target, pressTarget)
777
- ) {
778
- state.isPressWithinResponderRegion = true;
779
- } else {
780
- // If the event target isn't within the press target, check if we're still
781
- // within the responder region. The region may have changed if the
782
- // element's layout was modified after activation.
783
- updateIsPressWithinResponderRegion(
784
- touchEvent || nativeEvent,
785
- context,
786
- props,
787
- state,
788
- );
789
- }
790
- }
791
-
792
- if (state.isPressWithinResponderRegion && buttons !== 4) {
793
- dispatchEvent(
794
- event,
795
- onPress,
796
- context,
797
- state,
798
- 'press',
799
- DiscreteEvent,
800
- );
801
- }
802
- }
803
- state.touchEvent = null;
804
- } else if (type === 'mouseup') {
805
- state.ignoreEmulatedMouseEvents = false;
132
+ },
133
+ onTapCancel(e) {
134
+ if (active === 'tap') {
135
+ if (onPressEnd != null) {
136
+ onPressEnd(createGestureState(e, 'pressend'));
137
}
807
- break;
138
+ updateActive(null);
139
}
809
-
810
- case 'click': {
811
- // "keyup" occurs after "click"
812
- if (previousPointerType !== 'keyboard') {
813
- removeRootEventTypes(context, state);
814
- }
815
- break;
140
+ },
141
+ });
142
+
143
+ const keyboard = useKeyboard({
144
+ disabled: disabled || active === 'tap',
145
+ preventClick: preventDefault !== false,
146
+ preventKeys: preventDefault !== false ? [' ', 'Enter'] : [],
147
+ onClick(e) {
148
+ if (active == null && onPress != null) {
149
+ onPress(createGestureState(e, 'press'));
150
}
817
-
818
- // CANCEL
819
- case 'scroll': {
820
- // We ignore incoming scroll events when using mouse events
821
- if (previousPointerType === 'mouse') {
822
- return;
151
+ },
152
+ onKeyDown(e) {
153
+ if (active == null && isValidKey(e)) {
154
+ updateActive('keyboard');
155
+ if (onPressStart != null) {
156
+ onPressStart(createGestureState(e, 'pressstart'));
157
}
824
- const pressTarget = state.pressTarget;
825
- const scrollTarget = nativeEvent.target;
826
- const doc = context.getActiveDocument();
827
- // If the scroll target is the document or if the press target
828
- // is inside the scroll target, then this a scroll that should
829
- // trigger a cancel.
830
- if (
831
- pressTarget !== null &&
832
- (scrollTarget === doc ||
833
- context.isTargetWithinNode(pressTarget, scrollTarget))
834
- ) {
835
- dispatchCancel(event, context, props, state);
158
+ if (onPressChange != null) {
159
+ onPressChange(true);
160
}
837
- break;
161
+ // stop propagation
162
+ return false;
163
}
839
- case 'pointercancel':
840
- case 'touchcancel':
841
- case 'dragstart': {
842
- dispatchCancel(event, context, props, state);
164
+ },
165
+ onKeyUp(e) {
166
+ if (active === 'keyboard' && isValidKey(e)) {
167
+ if (onPressChange != null) {
168
+ onPressChange(false);
169
+ }
170
+ if (onPressEnd != null) {
171
+ onPressEnd(createGestureState(e, 'pressend'));
172
+ }
173
+ if (onPress != null) {
174
+ onPress(createGestureState(e, 'press'));
175
+ }
176
+ updateActive(null);
177
+ // stop propagation
178
+ return false;
179
}
844
- }
845
- },
846
- onUnmount(
847
- context: ReactDOMResponderContext,
848
- props: PressProps,
849
- state: PressState,
850
- ) {
851
- unmountResponder(context, props, state);
852
- },
853
-};
854
-
855
-export const PressResponder = React.unstable_createResponder(
856
- 'Press',
857
- pressResponderImpl,
858
-);
180
+ },
181
+ });
182
860
-export function usePress(
861
- props: PressProps,
862
-): ReactEventResponderListener<any, any> {
863
- return React.unstable_useResponder(PressResponder, props);
183
+ return [tap, keyboard];
184
}
packages/react-ui/events/src/dom/PressLegacy.js
new
+864
@@ -0,0 +1,864 @@
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
+ ReactDOMResponderEvent,
12
+ ReactDOMResponderContext,
13
+ PointerType,
14
+} from 'shared/ReactDOMTypes';
15
+import type {
16
+ EventPriority,
17
+ ReactEventResponderListener,
18
+} from 'shared/ReactTypes';
19
+
20
+import React from 'react';
21
+import {DiscreteEvent, UserBlockingEvent} from 'shared/ReactTypes';
22
+
23
+type PressProps = {|
24
+ disabled: boolean,
25
+ pressRetentionOffset: {
26
+ top: number,
27
+ right: number,
28
+ bottom: number,
29
+ left: number,
30
+ },
31
+ preventDefault: boolean,
32
+ onPress: (e: PressEvent) => void,
33
+ onPressChange: boolean => void,
34
+ onPressEnd: (e: PressEvent) => void,
35
+ onPressMove: (e: PressEvent) => void,
36
+ onPressStart: (e: PressEvent) => void,
37
+|};
38
+
39
+type PressState = {
40
+ activationPosition: null | $ReadOnly<{|
41
+ x: number,
42
+ y: number,
43
+ |}>,
44
+ addedRootEvents: boolean,
45
+ buttons: 0 | 1 | 4,
46
+ isActivePressed: boolean,
47
+ isActivePressStart: boolean,
48
+ isPressed: boolean,
49
+ isPressWithinResponderRegion: boolean,
50
+ pointerType: PointerType,
51
+ pressTarget: null | Element | Document,
52
+ responderRegionOnActivation: null | $ReadOnly<{|
53
+ bottom: number,
54
+ left: number,
55
+ right: number,
56
+ top: number,
57
+ |}>,
58
+ responderRegionOnDeactivation: null | $ReadOnly<{|
59
+ bottom: number,
60
+ left: number,
61
+ right: number,
62
+ top: number,
63
+ |}>,
64
+ ignoreEmulatedMouseEvents: boolean,
65
+ activePointerId: null | number,
66
+ shouldPreventClick: boolean,
67
+ touchEvent: null | Touch,
68
+};
69
+
70
+type PressEventType =
71
+ | 'press'
72
+ | 'pressmove'
73
+ | 'pressstart'
74
+ | 'pressend'
75
+ | 'presschange';
76
+
77
+type PressEvent = {|
78
+ altKey: boolean,
79
+ buttons: 0 | 1 | 4,
80
+ clientX: null | number,
81
+ clientY: null | number,
82
+ ctrlKey: boolean,
83
+ defaultPrevented: boolean,
84
+ metaKey: boolean,
85
+ pageX: null | number,
86
+ pageY: null | number,
87
+ pointerType: PointerType,
88
+ screenX: null | number,
89
+ screenY: null | number,
90
+ shiftKey: boolean,
91
+ target: Element | Document,
92
+ timeStamp: number,
93
+ type: PressEventType,
94
+ x: null | number,
95
+ y: null | number,
96
+|};
97
+
98
+const hasPointerEvents =
99
+ typeof window !== 'undefined' && window.PointerEvent !== undefined;
100
+
101
+const isMac =
102
+ typeof window !== 'undefined' && window.navigator != null
103
+ ? /^Mac/.test(window.navigator.platform)
104
+ : false;
105
+
106
+const DEFAULT_PRESS_RETENTION_OFFSET = {
107
+ bottom: 20,
108
+ top: 20,
109
+ left: 20,
110
+ right: 20,
111
+};
112
+
113
+const targetEventTypes = hasPointerEvents
114
+ ? ['keydown_active', 'pointerdown', 'click_active']
115
+ : ['keydown_active', 'touchstart', 'mousedown', 'click_active'];
116
+
117
+const rootEventTypes = hasPointerEvents
118
+ ? ['pointerup', 'pointermove', 'pointercancel', 'click', 'keyup', 'scroll']
119
+ : [
120
+ 'click',
121
+ 'keyup',
122
+ 'scroll',
123
+ 'mousemove',
124
+ 'touchmove',
125
+ 'touchcancel',
126
+ // Used as a 'cancel' signal for mouse interactions
127
+ 'dragstart',
128
+ 'mouseup',
129
+ 'touchend',
130
+ ];
131
+
132
+function isFunction(obj): boolean {
133
+ return typeof obj === 'function';
134
+}
135
+
136
+function createPressEvent(
137
+ context: ReactDOMResponderContext,
138
+ type: PressEventType,
139
+ target: Element | Document,
140
+ pointerType: PointerType,
141
+ event: ?ReactDOMResponderEvent,
142
+ touchEvent: null | Touch,
143
+ defaultPrevented: boolean,
144
+ state: PressState,
145
+): PressEvent {
146
+ const timeStamp = context.getTimeStamp();
147
+ let clientX = null;
148
+ let clientY = null;
149
+ let pageX = null;
150
+ let pageY = null;
151
+ let screenX = null;
152
+ let screenY = null;
153
+ let altKey = false;
154
+ let ctrlKey = false;
155
+ let metaKey = false;
156
+ let shiftKey = false;
157
+
158
+ if (event) {
159
+ const nativeEvent = (event.nativeEvent: any);
160
+ ({altKey, ctrlKey, metaKey, shiftKey} = nativeEvent);
161
+ // Only check for one property, checking for all of them is costly. We can assume
162
+ // if clientX exists, so do the rest.
163
+ let eventObject;
164
+ eventObject = (touchEvent: any) || (nativeEvent: any);
165
+ if (eventObject) {
166
+ ({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject);
167
+ }
168
+ }
169
+ return {
170
+ altKey,
171
+ buttons: state.buttons,
172
+ clientX,
173
+ clientY,
174
+ ctrlKey,
175
+ defaultPrevented,
176
+ metaKey,
177
+ pageX,
178
+ pageY,
179
+ pointerType,
180
+ screenX,
181
+ screenY,
182
+ shiftKey,
183
+ target,
184
+ timeStamp,
185
+ type,
186
+ x: clientX,
187
+ y: clientY,
188
+ };
189
+}
190
+
191
+function dispatchEvent(
192
+ event: ?ReactDOMResponderEvent,
193
+ listener: any => void,
194
+ context: ReactDOMResponderContext,
195
+ state: PressState,
196
+ name: PressEventType,
197
+ eventPriority: EventPriority,
198
+): void {
199
+ const target = ((state.pressTarget: any): Element | Document);
200
+ const pointerType = state.pointerType;
201
+ const defaultPrevented =
202
+ (event != null && event.nativeEvent.defaultPrevented === true) ||
203
+ (name === 'press' && state.shouldPreventClick);
204
+ const touchEvent = state.touchEvent;
205
+ const syntheticEvent = createPressEvent(
206
+ context,
207
+ name,
208
+ target,
209
+ pointerType,
210
+ event,
211
+ touchEvent,
212
+ defaultPrevented,
213
+ state,
214
+ );
215
+ context.dispatchEvent(syntheticEvent, listener, eventPriority);
216
+}
217
+
218
+function dispatchPressChangeEvent(
219
+ context: ReactDOMResponderContext,
220
+ props: PressProps,
221
+ state: PressState,
222
+): void {
223
+ const onPressChange = props.onPressChange;
224
+ if (isFunction(onPressChange)) {
225
+ const bool = state.isActivePressed;
226
+ context.dispatchEvent(bool, onPressChange, DiscreteEvent);
227
+ }
228
+}
229
+
230
+function dispatchPressStartEvents(
231
+ event: ReactDOMResponderEvent,
232
+ context: ReactDOMResponderContext,
233
+ props: PressProps,
234
+ state: PressState,
235
+): void {
236
+ state.isPressed = true;
237
+
238
+ if (!state.isActivePressStart) {
239
+ state.isActivePressStart = true;
240
+ const nativeEvent: any = event.nativeEvent;
241
+ const {clientX: x, clientY: y} = state.touchEvent || nativeEvent;
242
+ const wasActivePressed = state.isActivePressed;
243
+ state.isActivePressed = true;
244
+ if (x !== undefined && y !== undefined) {
245
+ state.activationPosition = {x, y};
246
+ }
247
+ const onPressStart = props.onPressStart;
248
+
249
+ if (isFunction(onPressStart)) {
250
+ dispatchEvent(
251
+ event,
252
+ onPressStart,
253
+ context,
254
+ state,
255
+ 'pressstart',
256
+ DiscreteEvent,
257
+ );
258
+ }
259
+ if (!wasActivePressed) {
260
+ dispatchPressChangeEvent(context, props, state);
261
+ }
262
+ }
263
+}
264
+
265
+function dispatchPressEndEvents(
266
+ event: ?ReactDOMResponderEvent,
267
+ context: ReactDOMResponderContext,
268
+ props: PressProps,
269
+ state: PressState,
270
+): void {
271
+ state.isActivePressStart = false;
272
+ state.isPressed = false;
273
+
274
+ if (state.isActivePressed) {
275
+ state.isActivePressed = false;
276
+ const onPressEnd = props.onPressEnd;
277
+
278
+ if (isFunction(onPressEnd)) {
279
+ dispatchEvent(
280
+ event,
281
+ onPressEnd,
282
+ context,
283
+ state,
284
+ 'pressend',
285
+ DiscreteEvent,
286
+ );
287
+ }
288
+ dispatchPressChangeEvent(context, props, state);
289
+ }
290
+
291
+ state.responderRegionOnDeactivation = null;
292
+}
293
+
294
+function dispatchCancel(
295
+ event: ReactDOMResponderEvent,
296
+ context: ReactDOMResponderContext,
297
+ props: PressProps,
298
+ state: PressState,
299
+): void {
300
+ state.touchEvent = null;
301
+ if (state.isPressed) {
302
+ state.ignoreEmulatedMouseEvents = false;
303
+ dispatchPressEndEvents(event, context, props, state);
304
+ }
305
+ removeRootEventTypes(context, state);
306
+}
307
+
308
+function isValidKeyboardEvent(nativeEvent: Object): boolean {
309
+ const {key, target} = nativeEvent;
310
+ const {tagName, isContentEditable} = target;
311
+ // Accessibility for keyboards. Space and Enter only.
312
+ // "Spacebar" is for IE 11
313
+ return (
314
+ (key === 'Enter' || key === ' ' || key === 'Spacebar') &&
315
+ (tagName !== 'INPUT' &&
316
+ tagName !== 'TEXTAREA' &&
317
+ isContentEditable !== true)
318
+ );
319
+}
320
+
321
+// TODO: account for touch hit slop
322
+function calculateResponderRegion(
323
+ context: ReactDOMResponderContext,
324
+ target: Element,
325
+ props: PressProps,
326
+) {
327
+ const pressRetentionOffset = context.objectAssign(
328
+ {},
329
+ DEFAULT_PRESS_RETENTION_OFFSET,
330
+ props.pressRetentionOffset,
331
+ );
332
+
333
+ let {left, right, bottom, top} = target.getBoundingClientRect();
334
+
335
+ if (pressRetentionOffset) {
336
+ if (pressRetentionOffset.bottom != null) {
337
+ bottom += pressRetentionOffset.bottom;
338
+ }
339
+ if (pressRetentionOffset.left != null) {
340
+ left -= pressRetentionOffset.left;
341
+ }
342
+ if (pressRetentionOffset.right != null) {
343
+ right += pressRetentionOffset.right;
344
+ }
345
+ if (pressRetentionOffset.top != null) {
346
+ top -= pressRetentionOffset.top;
347
+ }
348
+ }
349
+
350
+ return {
351
+ bottom,
352
+ top,
353
+ left,
354
+ right,
355
+ };
356
+}
357
+
358
+function getTouchFromPressEvent(nativeEvent: TouchEvent): null | Touch {
359
+ const targetTouches = nativeEvent.targetTouches;
360
+ if (targetTouches.length > 0) {
361
+ return targetTouches[0];
362
+ }
363
+ return null;
364
+}
365
+
366
+function unmountResponder(
367
+ context: ReactDOMResponderContext,
368
+ props: PressProps,
369
+ state: PressState,
370
+): void {
371
+ if (state.isPressed) {
372
+ removeRootEventTypes(context, state);
373
+ dispatchPressEndEvents(null, context, props, state);
374
+ }
375
+}
376
+
377
+function addRootEventTypes(
378
+ context: ReactDOMResponderContext,
379
+ state: PressState,
380
+): void {
381
+ if (!state.addedRootEvents) {
382
+ state.addedRootEvents = true;
383
+ context.addRootEventTypes(rootEventTypes);
384
+ }
385
+}
386
+
387
+function removeRootEventTypes(
388
+ context: ReactDOMResponderContext,
389
+ state: PressState,
390
+): void {
391
+ if (state.addedRootEvents) {
392
+ state.addedRootEvents = false;
393
+ context.removeRootEventTypes(rootEventTypes);
394
+ }
395
+}
396
+
397
+function getTouchById(
398
+ nativeEvent: TouchEvent,
399
+ pointerId: null | number,
400
+): null | Touch {
401
+ const changedTouches = nativeEvent.changedTouches;
402
+ for (let i = 0; i < changedTouches.length; i++) {
403
+ const touch = changedTouches[i];
404
+ if (touch.identifier === pointerId) {
405
+ return touch;
406
+ }
407
+ }
408
+ return null;
409
+}
410
+
411
+function getTouchTarget(context: ReactDOMResponderContext, touchEvent: Touch) {
412
+ const doc = context.getActiveDocument();
413
+ return doc.elementFromPoint(touchEvent.clientX, touchEvent.clientY);
414
+}
415
+
416
+function updateIsPressWithinResponderRegion(
417
+ nativeEventOrTouchEvent: Event | Touch,
418
+ context: ReactDOMResponderContext,
419
+ props: PressProps,
420
+ state: PressState,
421
+): void {
422
+ // Calculate the responder region we use for deactivation if not
423
+ // already done during move event.
424
+ if (state.responderRegionOnDeactivation == null) {
425
+ state.responderRegionOnDeactivation = calculateResponderRegion(
426
+ context,
427
+ ((state.pressTarget: any): Element),
428
+ props,
429
+ );
430
+ }
431
+ const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
432
+ let left, top, right, bottom;
433
+
434
+ if (responderRegionOnActivation != null) {
435
+ left = responderRegionOnActivation.left;
436
+ top = responderRegionOnActivation.top;
437
+ right = responderRegionOnActivation.right;
438
+ bottom = responderRegionOnActivation.bottom;
439
+
440
+ if (responderRegionOnDeactivation != null) {
441
+ left = Math.min(left, responderRegionOnDeactivation.left);
442
+ top = Math.min(top, responderRegionOnDeactivation.top);
443
+ right = Math.max(right, responderRegionOnDeactivation.right);
444
+ bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
445
+ }
446
+ }
447
+ const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any);
448
+
449
+ state.isPressWithinResponderRegion =
450
+ left != null &&
451
+ right != null &&
452
+ top != null &&
453
+ bottom != null &&
454
+ x !== null &&
455
+ y !== null &&
456
+ (x >= left && x <= right && y >= top && y <= bottom);
457
+}
458
+
459
+// After some investigation work, screen reader virtual
460
+// clicks (NVDA, Jaws, VoiceOver) do not have co-ords associated with the click
461
+// event and "detail" is always 0 (where normal clicks are > 0)
462
+function isScreenReaderVirtualClick(nativeEvent): boolean {
463
+ return (
464
+ nativeEvent.detail === 0 &&
465
+ nativeEvent.screenX === 0 &&
466
+ nativeEvent.screenY === 0 &&
467
+ nativeEvent.clientX === 0 &&
468
+ nativeEvent.clientY === 0
469
+ );
470
+}
471
+
472
+function targetIsDocument(target: null | Node): boolean {
473
+ // When target is null, it is the root
474
+ return target === null || target.nodeType === 9;
475
+}
476
+
477
+const pressResponderImpl = {
478
+ targetEventTypes,
479
+ getInitialState(): PressState {
480
+ return {
481
+ activationPosition: null,
482
+ addedRootEvents: false,
483
+ buttons: 0,
484
+ isActivePressed: false,
485
+ isActivePressStart: false,
486
+ isPressed: false,
487
+ isPressWithinResponderRegion: true,
488
+ pointerType: '',
489
+ pressTarget: null,
490
+ responderRegionOnActivation: null,
491
+ responderRegionOnDeactivation: null,
492
+ ignoreEmulatedMouseEvents: false,
493
+ activePointerId: null,
494
+ shouldPreventClick: false,
495
+ touchEvent: null,
496
+ };
497
+ },
498
+ onEvent(
499
+ event: ReactDOMResponderEvent,
500
+ context: ReactDOMResponderContext,
501
+ props: PressProps,
502
+ state: PressState,
503
+ ): void {
504
+ const {pointerType, type} = event;
505
+
506
+ if (props.disabled) {
507
+ removeRootEventTypes(context, state);
508
+ dispatchPressEndEvents(event, context, props, state);
509
+ state.ignoreEmulatedMouseEvents = false;
510
+ return;
511
+ }
512
+ const nativeEvent: any = event.nativeEvent;
513
+ const isPressed = state.isPressed;
514
+
515
+ switch (type) {
516
+ // START
517
+ case 'pointerdown':
518
+ case 'keydown':
519
+ case 'mousedown':
520
+ case 'touchstart': {
521
+ if (!isPressed) {
522
+ const isTouchEvent = type === 'touchstart';
523
+ const isPointerEvent = type === 'pointerdown';
524
+ const isKeyboardEvent = pointerType === 'keyboard';
525
+ const isMouseEvent = pointerType === 'mouse';
526
+
527
+ // Ignore emulated mouse events
528
+ if (type === 'mousedown' && state.ignoreEmulatedMouseEvents) {
529
+ return;
530
+ }
531
+
532
+ state.shouldPreventClick = false;
533
+ if (isTouchEvent) {
534
+ state.ignoreEmulatedMouseEvents = true;
535
+ } else if (isKeyboardEvent) {
536
+ // Ignore unrelated key events
537
+ if (isValidKeyboardEvent(nativeEvent)) {
538
+ const {
539
+ altKey,
540
+ ctrlKey,
541
+ metaKey,
542
+ shiftKey,
543
+ } = (nativeEvent: MouseEvent);
544
+ if (nativeEvent.key === ' ') {
545
+ nativeEvent.preventDefault();
546
+ } else if (
547
+ props.preventDefault !== false &&
548
+ !shiftKey &&
549
+ !metaKey &&
550
+ !ctrlKey &&
551
+ !altKey
552
+ ) {
553
+ state.shouldPreventClick = true;
554
+ }
555
+ } else {
556
+ return;
557
+ }
558
+ }
559
+
560
+ // We set these here, before the button check so we have this
561
+ // data around for handling of the context menu
562
+ state.pointerType = pointerType;
563
+ const pressTarget = (state.pressTarget = context.getResponderNode());
564
+ if (isPointerEvent) {
565
+ state.activePointerId = nativeEvent.pointerId;
566
+ } else if (isTouchEvent) {
567
+ const touchEvent = getTouchFromPressEvent(nativeEvent);
568
+ if (touchEvent === null) {
569
+ return;
570
+ }
571
+ state.touchEvent = touchEvent;
572
+ state.activePointerId = touchEvent.identifier;
573
+ }
574
+
575
+ // Ignore any device buttons except primary/middle and touch/pen contact.
576
+ // Additionally we ignore primary-button + ctrl-key with Macs as that
577
+ // acts like right-click and opens the contextmenu.
578
+ if (
579
+ nativeEvent.buttons === 2 ||
580
+ nativeEvent.buttons > 4 ||
581
+ (isMac && isMouseEvent && nativeEvent.ctrlKey)
582
+ ) {
583
+ return;
584
+ }
585
+ // Exclude document targets
586
+ if (!targetIsDocument(pressTarget)) {
587
+ state.responderRegionOnActivation = calculateResponderRegion(
588
+ context,
589
+ ((pressTarget: any): Element),
590
+ props,
591
+ );
592
+ }
593
+ state.responderRegionOnDeactivation = null;
594
+ state.isPressWithinResponderRegion = true;
595
+ state.buttons = nativeEvent.buttons;
596
+ dispatchPressStartEvents(event, context, props, state);
597
+ addRootEventTypes(context, state);
598
+ } else {
599
+ // Prevent spacebar press from scrolling the window
600
+ if (isValidKeyboardEvent(nativeEvent) && nativeEvent.key === ' ') {
601
+ nativeEvent.preventDefault();
602
+ }
603
+ }
604
+ break;
605
+ }
606
+
607
+ case 'click': {
608
+ if (state.shouldPreventClick) {
609
+ nativeEvent.preventDefault();
610
+ }
611
+ const onPress = props.onPress;
612
+
613
+ if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) {
614
+ state.pointerType = 'keyboard';
615
+ state.pressTarget = context.getResponderNode();
616
+ const preventDefault = props.preventDefault;
617
+
618
+ if (preventDefault !== false) {
619
+ nativeEvent.preventDefault();
620
+ }
621
+ dispatchEvent(event, onPress, context, state, 'press', DiscreteEvent);
622
+ }
623
+ break;
624
+ }
625
+ }
626
+ },
627
+ onRootEvent(
628
+ event: ReactDOMResponderEvent,
629
+ context: ReactDOMResponderContext,
630
+ props: PressProps,
631
+ state: PressState,
632
+ ): void {
633
+ let {pointerType, target, type} = event;
634
+
635
+ const nativeEvent: any = event.nativeEvent;
636
+ const isPressed = state.isPressed;
637
+ const activePointerId = state.activePointerId;
638
+ const previousPointerType = state.pointerType;
639
+
640
+ switch (type) {
641
+ // MOVE
642
+ case 'pointermove':
643
+ case 'mousemove':
644
+ case 'touchmove': {
645
+ let touchEvent;
646
+ // Ignore emulated events (pointermove will dispatch touch and mouse events)
647
+ // Ignore pointermove events during a keyboard press.
648
+ if (previousPointerType !== pointerType) {
649
+ return;
650
+ }
651
+ if (
652
+ type === 'pointermove' &&
653
+ activePointerId !== nativeEvent.pointerId
654
+ ) {
655
+ return;
656
+ } else if (type === 'touchmove') {
657
+ touchEvent = getTouchById(nativeEvent, activePointerId);
658
+ if (touchEvent === null) {
659
+ return;
660
+ }
661
+ state.touchEvent = touchEvent;
662
+ }
663
+ const pressTarget = state.pressTarget;
664
+
665
+ if (pressTarget !== null && !targetIsDocument(pressTarget)) {
666
+ if (
667
+ pointerType === 'mouse' &&
668
+ context.isTargetWithinNode(target, pressTarget)
669
+ ) {
670
+ state.isPressWithinResponderRegion = true;
671
+ } else {
672
+ // Calculate the responder region we use for deactivation, as the
673
+ // element dimensions may have changed since activation.
674
+ updateIsPressWithinResponderRegion(
675
+ touchEvent || nativeEvent,
676
+ context,
677
+ props,
678
+ state,
679
+ );
680
+ }
681
+ }
682
+
683
+ if (state.isPressWithinResponderRegion) {
684
+ if (isPressed) {
685
+ const onPressMove = props.onPressMove;
686
+
687
+ if (isFunction(onPressMove)) {
688
+ dispatchEvent(
689
+ event,
690
+ onPressMove,
691
+ context,
692
+ state,
693
+ 'pressmove',
694
+ UserBlockingEvent,
695
+ );
696
+ }
697
+ } else {
698
+ dispatchPressStartEvents(event, context, props, state);
699
+ }
700
+ } else {
701
+ dispatchPressEndEvents(event, context, props, state);
702
+ }
703
+ break;
704
+ }
705
+
706
+ // END
707
+ case 'pointerup':
708
+ case 'keyup':
709
+ case 'mouseup':
710
+ case 'touchend': {
711
+ if (isPressed) {
712
+ const buttons = state.buttons;
713
+ let isKeyboardEvent = false;
714
+ let touchEvent;
715
+ if (
716
+ type === 'pointerup' &&
717
+ activePointerId !== nativeEvent.pointerId
718
+ ) {
719
+ return;
720
+ } else if (type === 'touchend') {
721
+ touchEvent = getTouchById(nativeEvent, activePointerId);
722
+ if (touchEvent === null) {
723
+ return;
724
+ }
725
+ state.touchEvent = touchEvent;
726
+ target = getTouchTarget(context, touchEvent);
727
+ } else if (type === 'keyup') {
728
+ // Ignore unrelated keyboard events
729
+ if (!isValidKeyboardEvent(nativeEvent)) {
730
+ return;
731
+ }
732
+ isKeyboardEvent = true;
733
+ removeRootEventTypes(context, state);
734
+ } else if (buttons === 4) {
735
+ // Remove the root events here as no 'click' event is dispatched when this 'button' is pressed.
736
+ removeRootEventTypes(context, state);
737
+ }
738
+
739
+ // Determine whether to call preventDefault on subsequent native events.
740
+ if (
741
+ context.isTargetWithinResponder(target) &&
742
+ context.isTargetWithinHostComponent(target, 'a')
743
+ ) {
744
+ const {
745
+ altKey,
746
+ ctrlKey,
747
+ metaKey,
748
+ shiftKey,
749
+ } = (nativeEvent: MouseEvent);
750
+ // Check "open in new window/tab" and "open context menu" key modifiers
751
+ const preventDefault = props.preventDefault;
752
+
753
+ if (
754
+ preventDefault !== false &&
755
+ !shiftKey &&
756
+ !metaKey &&
757
+ !ctrlKey &&
758
+ !altKey
759
+ ) {
760
+ state.shouldPreventClick = true;
761
+ }
762
+ }
763
+
764
+ const pressTarget = state.pressTarget;
765
+ dispatchPressEndEvents(event, context, props, state);
766
+ const onPress = props.onPress;
767
+
768
+ if (pressTarget !== null && isFunction(onPress)) {
769
+ if (
770
+ !isKeyboardEvent &&
771
+ pressTarget !== null &&
772
+ !targetIsDocument(pressTarget)
773
+ ) {
774
+ if (
775
+ pointerType === 'mouse' &&
776
+ context.isTargetWithinNode(target, pressTarget)
777
+ ) {
778
+ state.isPressWithinResponderRegion = true;
779
+ } else {
780
+ // If the event target isn't within the press target, check if we're still
781
+ // within the responder region. The region may have changed if the
782
+ // element's layout was modified after activation.
783
+ updateIsPressWithinResponderRegion(
784
+ touchEvent || nativeEvent,
785
+ context,
786
+ props,
787
+ state,
788
+ );
789
+ }
790
+ }
791
+
792
+ if (state.isPressWithinResponderRegion && buttons !== 4) {
793
+ dispatchEvent(
794
+ event,
795
+ onPress,
796
+ context,
797
+ state,
798
+ 'press',
799
+ DiscreteEvent,
800
+ );
801
+ }
802
+ }
803
+ state.touchEvent = null;
804
+ } else if (type === 'mouseup') {
805
+ state.ignoreEmulatedMouseEvents = false;
806
+ }
807
+ break;
808
+ }
809
+
810
+ case 'click': {
811
+ // "keyup" occurs after "click"
812
+ if (previousPointerType !== 'keyboard') {
813
+ removeRootEventTypes(context, state);
814
+ }
815
+ break;
816
+ }
817
+
818
+ // CANCEL
819
+ case 'scroll': {
820
+ // We ignore incoming scroll events when using mouse events
821
+ if (previousPointerType === 'mouse') {
822
+ return;
823
+ }
824
+ const pressTarget = state.pressTarget;
825
+ const scrollTarget = nativeEvent.target;
826
+ const doc = context.getActiveDocument();
827
+ // If the scroll target is the document or if the press target
828
+ // is inside the scroll target, then this a scroll that should
829
+ // trigger a cancel.
830
+ if (
831
+ pressTarget !== null &&
832
+ (scrollTarget === doc ||
833
+ context.isTargetWithinNode(pressTarget, scrollTarget))
834
+ ) {
835
+ dispatchCancel(event, context, props, state);
836
+ }
837
+ break;
838
+ }
839
+ case 'pointercancel':
840
+ case 'touchcancel':
841
+ case 'dragstart': {
842
+ dispatchCancel(event, context, props, state);
843
+ }
844
+ }
845
+ },
846
+ onUnmount(
847
+ context: ReactDOMResponderContext,
848
+ props: PressProps,
849
+ state: PressState,
850
+ ) {
851
+ unmountResponder(context, props, state);
852
+ },
853
+};
854
+
855
+export const PressResponder = React.unstable_createResponder(
856
+ 'Press',
857
+ pressResponderImpl,
858
+);
859
+
860
+export function usePress(
861
+ props: PressProps,
862
+): ReactEventResponderListener<any, any> {
863
+ return React.unstable_useResponder(PressResponder, props);
864
+}
packages/react-ui/events/src/dom/Tap.js
+34
-31
@@ -36,26 +36,6 @@ type TapProps = $ReadOnly<{|
36
onTapUpdate?: (e: TapEvent) => void,
37
|}>;
38
39
-type TapState = {|
40
- activePointerId: null | number,
41
- buttons: 0 | 1 | 4,
42
- gestureState: TapGestureState,
43
- ignoreEmulatedEvents: boolean,
44
- initialPosition: {|x: number, y: number|},
45
- isActive: boolean,
46
- pointerType: PointerType,
47
- responderTarget: null | Element,
48
- rootEvents: null | Array<string>,
49
- shouldPreventClick: boolean,
50
-|};
51
-
52
-type TapEventType =
53
- | 'tap-cancel'
54
- | 'tap-change'
55
- | 'tap-end'
56
- | 'tap-start'
57
- | 'tap-update';
58
-
39
type TapGestureState = {|
40
altKey: boolean,
41
buttons: 0 | 1 | 4,
@@ -80,10 +60,31 @@ type TapGestureState = {|
60
y: number,
61
|};
62
83
-type TapEvent = $ReadOnly<{|
63
+type TapState = {|
64
+ activePointerId: null | number,
65
+ buttons: 0 | 1 | 4,
66
+ gestureState: TapGestureState,
67
+ ignoreEmulatedEvents: boolean,
68
+ initialPosition: {|x: number, y: number|},
69
+ isActive: boolean,
70
+ pointerType: PointerType,
71
+ responderTarget: null | Element,
72
+ rootEvents: null | Array<string>,
73
+ shouldPreventClick: boolean,
74
+|};
75
+
76
+type TapEventType =
77
+ | 'tap:cancel'
78
+ | 'tap:change'
79
+ | 'tap:end'
80
+ | 'tap:start'
81
+ | 'tap:update';
82
+
83
+type TapEvent = {|
84
...TapGestureState,
85
+ defaultPrevented: boolean,
86
type: TapEventType,
86
-|}>;
87
+|};
88
89
/**
90
* Native event dependencies
@@ -380,6 +381,7 @@ function dispatchStart(
381
const payload = context.objectAssign({}, state.gestureState, {type});
382
dispatchDiscreteEvent(context, payload, onTapStart);
383
}
384
+ dispatchChange(context, props, state);
385
}
386
387
function dispatchChange(
@@ -414,8 +416,13 @@ function dispatchEnd(
416
): void {
417
const type = 'tap:end';
418
const onTapEnd = props.onTapEnd;
419
+ dispatchChange(context, props, state);
420
if (onTapEnd != null) {
418
- const payload = context.objectAssign({}, state.gestureState, {type});
421
+ const defaultPrevented = state.shouldPreventClick === true;
422
+ const payload = context.objectAssign({}, state.gestureState, {
423
+ defaultPrevented,
424
+ type,
425
+ });
426
dispatchDiscreteEvent(context, payload, onTapEnd);
427
}
428
}
@@ -427,6 +434,7 @@ function dispatchCancel(
434
): void {
435
const type = 'tap:cancel';
436
const onTapCancel = props.onTapCancel;
437
+ dispatchChange(context, props, state);
438
if (onTapCancel != null) {
439
const payload = context.objectAssign({}, state.gestureState, {type});
440
dispatchDiscreteEvent(context, payload, onTapCancel);
@@ -451,8 +459,8 @@ const responderImpl = {
459
if (props.disabled) {
460
removeRootEventTypes(context, state);
461
if (state.isActive) {
454
- dispatchCancel(context, props, state);
462
state.isActive = false;
463
+ dispatchCancel(context, props, state);
464
}
465
return;
466
}
@@ -498,7 +506,6 @@ const responderImpl = {
506
state.initialPosition.y = gestureState.y;
507
508
dispatchStart(context, props, state);
501
- dispatchChange(context, props, state);
509
addRootEventTypes(rootEventTypes, context, state);
510
511
if (!hasPointerEvents) {
@@ -522,7 +529,7 @@ const responderImpl = {
529
const hitTarget = getHitTarget(event, context, state);
530
531
switch (eventType) {
525
- // MOVE
532
+ // UPDATE
533
case 'pointermove':
534
case 'mousemove':
535
case 'touchmove': {
@@ -557,7 +564,6 @@ const responderImpl = {
564
dispatchUpdate(context, props, state);
565
} else {
566
state.isActive = false;
560
- dispatchChange(context, props, state);
567
dispatchCancel(context, props, state);
568
}
569
}
@@ -578,7 +584,6 @@ const responderImpl = {
584
state.gestureState = createGestureState(context, props, state, event);
585
586
state.isActive = false;
581
- dispatchChange(context, props, state);
587
if (context.isTargetWithinResponder(hitTarget)) {
588
// Determine whether to call preventDefault on subsequent native events.
589
if (hasModifierKey(event)) {
@@ -606,7 +611,6 @@ const responderImpl = {
611
if (state.isActive && isActivePointer(event, state)) {
612
state.gestureState = createGestureState(context, props, state, event);
613
state.isActive = false;
609
- dispatchChange(context, props, state);
614
dispatchCancel(context, props, state);
615
}
616
break;
@@ -625,7 +629,6 @@ const responderImpl = {
629
) {
630
state.gestureState = createGestureState(context, props, state, event);
631
state.isActive = false;
628
- dispatchChange(context, props, state);
632
dispatchCancel(context, props, state);
633
}
634
break;
@@ -647,8 +650,8 @@ const responderImpl = {
650
): void {
651
removeRootEventTypes(context, state);
652
if (state.isActive) {
650
- dispatchCancel(context, props, state);
653
state.isActive = false;
654
+ dispatchCancel(context, props, state);
655
}
656
},
657
};
packages/react-ui/events/src/dom/__tests__/Keyboard-test.internal.js
+72
-22
@@ -158,7 +158,7 @@ describe('Keyboard responder', () => {
158
});
159
160
// e.g, "Enter" on link
161
- test('keyboard click is between key events', () => {
161
+ test('click is between key events', () => {
162
const target = createEventTarget(ref.current);
163
target.keydown({key: 'Enter'});
164
target.keyup({key: 'Enter'});
@@ -168,7 +168,7 @@ describe('Keyboard responder', () => {
168
expect.objectContaining({
169
altKey: false,
170
ctrlKey: false,
171
- defaultPrevented: false,
171
+ defaultPrevented: true,
172
metaKey: false,
173
pointerType: 'keyboard',
174
shiftKey: false,
@@ -180,7 +180,7 @@ describe('Keyboard responder', () => {
180
});
181
182
// e.g., "Spacebar" on button
183
- test('keyboard click is after key events', () => {
183
+ test('click is after key events', () => {
184
const target = createEventTarget(ref.current);
185
target.keydown({key: 'Enter'});
186
target.keyup({key: 'Enter'});
@@ -190,7 +190,27 @@ describe('Keyboard responder', () => {
190
expect.objectContaining({
191
altKey: false,
192
ctrlKey: false,
193
- defaultPrevented: false,
193
+ defaultPrevented: true,
194
+ metaKey: false,
195
+ pointerType: 'keyboard',
196
+ shiftKey: false,
197
+ target: target.node,
198
+ timeStamp: expect.any(Number),
199
+ type: 'keyboard:click',
200
+ }),
201
+ );
202
+ });
203
+
204
+ // e.g, generated by a screen-reader
205
+ test('click is orphan', () => {
206
+ const target = createEventTarget(ref.current);
207
+ target.virtualclick();
208
+ expect(onClick).toHaveBeenCalledTimes(1);
209
+ expect(onClick).toHaveBeenCalledWith(
210
+ expect.objectContaining({
211
+ altKey: false,
212
+ ctrlKey: false,
213
+ defaultPrevented: true,
214
metaKey: false,
215
pointerType: 'keyboard',
216
shiftKey: false,
@@ -326,6 +346,51 @@ describe('Keyboard responder', () => {
346
});
347
});
348
349
+ describe('preventClick', () => {
350
+ function render(props) {
351
+ const ref = React.createRef();
352
+ const Component = () => {
353
+ const listener = useKeyboard(props);
354
+ return <div ref={ref} listeners={listener} />;
355
+ };
356
+ ReactDOM.render(<Component />, container);
357
+ return ref;
358
+ }
359
+
360
+ test('prevents native click by default', () => {
361
+ const onClick = jest.fn();
362
+ const preventDefault = jest.fn();
363
+ const ref = render({onClick});
364
+
365
+ const target = createEventTarget(ref.current);
366
+ target.virtualclick({preventDefault});
367
+
368
+ expect(preventDefault).toBeCalled();
369
+ expect(onClick).toHaveBeenCalledTimes(1);
370
+ expect(onClick).toHaveBeenCalledWith(
371
+ expect.objectContaining({
372
+ defaultPrevented: true,
373
+ }),
374
+ );
375
+ });
376
+
377
+ test('allows native behaviour if false', () => {
378
+ const onClick = jest.fn();
379
+ const preventDefault = jest.fn();
380
+ const ref = render({onClick, preventClick: false});
381
+
382
+ const target = createEventTarget(ref.current);
383
+ target.virtualclick({preventDefault});
384
+ expect(preventDefault).not.toBeCalled();
385
+ expect(onClick).toHaveBeenCalledTimes(1);
386
+ expect(onClick).toHaveBeenCalledWith(
387
+ expect.objectContaining({
388
+ defaultPrevented: false,
389
+ }),
390
+ );
391
+ });
392
+ });
393
+
394
describe('preventKeys', () => {
395
function render(props) {
396
const ref = React.createRef();
@@ -347,9 +412,8 @@ describe('Keyboard responder', () => {
412
target.keydown({key: 'Tab', preventDefault});
413
target.virtualclick({preventDefault: preventDefaultClick});
414
350
- expect(onKeyDown).toHaveBeenCalledTimes(1);
415
expect(preventDefault).toBeCalled();
352
- expect(preventDefaultClick).toBeCalled();
416
+ expect(onKeyDown).toHaveBeenCalledTimes(1);
417
expect(onKeyDown).toHaveBeenCalledWith(
418
expect.objectContaining({
419
defaultPrevented: true,
@@ -362,19 +426,12 @@ describe('Keyboard responder', () => {
426
test('key config matches (modifier keys)', () => {
427
const onKeyDown = jest.fn();
428
const preventDefault = jest.fn();
365
- const preventDefaultClick = jest.fn();
429
const ref = render({onKeyDown, preventKeys: [['Tab', {shiftKey: true}]]});
430
431
const target = createEventTarget(ref.current);
432
target.keydown({key: 'Tab', preventDefault, shiftKey: true});
370
- target.virtualclick({
371
- preventDefault: preventDefaultClick,
372
- shiftKey: true,
373
- });
374
-
375
- expect(onKeyDown).toHaveBeenCalledTimes(1);
433
expect(preventDefault).toBeCalled();
377
- expect(preventDefaultClick).toBeCalled();
434
+ expect(onKeyDown).toHaveBeenCalledTimes(1);
435
expect(onKeyDown).toHaveBeenCalledWith(
436
expect.objectContaining({
437
defaultPrevented: true,
@@ -388,19 +445,12 @@ describe('Keyboard responder', () => {
445
test('key config does not match (modifier keys)', () => {
446
const onKeyDown = jest.fn();
447
const preventDefault = jest.fn();
391
- const preventDefaultClick = jest.fn();
448
const ref = render({onKeyDown, preventKeys: [['Tab', {shiftKey: true}]]});
449
450
const target = createEventTarget(ref.current);
451
target.keydown({key: 'Tab', preventDefault, shiftKey: false});
396
- target.virtualclick({
397
- preventDefault: preventDefaultClick,
398
- shiftKey: false,
399
- });
400
-
401
- expect(onKeyDown).toHaveBeenCalledTimes(1);
452
expect(preventDefault).not.toBeCalled();
403
- expect(preventDefaultClick).not.toBeCalled();
453
+ expect(onKeyDown).toHaveBeenCalledTimes(1);
454
expect(onKeyDown).toHaveBeenCalledWith(
455
expect.objectContaining({
456
defaultPrevented: false,
packages/react-ui/events/src/dom/__tests__/MixedResponders-test-internal.js
+32
-26
@@ -35,7 +35,7 @@ describe('mixing responders with the heritage event system', () => {
35
});
36
37
it('should properly only flush sync once when the event systems are mixed', () => {
38
- const usePress = require('react-ui/events/press').usePress;
38
+ const useTap = require('react-ui/events/tap').useTap;
39
const ref = React.createRef();
40
let renderCounts = 0;
41
@@ -43,12 +43,12 @@ describe('mixing responders with the heritage event system', () => {
43
const [, updateCounter] = React.useState(0);
44
renderCounts++;
45
46
- function handlePress() {
46
+ function handleTap() {
47
updateCounter(count => count + 1);
48
}
49
50
- const listener = usePress({
51
- onPress: handlePress,
50
+ const listener = useTap({
51
+ onTapEnd: handleTap,
52
});
53
54
return (
@@ -104,7 +104,7 @@ describe('mixing responders with the heritage event system', () => {
104
});
105
106
it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => {
107
- const usePress = require('react-ui/events/press').usePress;
107
+ const useTap = require('react-ui/events/tap').useTap;
108
const ref = React.createRef();
109
let renderCounts = 0;
110
@@ -112,12 +112,12 @@ describe('mixing responders with the heritage event system', () => {
112
const [, updateCounter] = React.useState(0);
113
renderCounts++;
114
115
- function handlePress() {
115
+ function handleTap() {
116
updateCounter(count => count + 1);
117
}
118
119
- const listener = usePress({
120
- onPress: handlePress,
119
+ const listener = useTap({
120
+ onTapEnd: handleTap,
121
});
122
123
return (
@@ -177,7 +177,7 @@ describe('mixing responders with the heritage event system', () => {
177
'event systems',
178
async () => {
179
const {useState} = React;
180
- const usePress = require('react-ui/events/press').usePress;
180
+ const useTap = require('react-ui/events/tap').useTap;
181
182
const button = React.createRef();
183
@@ -187,7 +187,7 @@ describe('mixing responders with the heritage event system', () => {
187
const [pressesCount, updatePressesCount] = useState(0);
188
const [clicksCount, updateClicksCount] = useState(0);
189
190
- function handlePress() {
190
+ function handleTap() {
191
// This dispatches a synchronous, discrete event in the legacy event
192
// system. However, because it's nested inside the new event system,
193
// its updates should not flush until the end of the outer handler.
@@ -198,14 +198,14 @@ describe('mixing responders with the heritage event system', () => {
198
updatePressesCount(pressesCount + 1);
199
}
200
201
- const listener = usePress({
202
- onPress: handlePress,
201
+ const tap = useTap({
202
+ onTapEnd: handleTap,
203
});
204
205
return (
206
<div>
207
<button
208
- listeners={listener}
208
+ listeners={tap}
209
ref={button}
210
onClick={() => updateClicksCount(clicksCount + 1)}>
211
Presses: {pressesCount}, Clicks: {clicksCount}
@@ -237,37 +237,35 @@ describe('mixing responders with the heritage event system', () => {
237
it('is async for non-input events', () => {
238
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
239
ReactFeatureFlags.enableUserBlockingEvents = true;
240
- const usePress = require('react-ui/events/press').usePress;
240
+ const useTap = require('react-ui/events/tap').useTap;
241
const useInput = require('react-ui/events/input').useInput;
242
const root = ReactDOM.unstable_createRoot(container);
243
let input;
244
245
let ops = [];
246
247
- function Component({innerRef, onChange, controlledValue, pressListener}) {
248
- const inputListener = useInput({
249
- onChange,
250
- });
247
+ function Component({innerRef, onChange, controlledValue, listeners}) {
248
+ const inputListener = useInput({onChange});
249
return (
250
<input
251
type="text"
252
ref={innerRef}
253
value={controlledValue}
256
- listeners={[inputListener, pressListener]}
254
+ listeners={[inputListener, listeners]}
255
/>
256
);
257
}
258
261
- function PressWrapper({innerRef, onPress, onChange, controlledValue}) {
262
- const pressListener = usePress({
263
- onPress,
259
+ function PressWrapper({innerRef, onTap, onChange, controlledValue}) {
260
+ const tap = useTap({
261
+ onTapEnd: onTap,
262
});
263
return (
264
<Component
265
onChange={onChange}
266
innerRef={el => (input = el)}
267
controlledValue={controlledValue}
270
- pressListener={pressListener}
268
+ listeners={tap}
269
/>
270
);
271
}
@@ -284,7 +282,7 @@ describe('mixing responders with the heritage event system', () => {
282
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
283
return (
284
<PressWrapper
287
- onPress={this.reset}
285
+ onTap={this.reset}
286
onChange={this.onChange}
287
innerRef={el => (input = el)}
288
controlledValue={controlledValue}
@@ -307,10 +305,18 @@ describe('mixing responders with the heritage event system', () => {
305
306
// Trigger a click event
307
input.dispatchEvent(
310
- new MouseEvent('mousedown', {bubbles: true, cancelable: true}),
308
+ new MouseEvent('mousedown', {
309
+ bubbles: true,
310
+ cancelable: true,
311
+ buttons: 1,
312
+ }),
313
);
314
input.dispatchEvent(
313
- new MouseEvent('mouseup', {bubbles: true, cancelable: true}),
315
+ new MouseEvent('mouseup', {
316
+ bubbles: true,
317
+ cancelable: true,
318
+ buttons: 0,
319
+ }),
320
);
321
// Nothing should have changed
322
expect(ops).toEqual([]);
packages/react-ui/events/src/dom/__tests__/Press-test.internal.js
+33
-521
@@ -12,13 +12,13 @@
12
import {
13
buttonsType,
14
createEventTarget,
15
+ describeWithPointerEvent,
16
setPointerEvent,
17
} from '../testing-library';
18
19
let React;
20
let ReactFeatureFlags;
21
let ReactDOM;
21
-let PressResponder;
22
let usePress;
23
24
function initializeModules(hasPointerEvents) {
@@ -28,23 +28,12 @@ function initializeModules(hasPointerEvents) {
28
ReactFeatureFlags.enableFlareAPI = true;
29
React = require('react');
30
ReactDOM = require('react-dom');
31
- PressResponder = require('react-ui/events/press').PressResponder;
31
usePress = require('react-ui/events/press').usePress;
32
}
33
35
-function removePressMoveStrings(eventString) {
36
- if (eventString === 'onPressMove') {
37
- return false;
38
- }
39
- return true;
40
-}
41
-
42
-const forcePointerEvents = true;
43
-const environmentTable = [[forcePointerEvents], [!forcePointerEvents]];
44
-
34
const pointerTypesTable = [['mouse'], ['touch']];
35
47
-describe.each(environmentTable)('Press responder', hasPointerEvents => {
36
+describeWithPointerEvent('Press responder', hasPointerEvents => {
37
let container;
38
39
beforeEach(() => {
@@ -60,33 +49,49 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
49
});
50
51
describe('disabled', () => {
63
- let onPressStart, onPress, onPressEnd, ref;
52
+ let onPressStart, onPressChange, onPressMove, onPressEnd, onPress, ref;
53
54
beforeEach(() => {
55
onPressStart = jest.fn();
67
- onPress = jest.fn();
56
+ onPressChange = jest.fn();
57
+ onPressMove = jest.fn();
58
onPressEnd = jest.fn();
59
+ onPress = jest.fn();
60
ref = React.createRef();
61
const Component = () => {
62
const listener = usePress({
63
disabled: true,
64
onPressStart,
74
- onPress,
65
+ onPressChange,
66
+ onPressMove,
67
onPressEnd,
68
+ onPress,
69
});
70
return <div ref={ref} listeners={listener} />;
71
};
72
ReactDOM.render(<Component />, container);
80
- document.elementFromPoint = () => ref.current;
73
});
74
83
- it('does not call callbacks', () => {
75
+ test('does not call callbacks for pointers', () => {
76
const target = createEventTarget(ref.current);
77
target.pointerdown();
78
target.pointerup();
79
expect(onPressStart).not.toBeCalled();
80
+ expect(onPressChange).not.toBeCalled();
81
+ expect(onPressMove).not.toBeCalled();
82
+ expect(onPressEnd).not.toBeCalled();
83
expect(onPress).not.toBeCalled();
84
+ });
85
+
86
+ test('does not call callbacks for keyboard', () => {
87
+ const target = createEventTarget(ref.current);
88
+ target.keydown({key: 'Enter'});
89
+ target.keyup({key: 'Enter'});
90
+ expect(onPressStart).not.toBeCalled();
91
+ expect(onPressChange).not.toBeCalled();
92
+ expect(onPressMove).not.toBeCalled();
93
expect(onPressEnd).not.toBeCalled();
94
+ expect(onPress).not.toBeCalled();
95
});
96
});
97
@@ -131,17 +136,6 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
136
);
137
});
138
134
- it('is not called after pointer move following middle-button press', () => {
135
- const node = ref.current;
136
- const target = createEventTarget(node);
137
- target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
138
- target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
139
- target.pointerup({pointerType: 'mouse'});
140
- target.pointerhover({x: 110, y: 110});
141
- target.pointerhover({x: 50, y: 50});
142
- expect(onPressStart).toHaveBeenCalledTimes(1);
143
- });
144
-
139
it('ignores any events not caused by primary/middle-click or touch/pen contact', () => {
140
const target = createEventTarget(ref.current);
141
target.pointerdown({buttons: buttonsType.secondary});
@@ -231,7 +225,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
225
const target = createEventTarget(ref.current);
226
target.keydown({key: 'Enter'});
227
// click occurs before keyup
234
- target.click();
228
+ target.virtualclick();
229
target.keyup({key: 'Enter'});
230
expect(onPressEnd).toHaveBeenCalledTimes(1);
231
expect(onPressEnd).toHaveBeenCalledWith(
@@ -399,28 +393,6 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
393
);
394
});
395
402
- it('is called if target rect is not right but the target is (for mouse events)', () => {
403
- const buttonRef = React.createRef();
404
- const divRef = React.createRef();
405
-
406
- const Component = () => {
407
- const listener = usePress({onPress});
408
- return (
409
- <div ref={divRef} listeners={listener}>
410
- <button ref={buttonRef} />
411
- </div>
412
- );
413
- };
414
- ReactDOM.render(<Component />, container);
415
-
416
- const target = createEventTarget(divRef.current);
417
- target.setBoundingClientRect({x: 0, y: 0, width: 0, height: 0});
418
- const innerTarget = createEventTarget(buttonRef.current);
419
- innerTarget.pointerdown({pointerType: 'mouse'});
420
- innerTarget.pointerup({pointerType: 'mouse'});
421
- expect(onPress).toBeCalled();
422
- });
423
-
396
it('is called once after virtual screen reader "click" event', () => {
397
const target = createEventTarget(ref.current);
398
const preventDefault = jest.fn();
@@ -488,386 +460,6 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
460
});
461
});
462
491
- describe.each(pointerTypesTable)('press with movement: %s', pointerType => {
492
- let events, ref, outerRef;
493
-
494
- beforeEach(() => {
495
- events = [];
496
- ref = React.createRef();
497
- outerRef = React.createRef();
498
- const createEventHandler = msg => () => {
499
- events.push(msg);
500
- };
501
- const Component = () => {
502
- const listener = usePress({
503
- onPress: createEventHandler('onPress'),
504
- onPressChange: createEventHandler('onPressChange'),
505
- onPressMove: createEventHandler('onPressMove'),
506
- onPressStart: createEventHandler('onPressStart'),
507
- onPressEnd: createEventHandler('onPressEnd'),
508
- });
509
- return (
510
- <div ref={outerRef}>
511
- <div ref={ref} listeners={listener} />
512
- </div>
513
- );
514
- };
515
- ReactDOM.render(<Component />, container);
516
- document.elementFromPoint = () => ref.current;
517
- });
518
-
519
- const rectMock = {width: 100, height: 100, x: 50, y: 50};
520
- const pressRectOffset = 20;
521
- const coordinatesInside = {
522
- x: rectMock.x - pressRectOffset,
523
- y: rectMock.y - pressRectOffset,
524
- };
525
- const coordinatesOutside = {
526
- x: rectMock.x - pressRectOffset - 1,
527
- y: rectMock.y - pressRectOffset - 1,
528
- };
529
-
530
- describe('within bounds of hit rect', () => {
531
- /** ┌──────────────────┐
532
- * │ ┌────────────┐ │
533
- * │ │ VisualRect │ │
534
- * │ └────────────┘ │
535
- * │ HitRect X │ <= Move to X and release
536
- * └──────────────────┘
537
- */
538
- it('"onPress*" events are called immediately', () => {
539
- const target = createEventTarget(ref.current);
540
- target.setBoundingClientRect(rectMock);
541
- target.pointerdown({pointerType});
542
- target.pointermove({pointerType, ...coordinatesInside});
543
- target.pointerup({pointerType, ...coordinatesInside});
544
- expect(events).toEqual([
545
- 'onPressStart',
546
- 'onPressChange',
547
- 'onPressMove',
548
- 'onPressEnd',
549
- 'onPressChange',
550
- 'onPress',
551
- ]);
552
- });
553
-
554
- it('"onPress*" events are correctly called with target change', () => {
555
- const target = createEventTarget(ref.current);
556
- const outerTarget = createEventTarget(outerRef.current);
557
- target.setBoundingClientRect(rectMock);
558
- target.pointerdown({pointerType});
559
- target.pointermove({pointerType, ...coordinatesInside});
560
- // TODO: this sequence may differ in the future between PointerEvent and mouse fallback when
561
- // use 'setPointerCapture'.
562
- if (pointerType === 'touch') {
563
- target.pointermove({pointerType, ...coordinatesOutside});
564
- } else {
565
- outerTarget.pointermove({pointerType, ...coordinatesOutside});
566
- }
567
- target.pointermove({pointerType, ...coordinatesInside});
568
- target.pointerup({pointerType, ...coordinatesInside});
569
-
570
- expect(events.filter(removePressMoveStrings)).toEqual([
571
- 'onPressStart',
572
- 'onPressChange',
573
- 'onPressEnd',
574
- 'onPressChange',
575
- 'onPressStart',
576
- 'onPressChange',
577
- 'onPressEnd',
578
- 'onPressChange',
579
- 'onPress',
580
- ]);
581
- });
582
-
583
- it('press retention offset can be configured', () => {
584
- let localEvents = [];
585
- const localRef = React.createRef();
586
- const createEventHandler = msg => () => {
587
- localEvents.push(msg);
588
- };
589
- const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40};
590
-
591
- const Component = () => {
592
- const listener = usePress({
593
- onPress: createEventHandler('onPress'),
594
- onPressChange: createEventHandler('onPressChange'),
595
- onPressMove: createEventHandler('onPressMove'),
596
- onPressStart: createEventHandler('onPressStart'),
597
- onPressEnd: createEventHandler('onPressEnd'),
598
- pressRetentionOffset,
599
- });
600
- return <div ref={localRef} listeners={listener} />;
601
- };
602
- ReactDOM.render(<Component />, container);
603
-
604
- const target = createEventTarget(localRef.current);
605
- target.setBoundingClientRect(rectMock);
606
- target.pointerdown({pointerType});
607
- target.pointermove({
608
- pointerType,
609
- x: rectMock.x,
610
- y: rectMock.y,
611
- });
612
- target.pointerup({pointerType, ...coordinatesInside});
613
- expect(localEvents).toEqual([
614
- 'onPressStart',
615
- 'onPressChange',
616
- 'onPressMove',
617
- 'onPressEnd',
618
- 'onPressChange',
619
- 'onPress',
620
- ]);
621
- });
622
-
623
- it('responder region accounts for decrease in element dimensions', () => {
624
- const target = createEventTarget(ref.current);
625
- target.setBoundingClientRect(rectMock);
626
- target.pointerdown({pointerType});
627
- // emulate smaller dimensions change on activation
628
- target.setBoundingClientRect({width: 80, height: 80, y: 60, x: 60});
629
- const coordinates = {x: rectMock.x, y: rectMock.y};
630
- // move to an area within the pre-activation region
631
- target.pointermove({pointerType, ...coordinates});
632
- target.pointerup({pointerType, ...coordinates});
633
- expect(events).toEqual([
634
- 'onPressStart',
635
- 'onPressChange',
636
- 'onPressMove',
637
- 'onPressEnd',
638
- 'onPressChange',
639
- 'onPress',
640
- ]);
641
- });
642
-
643
- it('responder region accounts for increase in element dimensions', () => {
644
- const target = createEventTarget(ref.current);
645
- target.setBoundingClientRect(rectMock);
646
- target.pointerdown({pointerType});
647
- // emulate larger dimensions change on activation
648
- target.setBoundingClientRect({width: 200, height: 200, y: 0, x: 0});
649
- const coordinates = {x: rectMock.x - 50, y: rectMock.y - 50};
650
- // move to an area within the post-activation region
651
- target.pointermove({pointerType, ...coordinates});
652
- target.pointerup({pointerType, ...coordinates});
653
- expect(events).toEqual([
654
- 'onPressStart',
655
- 'onPressChange',
656
- 'onPressMove',
657
- 'onPressEnd',
658
- 'onPressChange',
659
- 'onPress',
660
- ]);
661
- });
662
- });
663
-
664
- describe('beyond bounds of hit rect', () => {
665
- /** ┌──────────────────┐
666
- * │ ┌────────────┐ │
667
- * │ │ VisualRect │ │
668
- * │ └────────────┘ │
669
- * │ HitRect │
670
- * └──────────────────┘
671
- * X <= Move to X and release
672
- */
673
- it('"onPress" is not called on release', () => {
674
- const target = createEventTarget(ref.current);
675
- const targetContainer = createEventTarget(container);
676
- target.setBoundingClientRect(rectMock);
677
- target.pointerdown({pointerType});
678
- target.pointermove({pointerType, ...coordinatesInside});
679
- if (pointerType === 'mouse') {
680
- // TODO: use setPointerCapture so this is only true for fallback mouse events.
681
- targetContainer.pointermove({pointerType, ...coordinatesOutside});
682
- targetContainer.pointerup({pointerType, ...coordinatesOutside});
683
- } else {
684
- target.pointermove({pointerType, ...coordinatesOutside});
685
- target.pointerup({pointerType, ...coordinatesOutside});
686
- }
687
- expect(events.filter(removePressMoveStrings)).toEqual([
688
- 'onPressStart',
689
- 'onPressChange',
690
- 'onPressEnd',
691
- 'onPressChange',
692
- ]);
693
- });
694
- });
695
-
696
- it('"onPress" is called on re-entry to hit rect', () => {
697
- const target = createEventTarget(ref.current);
698
- const targetContainer = createEventTarget(container);
699
- target.setBoundingClientRect(rectMock);
700
- target.pointerdown({pointerType});
701
- target.pointermove({pointerType, ...coordinatesInside});
702
- if (pointerType === 'mouse') {
703
- // TODO: use setPointerCapture so this is only true for fallback mouse events.
704
- targetContainer.pointermove({pointerType, ...coordinatesOutside});
705
- } else {
706
- target.pointermove({pointerType, ...coordinatesOutside});
707
- }
708
- target.pointermove({pointerType, ...coordinatesInside});
709
- target.pointerup({pointerType, ...coordinatesInside});
710
-
711
- expect(events).toEqual([
712
- 'onPressStart',
713
- 'onPressChange',
714
- 'onPressMove',
715
- 'onPressEnd',
716
- 'onPressChange',
717
- 'onPressStart',
718
- 'onPressChange',
719
- 'onPressEnd',
720
- 'onPressChange',
721
- 'onPress',
722
- ]);
723
- });
724
- });
725
-
726
- describe('nested responders', () => {
727
- if (hasPointerEvents) {
728
- it('dispatch events in the correct order', () => {
729
- const events = [];
730
- const ref = React.createRef();
731
- const createEventHandler = msg => () => {
732
- events.push(msg);
733
- };
734
-
735
- const Inner = () => {
736
- const listener = usePress({
737
- onPress: createEventHandler('inner: onPress'),
738
- onPressChange: createEventHandler('inner: onPressChange'),
739
- onPressMove: createEventHandler('inner: onPressMove'),
740
- onPressStart: createEventHandler('inner: onPressStart'),
741
- onPressEnd: createEventHandler('inner: onPressEnd'),
742
- });
743
- return (
744
- <div
745
- ref={ref}
746
- listeners={listener}
747
- onPointerDown={createEventHandler('pointerdown')}
748
- onPointerUp={createEventHandler('pointerup')}
749
- onKeyDown={createEventHandler('keydown')}
750
- onKeyUp={createEventHandler('keyup')}
751
- />
752
- );
753
- };
754
-
755
- const Outer = () => {
756
- const listener = usePress({
757
- onPress: createEventHandler('outer: onPress'),
758
- onPressChange: createEventHandler('outer: onPressChange'),
759
- onPressMove: createEventHandler('outer: onPressMove'),
760
- onPressStart: createEventHandler('outer: onPressStart'),
761
- onPressEnd: createEventHandler('outer: onPressEnd'),
762
- });
763
- return (
764
- <div listeners={listener}>
765
- <Inner />
766
- </div>
767
- );
768
- };
769
- ReactDOM.render(<Outer />, container);
770
-
771
- const target = createEventTarget(ref.current);
772
- target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
773
- target.pointerdown();
774
- target.pointerup();
775
- expect(events).toEqual([
776
- 'inner: onPressStart',
777
- 'inner: onPressChange',
778
- 'pointerdown',
779
- 'inner: onPressEnd',
780
- 'inner: onPressChange',
781
- 'inner: onPress',
782
- 'pointerup',
783
- ]);
784
- });
785
- }
786
-
787
- describe('correctly not propagate', () => {
788
- it('for onPress', () => {
789
- const ref = React.createRef();
790
- const onPress = jest.fn();
791
-
792
- const Inner = () => {
793
- const listener = usePress({onPress});
794
- return <div ref={ref} listeners={listener} />;
795
- };
796
-
797
- const Outer = () => {
798
- const listener = usePress({onPress});
799
- return (
800
- <div listeners={listener}>
801
- <Inner />
802
- </div>
803
- );
804
- };
805
- ReactDOM.render(<Outer />, container);
806
-
807
- const target = createEventTarget(ref.current);
808
- target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
809
- target.pointerdown();
810
- target.pointerup();
811
- expect(onPress).toHaveBeenCalledTimes(1);
812
- });
813
-
814
- it('for onPressStart/onPressEnd', () => {
815
- const ref = React.createRef();
816
- const onPressStart = jest.fn();
817
- const onPressEnd = jest.fn();
818
-
819
- const Inner = () => {
820
- const listener = usePress({onPressStart, onPressEnd});
821
- return <div ref={ref} listeners={listener} />;
822
- };
823
-
824
- const Outer = () => {
825
- const listener = usePress({onPressStart, onPressEnd});
826
- return (
827
- <div listeners={listener}>
828
- <Inner />
829
- </div>
830
- );
831
- };
832
- ReactDOM.render(<Outer />, container);
833
-
834
- const target = createEventTarget(ref.current);
835
- target.pointerdown();
836
- expect(onPressStart).toHaveBeenCalledTimes(1);
837
- expect(onPressEnd).toHaveBeenCalledTimes(0);
838
- target.pointerup();
839
- expect(onPressStart).toHaveBeenCalledTimes(1);
840
- expect(onPressEnd).toHaveBeenCalledTimes(1);
841
- });
842
-
843
- it('for onPressChange', () => {
844
- const ref = React.createRef();
845
- const onPressChange = jest.fn();
846
-
847
- const Inner = () => {
848
- const listener = usePress({onPressChange});
849
- return <div ref={ref} listeners={listener} />;
850
- };
851
-
852
- const Outer = () => {
853
- const listener = usePress({onPressChange});
854
- return (
855
- <div listeners={listener}>
856
- <Inner />
857
- </div>
858
- );
859
- };
860
- ReactDOM.render(<Outer />, container);
861
-
862
- const target = createEventTarget(ref.current);
863
- target.pointerdown();
864
- expect(onPressChange).toHaveBeenCalledTimes(1);
865
- target.pointerup();
866
- expect(onPressChange).toHaveBeenCalledTimes(2);
867
- });
868
- });
869
- });
870
-
463
describe('link components', () => {
464
it('prevents native behavior by default', () => {
465
const onPress = jest.fn();
@@ -891,7 +483,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
483
484
it('prevents native behaviour for keyboard events by default', () => {
485
const onPress = jest.fn();
894
- const preventDefault = jest.fn();
486
+ const preventDefaultClick = jest.fn();
487
+ const preventDefaultKeyDown = jest.fn();
488
const ref = React.createRef();
489
490
const Component = () => {
@@ -901,10 +494,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
494
ReactDOM.render(<Component />, container);
495
496
const target = createEventTarget(ref.current);
904
- target.keydown({key: 'Enter'});
905
- target.click({preventDefault});
497
+ target.keydown({key: 'Enter', preventDefault: preventDefaultKeyDown});
498
+ target.virtualclick({preventDefault: preventDefaultClick});
499
target.keyup({key: 'Enter'});
907
- expect(preventDefault).toBeCalled();
500
+ expect(preventDefaultKeyDown).toBeCalled();
501
+ expect(preventDefaultClick).toBeCalled();
502
+ expect(onPress).toHaveBeenCalledTimes(1);
503
expect(onPress).toHaveBeenCalledWith(
504
expect.objectContaining({defaultPrevented: true}),
505
);
@@ -1010,99 +605,16 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
605
606
const target = createEventTarget(ref.current);
607
target.keydown({key: 'Enter'});
1013
- target.click({preventDefault});
608
+ target.virtualclick({preventDefault});
609
target.keyup({key: 'Enter'});
610
expect(preventDefault).not.toBeCalled();
611
+ expect(onPress).toHaveBeenCalledTimes(1);
612
expect(onPress).toHaveBeenCalledWith(
613
expect.objectContaining({defaultPrevented: false}),
614
);
615
});
616
});
617
1022
- describe('responder cancellation', () => {
1023
- it.each(pointerTypesTable)('ends on pointer cancel', pointerType => {
1024
- const onPressEnd = jest.fn();
1025
- const ref = React.createRef();
1026
-
1027
- const Component = () => {
1028
- const listener = usePress({onPressEnd});
1029
- return <a href="#" ref={ref} listeners={listener} />;
1030
- };
1031
- ReactDOM.render(<Component />, container);
1032
-
1033
- const target = createEventTarget(ref.current);
1034
- target.pointerdown({pointerType});
1035
- target.pointercancel({pointerType});
1036
- expect(onPressEnd).toHaveBeenCalledTimes(1);
1037
- });
1038
- });
1039
-
1040
- it('does end on "scroll" to document (not mouse)', () => {
1041
- const onPressEnd = jest.fn();
1042
- const ref = React.createRef();
1043
-
1044
- const Component = () => {
1045
- const listener = usePress({onPressEnd});
1046
- return <a href="#" ref={ref} listeners={listener} />;
1047
- };
1048
- ReactDOM.render(<Component />, container);
1049
-
1050
- const target = createEventTarget(ref.current);
1051
- const targetDocument = createEventTarget(document);
1052
- target.pointerdown({pointerType: 'touch'});
1053
- targetDocument.scroll();
1054
- expect(onPressEnd).toHaveBeenCalledTimes(1);
1055
- });
1056
-
1057
- it('does end on "scroll" to a parent container (not mouse)', () => {
1058
- const onPressEnd = jest.fn();
1059
- const ref = React.createRef();
1060
- const containerRef = React.createRef();
1061
-
1062
- const Component = () => {
1063
- const listener = usePress({onPressEnd});
1064
- return (
1065
- <div ref={containerRef}>
1066
- <a ref={ref} listeners={listener} />
1067
- </div>
1068
- );
1069
- };
1070
- ReactDOM.render(<Component />, container);
1071
-
1072
- const target = createEventTarget(ref.current);
1073
- const targetContainer = createEventTarget(containerRef.current);
1074
- target.pointerdown({pointerType: 'touch'});
1075
- targetContainer.scroll();
1076
- expect(onPressEnd).toHaveBeenCalledTimes(1);
1077
- });
1078
-
1079
- it('does not end on "scroll" to an element outside', () => {
1080
- const onPressEnd = jest.fn();
1081
- const ref = React.createRef();
1082
- const outsideRef = React.createRef();
1083
-
1084
- const Component = () => {
1085
- const listener = usePress({onPressEnd});
1086
- return (
1087
- <div>
1088
- <a ref={ref} listeners={listener} />
1089
- <span ref={outsideRef} />
1090
- </div>
1091
- );
1092
- };
1093
- ReactDOM.render(<Component />, container);
1094
-
1095
- const target = createEventTarget(ref.current);
1096
- const targetOutside = createEventTarget(outsideRef.current);
1097
- target.pointerdown();
1098
- targetOutside.scroll();
1099
- expect(onPressEnd).not.toBeCalled();
1100
- });
1101
-
1102
- it('expect displayName to show up for event component', () => {
1103
- expect(PressResponder.displayName).toBe('Press');
1104
- });
1105
-
618
it('should not trigger an invariant in addRootEventTypes()', () => {
619
const ref = React.createRef();
620
packages/react-ui/events/src/dom/__tests__/PressLegacy-test.internal.js
new
+1121
@@ -0,0 +1,1121 @@
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
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+import {
13
+ buttonsType,
14
+ createEventTarget,
15
+ setPointerEvent,
16
+} from '../testing-library';
17
+
18
+let React;
19
+let ReactFeatureFlags;
20
+let ReactDOM;
21
+let PressResponder;
22
+let usePress;
23
+
24
+function initializeModules(hasPointerEvents) {
25
+ jest.resetModules();
26
+ setPointerEvent(hasPointerEvents);
27
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
28
+ ReactFeatureFlags.enableFlareAPI = true;
29
+ React = require('react');
30
+ ReactDOM = require('react-dom');
31
+ PressResponder = require('react-ui/events/press-legacy').PressResponder;
32
+ usePress = require('react-ui/events/press-legacy').usePress;
33
+}
34
+
35
+function removePressMoveStrings(eventString) {
36
+ if (eventString === 'onPressMove') {
37
+ return false;
38
+ }
39
+ return true;
40
+}
41
+
42
+const forcePointerEvents = true;
43
+const environmentTable = [[forcePointerEvents], [!forcePointerEvents]];
44
+
45
+const pointerTypesTable = [['mouse'], ['touch']];
46
+
47
+describe.each(environmentTable)('Press responder', hasPointerEvents => {
48
+ let container;
49
+
50
+ beforeEach(() => {
51
+ initializeModules(hasPointerEvents);
52
+ container = document.createElement('div');
53
+ document.body.appendChild(container);
54
+ });
55
+
56
+ afterEach(() => {
57
+ ReactDOM.render(null, container);
58
+ document.body.removeChild(container);
59
+ container = null;
60
+ });
61
+
62
+ describe('disabled', () => {
63
+ let onPressStart, onPress, onPressEnd, ref;
64
+
65
+ beforeEach(() => {
66
+ onPressStart = jest.fn();
67
+ onPress = jest.fn();
68
+ onPressEnd = jest.fn();
69
+ ref = React.createRef();
70
+ const Component = () => {
71
+ const listener = usePress({
72
+ disabled: true,
73
+ onPressStart,
74
+ onPress,
75
+ onPressEnd,
76
+ });
77
+ return <div ref={ref} listeners={listener} />;
78
+ };
79
+ ReactDOM.render(<Component />, container);
80
+ document.elementFromPoint = () => ref.current;
81
+ });
82
+
83
+ it('does not call callbacks', () => {
84
+ const target = createEventTarget(ref.current);
85
+ target.pointerdown();
86
+ target.pointerup();
87
+ expect(onPressStart).not.toBeCalled();
88
+ expect(onPress).not.toBeCalled();
89
+ expect(onPressEnd).not.toBeCalled();
90
+ });
91
+ });
92
+
93
+ describe('onPressStart', () => {
94
+ let onPressStart, ref;
95
+
96
+ beforeEach(() => {
97
+ onPressStart = jest.fn();
98
+ ref = React.createRef();
99
+ const Component = () => {
100
+ const listener = usePress({
101
+ onPressStart,
102
+ });
103
+ return <div ref={ref} listeners={listener} />;
104
+ };
105
+ ReactDOM.render(<Component />, container);
106
+ document.elementFromPoint = () => ref.current;
107
+ });
108
+
109
+ it.each(pointerTypesTable)(
110
+ 'is called after pointer down: %s',
111
+ pointerType => {
112
+ const target = createEventTarget(ref.current);
113
+ target.pointerdown({pointerType});
114
+ expect(onPressStart).toHaveBeenCalledTimes(1);
115
+ expect(onPressStart).toHaveBeenCalledWith(
116
+ expect.objectContaining({pointerType, type: 'pressstart'}),
117
+ );
118
+ },
119
+ );
120
+
121
+ it('is called after middle-button pointer down', () => {
122
+ const target = createEventTarget(ref.current);
123
+ target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
124
+ expect(onPressStart).toHaveBeenCalledTimes(1);
125
+ expect(onPressStart).toHaveBeenCalledWith(
126
+ expect.objectContaining({
127
+ buttons: buttonsType.middle,
128
+ pointerType: 'mouse',
129
+ type: 'pressstart',
130
+ }),
131
+ );
132
+ });
133
+
134
+ it('is not called after pointer move following middle-button press', () => {
135
+ const node = ref.current;
136
+ const target = createEventTarget(node);
137
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
138
+ target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
139
+ target.pointerup({pointerType: 'mouse'});
140
+ target.pointerhover({x: 110, y: 110});
141
+ target.pointerhover({x: 50, y: 50});
142
+ expect(onPressStart).toHaveBeenCalledTimes(1);
143
+ });
144
+
145
+ it('ignores any events not caused by primary/middle-click or touch/pen contact', () => {
146
+ const target = createEventTarget(ref.current);
147
+ target.pointerdown({buttons: buttonsType.secondary});
148
+ target.pointerup({buttons: buttonsType.secondary});
149
+ target.pointerdown({buttons: buttonsType.eraser});
150
+ target.pointerup({buttons: buttonsType.eraser});
151
+ expect(onPressStart).toHaveBeenCalledTimes(0);
152
+ });
153
+
154
+ it('is called once after "keydown" events for Enter', () => {
155
+ const target = createEventTarget(ref.current);
156
+ target.keydown({key: 'Enter'});
157
+ target.keydown({key: 'Enter'});
158
+ target.keydown({key: 'Enter'});
159
+ expect(onPressStart).toHaveBeenCalledTimes(1);
160
+ expect(onPressStart).toHaveBeenCalledWith(
161
+ expect.objectContaining({pointerType: 'keyboard', type: 'pressstart'}),
162
+ );
163
+ });
164
+
165
+ it('is called once after "keydown" events for Spacebar', () => {
166
+ const target = createEventTarget(ref.current);
167
+ const preventDefault = jest.fn();
168
+ target.keydown({key: ' ', preventDefault});
169
+ expect(preventDefault).toBeCalled();
170
+ target.keydown({key: ' ', preventDefault});
171
+ expect(onPressStart).toHaveBeenCalledTimes(1);
172
+ expect(onPressStart).toHaveBeenCalledWith(
173
+ expect.objectContaining({
174
+ pointerType: 'keyboard',
175
+ type: 'pressstart',
176
+ }),
177
+ );
178
+ });
179
+
180
+ it('is not called after "keydown" for other keys', () => {
181
+ const target = createEventTarget(ref.current);
182
+ target.keydown({key: 'a'});
183
+ expect(onPressStart).not.toBeCalled();
184
+ });
185
+ });
186
+
187
+ describe('onPressEnd', () => {
188
+ let onPressEnd, ref;
189
+
190
+ beforeEach(() => {
191
+ onPressEnd = jest.fn();
192
+ ref = React.createRef();
193
+ const Component = () => {
194
+ const listener = usePress({
195
+ onPressEnd,
196
+ });
197
+ return <div ref={ref} listeners={listener} />;
198
+ };
199
+ ReactDOM.render(<Component />, container);
200
+ document.elementFromPoint = () => ref.current;
201
+ });
202
+
203
+ it.each(pointerTypesTable)(
204
+ 'is called after pointer up: %s',
205
+ pointerType => {
206
+ const target = createEventTarget(ref.current);
207
+ target.pointerdown({pointerType});
208
+ target.pointerup({pointerType});
209
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
210
+ expect(onPressEnd).toHaveBeenCalledWith(
211
+ expect.objectContaining({pointerType, type: 'pressend'}),
212
+ );
213
+ },
214
+ );
215
+
216
+ it('is called after middle-button pointer up', () => {
217
+ const target = createEventTarget(ref.current);
218
+ target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
219
+ target.pointerup({pointerType: 'mouse'});
220
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
221
+ expect(onPressEnd).toHaveBeenCalledWith(
222
+ expect.objectContaining({
223
+ buttons: buttonsType.middle,
224
+ pointerType: 'mouse',
225
+ type: 'pressend',
226
+ }),
227
+ );
228
+ });
229
+
230
+ it('is called after "keyup" event for Enter', () => {
231
+ const target = createEventTarget(ref.current);
232
+ target.keydown({key: 'Enter'});
233
+ // click occurs before keyup
234
+ target.click();
235
+ target.keyup({key: 'Enter'});
236
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
237
+ expect(onPressEnd).toHaveBeenCalledWith(
238
+ expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}),
239
+ );
240
+ });
241
+
242
+ it('is called after "keyup" event for Spacebar', () => {
243
+ const target = createEventTarget(ref.current);
244
+ target.keydown({key: ' '});
245
+ target.keyup({key: ' '});
246
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
247
+ expect(onPressEnd).toHaveBeenCalledWith(
248
+ expect.objectContaining({pointerType: 'keyboard', type: 'pressend'}),
249
+ );
250
+ });
251
+
252
+ it('is not called after "keyup" event for other keys', () => {
253
+ const target = createEventTarget(ref.current);
254
+ target.keydown({key: 'Enter'});
255
+ target.keyup({key: 'a'});
256
+ expect(onPressEnd).not.toBeCalled();
257
+ });
258
+
259
+ it('is called with keyboard modifiers', () => {
260
+ const target = createEventTarget(ref.current);
261
+ target.keydown({key: 'Enter'});
262
+ target.keyup({
263
+ key: 'Enter',
264
+ metaKey: true,
265
+ ctrlKey: true,
266
+ altKey: true,
267
+ shiftKey: true,
268
+ });
269
+ expect(onPressEnd).toHaveBeenCalledWith(
270
+ expect.objectContaining({
271
+ pointerType: 'keyboard',
272
+ type: 'pressend',
273
+ metaKey: true,
274
+ ctrlKey: true,
275
+ altKey: true,
276
+ shiftKey: true,
277
+ }),
278
+ );
279
+ });
280
+ });
281
+
282
+ describe('onPressChange', () => {
283
+ let onPressChange, ref;
284
+
285
+ beforeEach(() => {
286
+ onPressChange = jest.fn();
287
+ ref = React.createRef();
288
+ const Component = () => {
289
+ const listener = usePress({
290
+ onPressChange,
291
+ });
292
+ return <div ref={ref} listeners={listener} />;
293
+ };
294
+ ReactDOM.render(<Component />, container);
295
+ document.elementFromPoint = () => ref.current;
296
+ });
297
+
298
+ it.each(pointerTypesTable)(
299
+ 'is called after pointer down and up: %s',
300
+ pointerType => {
301
+ const target = createEventTarget(ref.current);
302
+ target.pointerdown({pointerType});
303
+ expect(onPressChange).toHaveBeenCalledTimes(1);
304
+ expect(onPressChange).toHaveBeenCalledWith(true);
305
+ target.pointerup({pointerType});
306
+ expect(onPressChange).toHaveBeenCalledTimes(2);
307
+ expect(onPressChange).toHaveBeenCalledWith(false);
308
+ },
309
+ );
310
+
311
+ it('is called after valid "keydown" and "keyup" events', () => {
312
+ const target = createEventTarget(ref.current);
313
+ target.keydown({key: 'Enter'});
314
+ expect(onPressChange).toHaveBeenCalledTimes(1);
315
+ expect(onPressChange).toHaveBeenCalledWith(true);
316
+ target.keyup({key: 'Enter'});
317
+ expect(onPressChange).toHaveBeenCalledTimes(2);
318
+ expect(onPressChange).toHaveBeenCalledWith(false);
319
+ });
320
+ });
321
+
322
+ describe('onPress', () => {
323
+ let onPress, ref;
324
+
325
+ beforeEach(() => {
326
+ onPress = jest.fn();
327
+ ref = React.createRef();
328
+ const Component = () => {
329
+ const listener = usePress({
330
+ onPress,
331
+ });
332
+ return <div ref={ref} listeners={listener} />;
333
+ };
334
+ ReactDOM.render(<Component />, container);
335
+ ref.current.getBoundingClientRect = () => ({
336
+ top: 0,
337
+ left: 0,
338
+ bottom: 100,
339
+ right: 100,
340
+ });
341
+ document.elementFromPoint = () => ref.current;
342
+ });
343
+
344
+ it.each(pointerTypesTable)(
345
+ 'is called after pointer up: %s',
346
+ pointerType => {
347
+ const target = createEventTarget(ref.current);
348
+ target.pointerdown({pointerType});
349
+ target.pointerup({pointerType, x: 10, y: 10});
350
+ expect(onPress).toHaveBeenCalledTimes(1);
351
+ expect(onPress).toHaveBeenCalledWith(
352
+ expect.objectContaining({pointerType, type: 'press'}),
353
+ );
354
+ },
355
+ );
356
+
357
+ it('is not called after middle-button press', () => {
358
+ const target = createEventTarget(ref.current);
359
+ target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
360
+ target.pointerup({pointerType: 'mouse'});
361
+ expect(onPress).not.toHaveBeenCalled();
362
+ });
363
+
364
+ it('is called after valid "keyup" event', () => {
365
+ const target = createEventTarget(ref.current);
366
+ target.keydown({key: 'Enter'});
367
+ target.keyup({key: 'Enter'});
368
+ expect(onPress).toHaveBeenCalledTimes(1);
369
+ expect(onPress).toHaveBeenCalledWith(
370
+ expect.objectContaining({pointerType: 'keyboard', type: 'press'}),
371
+ );
372
+ });
373
+
374
+ it('is not called after invalid "keyup" event', () => {
375
+ const inputRef = React.createRef();
376
+ const Component = () => {
377
+ const listener = usePress({onPress});
378
+ return <input ref={inputRef} listeners={listener} />;
379
+ };
380
+ ReactDOM.render(<Component />, container);
381
+ const target = createEventTarget(inputRef.current);
382
+ target.keydown({key: 'Enter'});
383
+ target.keyup({key: 'Enter'});
384
+ target.keydown({key: ' '});
385
+ target.keyup({key: ' '});
386
+ expect(onPress).not.toBeCalled();
387
+ });
388
+
389
+ it('is called with modifier keys', () => {
390
+ const target = createEventTarget(ref.current);
391
+ target.pointerdown({metaKey: true, pointerType: 'mouse'});
392
+ target.pointerup({metaKey: true, pointerType: 'mouse'});
393
+ expect(onPress).toHaveBeenCalledWith(
394
+ expect.objectContaining({
395
+ pointerType: 'mouse',
396
+ type: 'press',
397
+ metaKey: true,
398
+ }),
399
+ );
400
+ });
401
+
402
+ it('is called if target rect is not right but the target is (for mouse events)', () => {
403
+ const buttonRef = React.createRef();
404
+ const divRef = React.createRef();
405
+
406
+ const Component = () => {
407
+ const listener = usePress({onPress});
408
+ return (
409
+ <div ref={divRef} listeners={listener}>
410
+ <button ref={buttonRef} />
411
+ </div>
412
+ );
413
+ };
414
+ ReactDOM.render(<Component />, container);
415
+
416
+ const target = createEventTarget(divRef.current);
417
+ target.setBoundingClientRect({x: 0, y: 0, width: 0, height: 0});
418
+ const innerTarget = createEventTarget(buttonRef.current);
419
+ innerTarget.pointerdown({pointerType: 'mouse'});
420
+ innerTarget.pointerup({pointerType: 'mouse'});
421
+ expect(onPress).toBeCalled();
422
+ });
423
+
424
+ it('is called once after virtual screen reader "click" event', () => {
425
+ const target = createEventTarget(ref.current);
426
+ const preventDefault = jest.fn();
427
+ target.virtualclick({preventDefault});
428
+ expect(preventDefault).toBeCalled();
429
+ expect(onPress).toHaveBeenCalledTimes(1);
430
+ expect(onPress).toHaveBeenCalledWith(
431
+ expect.objectContaining({
432
+ pointerType: 'keyboard',
433
+ type: 'press',
434
+ }),
435
+ );
436
+ });
437
+ });
438
+
439
+ describe('onPressMove', () => {
440
+ let onPressMove, ref;
441
+
442
+ beforeEach(() => {
443
+ onPressMove = jest.fn();
444
+ ref = React.createRef();
445
+ const Component = () => {
446
+ const listener = usePress({
447
+ onPressMove,
448
+ });
449
+ return <div ref={ref} listeners={listener} />;
450
+ };
451
+ ReactDOM.render(<Component />, container);
452
+ ref.current.getBoundingClientRect = () => ({
453
+ top: 0,
454
+ left: 0,
455
+ bottom: 100,
456
+ right: 100,
457
+ });
458
+ document.elementFromPoint = () => ref.current;
459
+ });
460
+
461
+ it.each(pointerTypesTable)(
462
+ 'is called after pointer move: %s',
463
+ pointerType => {
464
+ const node = ref.current;
465
+ const target = createEventTarget(node);
466
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
467
+ target.pointerdown({pointerType});
468
+ target.pointermove({pointerType, x: 10, y: 10});
469
+ target.pointermove({pointerType, x: 20, y: 20});
470
+ expect(onPressMove).toHaveBeenCalledTimes(2);
471
+ expect(onPressMove).toHaveBeenCalledWith(
472
+ expect.objectContaining({pointerType, type: 'pressmove'}),
473
+ );
474
+ },
475
+ );
476
+
477
+ it('is not called if pointer move occurs during keyboard press', () => {
478
+ const target = createEventTarget(ref.current);
479
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
480
+ target.keydown({key: 'Enter'});
481
+ target.pointermove({
482
+ buttons: buttonsType.none,
483
+ pointerType: 'mouse',
484
+ x: 10,
485
+ y: 10,
486
+ });
487
+ expect(onPressMove).not.toBeCalled();
488
+ });
489
+ });
490
+
491
+ describe.each(pointerTypesTable)('press with movement: %s', pointerType => {
492
+ let events, ref, outerRef;
493
+
494
+ beforeEach(() => {
495
+ events = [];
496
+ ref = React.createRef();
497
+ outerRef = React.createRef();
498
+ const createEventHandler = msg => () => {
499
+ events.push(msg);
500
+ };
501
+ const Component = () => {
502
+ const listener = usePress({
503
+ onPress: createEventHandler('onPress'),
504
+ onPressChange: createEventHandler('onPressChange'),
505
+ onPressMove: createEventHandler('onPressMove'),
506
+ onPressStart: createEventHandler('onPressStart'),
507
+ onPressEnd: createEventHandler('onPressEnd'),
508
+ });
509
+ return (
510
+ <div ref={outerRef}>
511
+ <div ref={ref} listeners={listener} />
512
+ </div>
513
+ );
514
+ };
515
+ ReactDOM.render(<Component />, container);
516
+ document.elementFromPoint = () => ref.current;
517
+ });
518
+
519
+ const rectMock = {width: 100, height: 100, x: 50, y: 50};
520
+ const pressRectOffset = 20;
521
+ const coordinatesInside = {
522
+ x: rectMock.x - pressRectOffset,
523
+ y: rectMock.y - pressRectOffset,
524
+ };
525
+ const coordinatesOutside = {
526
+ x: rectMock.x - pressRectOffset - 1,
527
+ y: rectMock.y - pressRectOffset - 1,
528
+ };
529
+
530
+ describe('within bounds of hit rect', () => {
531
+ /** ┌──────────────────┐
532
+ * │ ┌────────────┐ │
533
+ * │ │ VisualRect │ │
534
+ * │ └────────────┘ │
535
+ * │ HitRect X │ <= Move to X and release
536
+ * └──────────────────┘
537
+ */
538
+ it('"onPress*" events are called immediately', () => {
539
+ const target = createEventTarget(ref.current);
540
+ target.setBoundingClientRect(rectMock);
541
+ target.pointerdown({pointerType});
542
+ target.pointermove({pointerType, ...coordinatesInside});
543
+ target.pointerup({pointerType, ...coordinatesInside});
544
+ expect(events).toEqual([
545
+ 'onPressStart',
546
+ 'onPressChange',
547
+ 'onPressMove',
548
+ 'onPressEnd',
549
+ 'onPressChange',
550
+ 'onPress',
551
+ ]);
552
+ });
553
+
554
+ it('"onPress*" events are correctly called with target change', () => {
555
+ const target = createEventTarget(ref.current);
556
+ const outerTarget = createEventTarget(outerRef.current);
557
+ target.setBoundingClientRect(rectMock);
558
+ target.pointerdown({pointerType});
559
+ target.pointermove({pointerType, ...coordinatesInside});
560
+ // TODO: this sequence may differ in the future between PointerEvent and mouse fallback when
561
+ // use 'setPointerCapture'.
562
+ if (pointerType === 'touch') {
563
+ target.pointermove({pointerType, ...coordinatesOutside});
564
+ } else {
565
+ outerTarget.pointermove({pointerType, ...coordinatesOutside});
566
+ }
567
+ target.pointermove({pointerType, ...coordinatesInside});
568
+ target.pointerup({pointerType, ...coordinatesInside});
569
+
570
+ expect(events.filter(removePressMoveStrings)).toEqual([
571
+ 'onPressStart',
572
+ 'onPressChange',
573
+ 'onPressEnd',
574
+ 'onPressChange',
575
+ 'onPressStart',
576
+ 'onPressChange',
577
+ 'onPressEnd',
578
+ 'onPressChange',
579
+ 'onPress',
580
+ ]);
581
+ });
582
+
583
+ it('press retention offset can be configured', () => {
584
+ let localEvents = [];
585
+ const localRef = React.createRef();
586
+ const createEventHandler = msg => () => {
587
+ localEvents.push(msg);
588
+ };
589
+ const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40};
590
+
591
+ const Component = () => {
592
+ const listener = usePress({
593
+ onPress: createEventHandler('onPress'),
594
+ onPressChange: createEventHandler('onPressChange'),
595
+ onPressMove: createEventHandler('onPressMove'),
596
+ onPressStart: createEventHandler('onPressStart'),
597
+ onPressEnd: createEventHandler('onPressEnd'),
598
+ pressRetentionOffset,
599
+ });
600
+ return <div ref={localRef} listeners={listener} />;
601
+ };
602
+ ReactDOM.render(<Component />, container);
603
+
604
+ const target = createEventTarget(localRef.current);
605
+ target.setBoundingClientRect(rectMock);
606
+ target.pointerdown({pointerType});
607
+ target.pointermove({
608
+ pointerType,
609
+ x: rectMock.x,
610
+ y: rectMock.y,
611
+ });
612
+ target.pointerup({pointerType, ...coordinatesInside});
613
+ expect(localEvents).toEqual([
614
+ 'onPressStart',
615
+ 'onPressChange',
616
+ 'onPressMove',
617
+ 'onPressEnd',
618
+ 'onPressChange',
619
+ 'onPress',
620
+ ]);
621
+ });
622
+
623
+ it('responder region accounts for decrease in element dimensions', () => {
624
+ const target = createEventTarget(ref.current);
625
+ target.setBoundingClientRect(rectMock);
626
+ target.pointerdown({pointerType});
627
+ // emulate smaller dimensions change on activation
628
+ target.setBoundingClientRect({width: 80, height: 80, y: 60, x: 60});
629
+ const coordinates = {x: rectMock.x, y: rectMock.y};
630
+ // move to an area within the pre-activation region
631
+ target.pointermove({pointerType, ...coordinates});
632
+ target.pointerup({pointerType, ...coordinates});
633
+ expect(events).toEqual([
634
+ 'onPressStart',
635
+ 'onPressChange',
636
+ 'onPressMove',
637
+ 'onPressEnd',
638
+ 'onPressChange',
639
+ 'onPress',
640
+ ]);
641
+ });
642
+
643
+ it('responder region accounts for increase in element dimensions', () => {
644
+ const target = createEventTarget(ref.current);
645
+ target.setBoundingClientRect(rectMock);
646
+ target.pointerdown({pointerType});
647
+ // emulate larger dimensions change on activation
648
+ target.setBoundingClientRect({width: 200, height: 200, y: 0, x: 0});
649
+ const coordinates = {x: rectMock.x - 50, y: rectMock.y - 50};
650
+ // move to an area within the post-activation region
651
+ target.pointermove({pointerType, ...coordinates});
652
+ target.pointerup({pointerType, ...coordinates});
653
+ expect(events).toEqual([
654
+ 'onPressStart',
655
+ 'onPressChange',
656
+ 'onPressMove',
657
+ 'onPressEnd',
658
+ 'onPressChange',
659
+ 'onPress',
660
+ ]);
661
+ });
662
+ });
663
+
664
+ describe('beyond bounds of hit rect', () => {
665
+ /** ┌──────────────────┐
666
+ * │ ┌────────────┐ │
667
+ * │ │ VisualRect │ │
668
+ * │ └────────────┘ │
669
+ * │ HitRect │
670
+ * └──────────────────┘
671
+ * X <= Move to X and release
672
+ */
673
+ it('"onPress" is not called on release', () => {
674
+ const target = createEventTarget(ref.current);
675
+ const targetContainer = createEventTarget(container);
676
+ target.setBoundingClientRect(rectMock);
677
+ target.pointerdown({pointerType});
678
+ target.pointermove({pointerType, ...coordinatesInside});
679
+ if (pointerType === 'mouse') {
680
+ // TODO: use setPointerCapture so this is only true for fallback mouse events.
681
+ targetContainer.pointermove({pointerType, ...coordinatesOutside});
682
+ targetContainer.pointerup({pointerType, ...coordinatesOutside});
683
+ } else {
684
+ target.pointermove({pointerType, ...coordinatesOutside});
685
+ target.pointerup({pointerType, ...coordinatesOutside});
686
+ }
687
+ expect(events.filter(removePressMoveStrings)).toEqual([
688
+ 'onPressStart',
689
+ 'onPressChange',
690
+ 'onPressEnd',
691
+ 'onPressChange',
692
+ ]);
693
+ });
694
+ });
695
+
696
+ it('"onPress" is called on re-entry to hit rect', () => {
697
+ const target = createEventTarget(ref.current);
698
+ const targetContainer = createEventTarget(container);
699
+ target.setBoundingClientRect(rectMock);
700
+ target.pointerdown({pointerType});
701
+ target.pointermove({pointerType, ...coordinatesInside});
702
+ if (pointerType === 'mouse') {
703
+ // TODO: use setPointerCapture so this is only true for fallback mouse events.
704
+ targetContainer.pointermove({pointerType, ...coordinatesOutside});
705
+ } else {
706
+ target.pointermove({pointerType, ...coordinatesOutside});
707
+ }
708
+ target.pointermove({pointerType, ...coordinatesInside});
709
+ target.pointerup({pointerType, ...coordinatesInside});
710
+
711
+ expect(events).toEqual([
712
+ 'onPressStart',
713
+ 'onPressChange',
714
+ 'onPressMove',
715
+ 'onPressEnd',
716
+ 'onPressChange',
717
+ 'onPressStart',
718
+ 'onPressChange',
719
+ 'onPressEnd',
720
+ 'onPressChange',
721
+ 'onPress',
722
+ ]);
723
+ });
724
+ });
725
+
726
+ describe('nested responders', () => {
727
+ if (hasPointerEvents) {
728
+ it('dispatch events in the correct order', () => {
729
+ const events = [];
730
+ const ref = React.createRef();
731
+ const createEventHandler = msg => () => {
732
+ events.push(msg);
733
+ };
734
+
735
+ const Inner = () => {
736
+ const listener = usePress({
737
+ onPress: createEventHandler('inner: onPress'),
738
+ onPressChange: createEventHandler('inner: onPressChange'),
739
+ onPressMove: createEventHandler('inner: onPressMove'),
740
+ onPressStart: createEventHandler('inner: onPressStart'),
741
+ onPressEnd: createEventHandler('inner: onPressEnd'),
742
+ });
743
+ return (
744
+ <div
745
+ ref={ref}
746
+ listeners={listener}
747
+ onPointerDown={createEventHandler('pointerdown')}
748
+ onPointerUp={createEventHandler('pointerup')}
749
+ onKeyDown={createEventHandler('keydown')}
750
+ onKeyUp={createEventHandler('keyup')}
751
+ />
752
+ );
753
+ };
754
+
755
+ const Outer = () => {
756
+ const listener = usePress({
757
+ onPress: createEventHandler('outer: onPress'),
758
+ onPressChange: createEventHandler('outer: onPressChange'),
759
+ onPressMove: createEventHandler('outer: onPressMove'),
760
+ onPressStart: createEventHandler('outer: onPressStart'),
761
+ onPressEnd: createEventHandler('outer: onPressEnd'),
762
+ });
763
+ return (
764
+ <div listeners={listener}>
765
+ <Inner />
766
+ </div>
767
+ );
768
+ };
769
+ ReactDOM.render(<Outer />, container);
770
+
771
+ const target = createEventTarget(ref.current);
772
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
773
+ target.pointerdown();
774
+ target.pointerup();
775
+ expect(events).toEqual([
776
+ 'inner: onPressStart',
777
+ 'inner: onPressChange',
778
+ 'pointerdown',
779
+ 'inner: onPressEnd',
780
+ 'inner: onPressChange',
781
+ 'inner: onPress',
782
+ 'pointerup',
783
+ ]);
784
+ });
785
+ }
786
+
787
+ describe('correctly not propagate', () => {
788
+ it('for onPress', () => {
789
+ const ref = React.createRef();
790
+ const onPress = jest.fn();
791
+
792
+ const Inner = () => {
793
+ const listener = usePress({onPress});
794
+ return <div ref={ref} listeners={listener} />;
795
+ };
796
+
797
+ const Outer = () => {
798
+ const listener = usePress({onPress});
799
+ return (
800
+ <div listeners={listener}>
801
+ <Inner />
802
+ </div>
803
+ );
804
+ };
805
+ ReactDOM.render(<Outer />, container);
806
+
807
+ const target = createEventTarget(ref.current);
808
+ target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
809
+ target.pointerdown();
810
+ target.pointerup();
811
+ expect(onPress).toHaveBeenCalledTimes(1);
812
+ });
813
+
814
+ it('for onPressStart/onPressEnd', () => {
815
+ const ref = React.createRef();
816
+ const onPressStart = jest.fn();
817
+ const onPressEnd = jest.fn();
818
+
819
+ const Inner = () => {
820
+ const listener = usePress({onPressStart, onPressEnd});
821
+ return <div ref={ref} listeners={listener} />;
822
+ };
823
+
824
+ const Outer = () => {
825
+ const listener = usePress({onPressStart, onPressEnd});
826
+ return (
827
+ <div listeners={listener}>
828
+ <Inner />
829
+ </div>
830
+ );
831
+ };
832
+ ReactDOM.render(<Outer />, container);
833
+
834
+ const target = createEventTarget(ref.current);
835
+ target.pointerdown();
836
+ expect(onPressStart).toHaveBeenCalledTimes(1);
837
+ expect(onPressEnd).toHaveBeenCalledTimes(0);
838
+ target.pointerup();
839
+ expect(onPressStart).toHaveBeenCalledTimes(1);
840
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
841
+ });
842
+
843
+ it('for onPressChange', () => {
844
+ const ref = React.createRef();
845
+ const onPressChange = jest.fn();
846
+
847
+ const Inner = () => {
848
+ const listener = usePress({onPressChange});
849
+ return <div ref={ref} listeners={listener} />;
850
+ };
851
+
852
+ const Outer = () => {
853
+ const listener = usePress({onPressChange});
854
+ return (
855
+ <div listeners={listener}>
856
+ <Inner />
857
+ </div>
858
+ );
859
+ };
860
+ ReactDOM.render(<Outer />, container);
861
+
862
+ const target = createEventTarget(ref.current);
863
+ target.pointerdown();
864
+ expect(onPressChange).toHaveBeenCalledTimes(1);
865
+ target.pointerup();
866
+ expect(onPressChange).toHaveBeenCalledTimes(2);
867
+ });
868
+ });
869
+ });
870
+
871
+ describe('link components', () => {
872
+ it('prevents native behavior by default', () => {
873
+ const onPress = jest.fn();
874
+ const preventDefault = jest.fn();
875
+ const ref = React.createRef();
876
+
877
+ const Component = () => {
878
+ const listener = usePress({onPress});
879
+ return <a href="#" ref={ref} listeners={listener} />;
880
+ };
881
+ ReactDOM.render(<Component />, container);
882
+
883
+ const target = createEventTarget(ref.current);
884
+ target.pointerdown();
885
+ target.pointerup({preventDefault});
886
+ expect(preventDefault).toBeCalled();
887
+ expect(onPress).toHaveBeenCalledWith(
888
+ expect.objectContaining({defaultPrevented: true}),
889
+ );
890
+ });
891
+
892
+ it('prevents native behaviour for keyboard events by default', () => {
893
+ const onPress = jest.fn();
894
+ const preventDefault = jest.fn();
895
+ const ref = React.createRef();
896
+
897
+ const Component = () => {
898
+ const listener = usePress({onPress});
899
+ return <a href="#" ref={ref} listeners={listener} />;
900
+ };
901
+ ReactDOM.render(<Component />, container);
902
+
903
+ const target = createEventTarget(ref.current);
904
+ target.keydown({key: 'Enter'});
905
+ target.click({preventDefault});
906
+ target.keyup({key: 'Enter'});
907
+ expect(preventDefault).toBeCalled();
908
+ expect(onPress).toHaveBeenCalledWith(
909
+ expect.objectContaining({defaultPrevented: true}),
910
+ );
911
+ });
912
+
913
+ it('deeply prevents native behaviour by default', () => {
914
+ const onPress = jest.fn();
915
+ const preventDefault = jest.fn();
916
+ const buttonRef = React.createRef();
917
+
918
+ const Component = () => {
919
+ const listener = usePress({onPress});
920
+ return (
921
+ <a href="#">
922
+ <button ref={buttonRef} listeners={listener} />
923
+ </a>
924
+ );
925
+ };
926
+ ReactDOM.render(<Component />, container);
927
+
928
+ const target = createEventTarget(buttonRef.current);
929
+ target.pointerdown();
930
+ target.pointerup({preventDefault});
931
+ expect(preventDefault).toBeCalled();
932
+ });
933
+
934
+ it('prevents native behaviour by default with nested elements', () => {
935
+ const onPress = jest.fn();
936
+ const preventDefault = jest.fn();
937
+ const ref = React.createRef();
938
+
939
+ const Component = () => {
940
+ const listener = usePress({onPress});
941
+ return (
942
+ <a href="#" listeners={listener}>
943
+ <div ref={ref} />
944
+ </a>
945
+ );
946
+ };
947
+ ReactDOM.render(<Component />, container);
948
+
949
+ const target = createEventTarget(ref.current);
950
+ target.pointerdown();
951
+ target.pointerup({preventDefault});
952
+ expect(preventDefault).toBeCalled();
953
+ expect(onPress).toHaveBeenCalledWith(
954
+ expect.objectContaining({defaultPrevented: true}),
955
+ );
956
+ });
957
+
958
+ it('uses native behaviour for interactions with modifier keys', () => {
959
+ const onPress = jest.fn();
960
+ const preventDefault = jest.fn();
961
+ const ref = React.createRef();
962
+
963
+ const Component = () => {
964
+ const listener = usePress({onPress});
965
+ return <a href="#" ref={ref} listeners={listener} />;
966
+ };
967
+ ReactDOM.render(<Component />, container);
968
+
969
+ ['metaKey', 'ctrlKey', 'shiftKey'].forEach(modifierKey => {
970
+ const target = createEventTarget(ref.current);
971
+ target.pointerdown({[modifierKey]: true});
972
+ target.pointerup({[modifierKey]: true, preventDefault});
973
+ expect(preventDefault).not.toBeCalled();
974
+ expect(onPress).toHaveBeenCalledWith(
975
+ expect.objectContaining({defaultPrevented: false}),
976
+ );
977
+ });
978
+ });
979
+
980
+ it('uses native behaviour for pointer events if preventDefault is false', () => {
981
+ const onPress = jest.fn();
982
+ const preventDefault = jest.fn();
983
+ const ref = React.createRef();
984
+
985
+ const Component = () => {
986
+ const listener = usePress({onPress, preventDefault: false});
987
+ return <a href="#" ref={ref} listeners={listener} />;
988
+ };
989
+ ReactDOM.render(<Component />, container);
990
+
991
+ const target = createEventTarget(ref.current);
992
+ target.pointerdown();
993
+ target.pointerup({preventDefault});
994
+ expect(preventDefault).not.toBeCalled();
995
+ expect(onPress).toHaveBeenCalledWith(
996
+ expect.objectContaining({defaultPrevented: false}),
997
+ );
998
+ });
999
+
1000
+ it('uses native behaviour for keyboard events if preventDefault is false', () => {
1001
+ const onPress = jest.fn();
1002
+ const preventDefault = jest.fn();
1003
+ const ref = React.createRef();
1004
+
1005
+ const Component = () => {
1006
+ const listener = usePress({onPress, preventDefault: false});
1007
+ return <a href="#" ref={ref} listeners={listener} />;
1008
+ };
1009
+ ReactDOM.render(<Component />, container);
1010
+
1011
+ const target = createEventTarget(ref.current);
1012
+ target.keydown({key: 'Enter'});
1013
+ target.click({preventDefault});
1014
+ target.keyup({key: 'Enter'});
1015
+ expect(preventDefault).not.toBeCalled();
1016
+ expect(onPress).toHaveBeenCalledWith(
1017
+ expect.objectContaining({defaultPrevented: false}),
1018
+ );
1019
+ });
1020
+ });
1021
+
1022
+ describe('responder cancellation', () => {
1023
+ it.each(pointerTypesTable)('ends on pointer cancel', pointerType => {
1024
+ const onPressEnd = jest.fn();
1025
+ const ref = React.createRef();
1026
+
1027
+ const Component = () => {
1028
+ const listener = usePress({onPressEnd});
1029
+ return <a href="#" ref={ref} listeners={listener} />;
1030
+ };
1031
+ ReactDOM.render(<Component />, container);
1032
+
1033
+ const target = createEventTarget(ref.current);
1034
+ target.pointerdown({pointerType});
1035
+ target.pointercancel({pointerType});
1036
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
1037
+ });
1038
+ });
1039
+
1040
+ it('does end on "scroll" to document (not mouse)', () => {
1041
+ const onPressEnd = jest.fn();
1042
+ const ref = React.createRef();
1043
+
1044
+ const Component = () => {
1045
+ const listener = usePress({onPressEnd});
1046
+ return <a href="#" ref={ref} listeners={listener} />;
1047
+ };
1048
+ ReactDOM.render(<Component />, container);
1049
+
1050
+ const target = createEventTarget(ref.current);
1051
+ const targetDocument = createEventTarget(document);
1052
+ target.pointerdown({pointerType: 'touch'});
1053
+ targetDocument.scroll();
1054
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
1055
+ });
1056
+
1057
+ it('does end on "scroll" to a parent container (not mouse)', () => {
1058
+ const onPressEnd = jest.fn();
1059
+ const ref = React.createRef();
1060
+ const containerRef = React.createRef();
1061
+
1062
+ const Component = () => {
1063
+ const listener = usePress({onPressEnd});
1064
+ return (
1065
+ <div ref={containerRef}>
1066
+ <a ref={ref} listeners={listener} />
1067
+ </div>
1068
+ );
1069
+ };
1070
+ ReactDOM.render(<Component />, container);
1071
+
1072
+ const target = createEventTarget(ref.current);
1073
+ const targetContainer = createEventTarget(containerRef.current);
1074
+ target.pointerdown({pointerType: 'touch'});
1075
+ targetContainer.scroll();
1076
+ expect(onPressEnd).toHaveBeenCalledTimes(1);
1077
+ });
1078
+
1079
+ it('does not end on "scroll" to an element outside', () => {
1080
+ const onPressEnd = jest.fn();
1081
+ const ref = React.createRef();
1082
+ const outsideRef = React.createRef();
1083
+
1084
+ const Component = () => {
1085
+ const listener = usePress({onPressEnd});
1086
+ return (
1087
+ <div>
1088
+ <a ref={ref} listeners={listener} />
1089
+ <span ref={outsideRef} />
1090
+ </div>
1091
+ );
1092
+ };
1093
+ ReactDOM.render(<Component />, container);
1094
+
1095
+ const target = createEventTarget(ref.current);
1096
+ const targetOutside = createEventTarget(outsideRef.current);
1097
+ target.pointerdown();
1098
+ targetOutside.scroll();
1099
+ expect(onPressEnd).not.toBeCalled();
1100
+ });
1101
+
1102
+ it('expect displayName to show up for event component', () => {
1103
+ expect(PressResponder.displayName).toBe('Press');
1104
+ });
1105
+
1106
+ it('should not trigger an invariant in addRootEventTypes()', () => {
1107
+ const ref = React.createRef();
1108
+
1109
+ const Component = () => {
1110
+ const listener = usePress();
1111
+ return <button ref={ref} listeners={listener} />;
1112
+ };
1113
+ ReactDOM.render(<Component />, container);
1114
+
1115
+ const target = createEventTarget(ref.current);
1116
+ target.pointerdown();
1117
+ target.pointermove();
1118
+ target.pointerup();
1119
+ target.pointerdown();
1120
+ });
1121
+});
packages/react-ui/events/src/dom/__tests__/Tap-test.internal.js
+12
-4
@@ -703,7 +703,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
703
target.pointerdown();
704
target.pointerup({preventDefault});
705
expect(preventDefault).toBeCalled();
706
- expect(onTapEnd).toBeCalled();
706
+ expect(onTapEnd).toHaveBeenCalledWith(
707
+ expect.objectContaining({defaultPrevented: true}),
708
+ );
709
});
710
711
test('prevents native behaviour by default (inner target)', () => {
@@ -711,7 +713,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
713
innerTarget.pointerdown();
714
innerTarget.pointerup({preventDefault});
715
expect(preventDefault).toBeCalled();
714
- expect(onTapEnd).toBeCalled();
716
+ expect(onTapEnd).toHaveBeenCalledWith(
717
+ expect.objectContaining({defaultPrevented: true}),
718
+ );
719
});
720
721
test('allows native behaviour by default (modifier keys)', () => {
@@ -720,7 +724,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
724
target.pointerdown({[modifierKey]: true});
725
target.pointerup({[modifierKey]: true, preventDefault});
726
expect(preventDefault).not.toBeCalled();
723
- expect(onTapEnd).toBeCalled();
727
+ expect(onTapEnd).toHaveBeenCalledWith(
728
+ expect.objectContaining({defaultPrevented: false}),
729
+ );
730
});
731
});
732
@@ -731,7 +737,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
737
target.pointerdown();
738
target.pointerup({preventDefault});
739
expect(preventDefault).not.toBeCalled();
734
- expect(onTapEnd).toBeCalled();
740
+ expect(onTapEnd).toHaveBeenCalledWith(
741
+ expect.objectContaining({defaultPrevented: false}),
742
+ );
743
});
744
});
745
});
packages/react-ui/npm/drag.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-drag.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/drag.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-drag.development.js');
6
+ module.exports = require('./cjs/react-ui-events/drag.development.js');
7
}
packages/react-ui/npm/focus.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-focus.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/focus.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-focus.development.js');
6
+ module.exports = require('./cjs/react-ui-events/focus.development.js');
7
}
packages/react-ui/npm/hover.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-hover.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/hover.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-hover.development.js');
6
+ module.exports = require('./cjs/react-ui-events/hover.development.js');
7
}
packages/react-ui/npm/input.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-input.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/input.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-input.development.js');
6
+ module.exports = require('./cjs/react-ui-events/input.development.js');
7
}
packages/react-ui/npm/keyboard.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-keyboard.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/keyboard.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-keyboard.development.js');
6
+ module.exports = require('./cjs/react-ui-events/keyboard.development.js');
7
}
packages/react-ui/npm/press-legacy.js
new
+7
@@ -0,0 +1,7 @@
1
+'use strict';
2
+
3
+if (process.env.NODE_ENV === 'production') {
4
+ module.exports = require('./cjs/react-ui-events/press-legacy.production.min.js');
5
+} else {
6
+ module.exports = require('./cjs/react-ui-events/press-legacy.development.js');
7
+}
packages/react-ui/npm/press.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-press.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/press.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-press.development.js');
6
+ module.exports = require('./cjs/react-ui-events/press.development.js');
7
}
packages/react-ui/npm/scroll.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-scroll.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/scroll.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-scroll.development.js');
6
+ module.exports = require('./cjs/react-ui-events/scroll.development.js');
7
}
packages/react-ui/npm/swipe.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-swipe.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/swipe.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-swipe.development.js');
6
+ module.exports = require('./cjs/react-ui-events/swipe.development.js');
7
}
packages/react-ui/npm/tap.js
+2
-2
@@ -1,7 +1,7 @@
1
'use strict';
2
3
if (process.env.NODE_ENV === 'production') {
4
- module.exports = require('./cjs/react-events-tap.production.min.js');
4
+ module.exports = require('./cjs/react-ui-events/tap.production.min.js');
5
} else {
6
- module.exports = require('./cjs/react-events-tap.development.js');
6
+ module.exports = require('./cjs/react-ui-events/tap.development.js');
7
}
packages/react-ui/package.json
+1
@@ -20,6 +20,7 @@
20
"events/input.js",
21
"events/keyboard.js",
22
"events/press.js",
23
+ "events/press-legacy.js",
24
"events/scroll.js",
25
"events/swipe.js",
26
"events/tap.js",
scripts/rollup/bundles.js
+15
@@ -597,6 +597,21 @@ const bundles = [
597
moduleType: NON_FIBER_RENDERER,
598
entry: 'react-ui/events/press',
599
global: 'ReactEventsPress',
600
+ externals: ['react', 'react-ui/events/tap', 'react-ui/events/keyboard'],
601
+ },
602
+
603
+ {
604
+ bundleTypes: [
605
+ UMD_DEV,
606
+ UMD_PROD,
607
+ NODE_DEV,
608
+ NODE_PROD,
609
+ FB_WWW_DEV,
610
+ FB_WWW_PROD,
611
+ ],
612
+ moduleType: NON_FIBER_RENDERER,
613
+ entry: 'react-ui/events/press-legacy',
614
+ global: 'ReactEventsPressLegacy',
615
externals: ['react'],
616
},
617
scripts/rollup/modules.js
+2
@@ -23,6 +23,8 @@ const importSideEffects = Object.freeze({
23
const knownGlobals = Object.freeze({
24
react: 'React',
25
'react-dom': 'ReactDOM',
26
+ 'react-ui/events/keyboard': 'ReactEventsKeyboard',
27
+ 'react-ui/events/tap': 'ReactEventsTap',
28
scheduler: 'Scheduler',
29
'scheduler/tracing': 'SchedulerTracing',
30
'scheduler/unstable_mock': 'SchedulerMock',
scripts/shared/pathsByLanguageVersion.js
+2
-1
@@ -12,7 +12,8 @@ const esNextPaths = [
12
'packages/*/*.js',
13
// Source files
14
'packages/*/src/**/*.js',
15
- 'packages/react-ui/*/src/**/*.js',
15
+ 'packages/react-ui/**/*.js',
16
+ 'packages/react-ui/**/*.js',
17
'packages/legacy-events/**/*.js',
18
'packages/shared/**/*.js',
19
// Shims and Flow environment