@samitouri / QOS-React-2 / commits / 3af05de1aa

[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