[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'.
This commit is contained in:
Nicolas Gallagher
2019-09-16 14:36:27 -07:00
committed by GitHub
parent 494300b366
commit 3af05de1aa
26 changed files with 2363 additions and 1437 deletions

View File

@@ -7,11 +7,11 @@
* @flow
*/
import type {KeyboardEvent} from 'react-ui/events/src/dom/Keyboard';
import type {KeyboardEvent} from 'react-ui/events/keyboard';
import React from 'react';
import {tabFocusableImpl} from './TabbableScope';
import {useKeyboard} from '../../events/keyboard';
import {useKeyboard} from 'react-ui/events/keyboard';
type GridComponentProps = {
children: React.Node,

View File

@@ -8,16 +8,17 @@
*/
import type {ReactScopeMethods} from 'shared/ReactTypes';
import type {KeyboardEvent} from 'react-ui/events/src/dom/Keyboard';
import type {KeyboardEvent} from 'react-ui/events/keyboard';
import React from 'react';
import {TabbableScope} from './TabbableScope';
import {useKeyboard} from '../../events/keyboard';
import {useKeyboard} from 'react-ui/events/keyboard';
type TabFocusControllerProps = {
children: React.Node,
contain?: boolean,
};
const {useRef} = React;
function getTabbableNodes(scope: ReactScopeMethods) {

View File

@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
module.exports = require('./src/dom/PressLegacy');

View File

@@ -27,6 +27,7 @@ type KeyboardProps = {|
onClick?: (e: KeyboardEvent) => ?boolean,
onKeyDown?: (e: KeyboardEvent) => ?boolean,
onKeyUp?: (e: KeyboardEvent) => ?boolean,
preventClick?: boolean,
preventKeys?: PreventKeysArray,
|};
@@ -256,6 +257,12 @@ const keyboardResponderImpl = {
);
}
} else if (type === 'click' && isVirtualClick(event)) {
if (props.preventClick !== false) {
// 'click' occurs before or after 'keyup', and may need native
// behavior prevented
nativeEvent.preventDefault();
state.defaultPrevented = true;
}
const onClick = props.onClick;
if (onClick != null) {
dispatchKeyboardEvent(
@@ -266,10 +273,6 @@ const keyboardResponderImpl = {
state.defaultPrevented,
);
}
if (state.defaultPrevented && !nativeEvent.defaultPrevented) {
// 'click' occurs before 'keyup' and may need native behavior prevented
nativeEvent.preventDefault();
}
} else if (type === 'keyup') {
state.isActive = false;
const onKeyUp = props.onKeyUp;

View File

@@ -7,858 +7,178 @@
* @flow
*/
import type {
ReactDOMResponderEvent,
ReactDOMResponderContext,
PointerType,
} from 'shared/ReactDOMTypes';
import type {
EventPriority,
ReactEventResponderListener,
} from 'shared/ReactTypes';
import type {PointerType} from 'shared/ReactDOMTypes';
import React from 'react';
import {DiscreteEvent, UserBlockingEvent} from 'shared/ReactTypes';
import {useTap} from 'react-ui/events/tap';
import {useKeyboard} from 'react-ui/events/keyboard';
type PressProps = {|
disabled: boolean,
pressRetentionOffset: {
top: number,
right: number,
bottom: number,
left: number,
},
preventDefault: boolean,
onPress: (e: PressEvent) => void,
onPressChange: boolean => void,
onPressEnd: (e: PressEvent) => void,
onPressMove: (e: PressEvent) => void,
onPressStart: (e: PressEvent) => void,
|};
const emptyObject = {};
type PressState = {
activationPosition: null | $ReadOnly<{|
x: number,
y: number,
|}>,
addedRootEvents: boolean,
buttons: 0 | 1 | 4,
isActivePressed: boolean,
isActivePressStart: boolean,
isPressed: boolean,
isPressWithinResponderRegion: boolean,
pointerType: PointerType,
pressTarget: null | Element | Document,
responderRegionOnActivation: null | $ReadOnly<{|
bottom: number,
left: number,
right: number,
top: number,
|}>,
responderRegionOnDeactivation: null | $ReadOnly<{|
bottom: number,
left: number,
right: number,
top: number,
|}>,
ignoreEmulatedMouseEvents: boolean,
activePointerId: null | number,
shouldPreventClick: boolean,
touchEvent: null | Touch,
};
type PressProps = $ReadOnly<{|
disabled?: boolean,
preventDefault?: boolean,
onPress?: (e: PressEvent) => void,
onPressChange?: boolean => void,
onPressEnd?: (e: PressEvent) => void,
onPressMove?: (e: PressEvent) => void,
onPressStart?: (e: PressEvent) => void,
|}>;
type PressEventType =
| 'press'
| 'pressmove'
| 'pressstart'
| 'presschange'
| 'pressmove'
| 'pressend'
| 'presschange';
| 'press';
type PressEvent = {|
altKey: boolean,
buttons: 0 | 1 | 4,
clientX: null | number,
clientY: null | number,
buttons: null | 0 | 1 | 4,
ctrlKey: boolean,
defaultPrevented: boolean,
key: null | string,
metaKey: boolean,
pageX: null | number,
pageY: null | number,
pageX: number,
pageY: number,
pointerType: PointerType,
screenX: null | number,
screenY: null | number,
shiftKey: boolean,
target: Element | Document,
target: null | Element,
timeStamp: number,
type: PressEventType,
x: null | number,
y: null | number,
x: number,
y: number,
|};
const hasPointerEvents =
typeof window !== 'undefined' && window.PointerEvent !== undefined;
const isMac =
typeof window !== 'undefined' && window.navigator != null
? /^Mac/.test(window.navigator.platform)
: false;
const DEFAULT_PRESS_RETENTION_OFFSET = {
bottom: 20,
top: 20,
left: 20,
right: 20,
};
const targetEventTypes = hasPointerEvents
? ['keydown_active', 'pointerdown', 'click_active']
: ['keydown_active', 'touchstart', 'mousedown', 'click_active'];
const rootEventTypes = hasPointerEvents
? ['pointerup', 'pointermove', 'pointercancel', 'click', 'keyup', 'scroll']
: [
'click',
'keyup',
'scroll',
'mousemove',
'touchmove',
'touchcancel',
// Used as a 'cancel' signal for mouse interactions
'dragstart',
'mouseup',
'touchend',
];
function isFunction(obj): boolean {
return typeof obj === 'function';
}
function createPressEvent(
context: ReactDOMResponderContext,
type: PressEventType,
target: Element | Document,
pointerType: PointerType,
event: ?ReactDOMResponderEvent,
touchEvent: null | Touch,
defaultPrevented: boolean,
state: PressState,
): PressEvent {
const timeStamp = context.getTimeStamp();
let clientX = null;
let clientY = null;
let pageX = null;
let pageY = null;
let screenX = null;
let screenY = null;
let altKey = false;
let ctrlKey = false;
let metaKey = false;
let shiftKey = false;
if (event) {
const nativeEvent = (event.nativeEvent: any);
({altKey, ctrlKey, metaKey, shiftKey} = nativeEvent);
// Only check for one property, checking for all of them is costly. We can assume
// if clientX exists, so do the rest.
let eventObject;
eventObject = (touchEvent: any) || (nativeEvent: any);
if (eventObject) {
({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject);
}
}
function createGestureState(e: any, type: PressEventType): PressEvent {
return {
altKey,
buttons: state.buttons,
clientX,
clientY,
ctrlKey,
defaultPrevented,
metaKey,
pageX,
pageY,
pointerType,
screenX,
screenY,
shiftKey,
target,
timeStamp,
altKey: e.altKey,
buttons: e.buttons,
ctrlKey: e.ctrlKey,
defaultPrevented: e.defaultPrevented,
key: e.key,
metaKey: e.metaKey,
pageX: e.pageX,
pageY: e.pageX,
pointerType: e.pointerType,
shiftKey: e.shiftKey,
target: e.target,
timeStamp: e.timeStamp,
type,
x: clientX,
y: clientY,
x: e.x,
y: e.y,
};
}
function dispatchEvent(
event: ?ReactDOMResponderEvent,
listener: any => void,
context: ReactDOMResponderContext,
state: PressState,
name: PressEventType,
eventPriority: EventPriority,
): void {
const target = ((state.pressTarget: any): Element | Document);
const pointerType = state.pointerType;
const defaultPrevented =
(event != null && event.nativeEvent.defaultPrevented === true) ||
(name === 'press' && state.shouldPreventClick);
const touchEvent = state.touchEvent;
const syntheticEvent = createPressEvent(
context,
name,
target,
pointerType,
event,
touchEvent,
defaultPrevented,
state,
);
context.dispatchEvent(syntheticEvent, listener, eventPriority);
}
function dispatchPressChangeEvent(
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
const onPressChange = props.onPressChange;
if (isFunction(onPressChange)) {
const bool = state.isActivePressed;
context.dispatchEvent(bool, onPressChange, DiscreteEvent);
}
}
function dispatchPressStartEvents(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
state.isPressed = true;
if (!state.isActivePressStart) {
state.isActivePressStart = true;
const nativeEvent: any = event.nativeEvent;
const {clientX: x, clientY: y} = state.touchEvent || nativeEvent;
const wasActivePressed = state.isActivePressed;
state.isActivePressed = true;
if (x !== undefined && y !== undefined) {
state.activationPosition = {x, y};
}
const onPressStart = props.onPressStart;
if (isFunction(onPressStart)) {
dispatchEvent(
event,
onPressStart,
context,
state,
'pressstart',
DiscreteEvent,
);
}
if (!wasActivePressed) {
dispatchPressChangeEvent(context, props, state);
}
}
}
function dispatchPressEndEvents(
event: ?ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
state.isActivePressStart = false;
state.isPressed = false;
if (state.isActivePressed) {
state.isActivePressed = false;
const onPressEnd = props.onPressEnd;
if (isFunction(onPressEnd)) {
dispatchEvent(
event,
onPressEnd,
context,
state,
'pressend',
DiscreteEvent,
);
}
dispatchPressChangeEvent(context, props, state);
}
state.responderRegionOnDeactivation = null;
}
function dispatchCancel(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
state.touchEvent = null;
if (state.isPressed) {
state.ignoreEmulatedMouseEvents = false;
dispatchPressEndEvents(event, context, props, state);
}
removeRootEventTypes(context, state);
}
function isValidKeyboardEvent(nativeEvent: Object): boolean {
const {key, target} = nativeEvent;
const {tagName, isContentEditable} = target;
// Accessibility for keyboards. Space and Enter only.
// "Spacebar" is for IE 11
function isValidKey(e): boolean {
const {key, target} = e;
const {tagName, isContentEditable} = (target: any);
return (
(key === 'Enter' || key === ' ' || key === 'Spacebar') &&
(key === 'Enter' || key === ' ') &&
(tagName !== 'INPUT' &&
tagName !== 'TEXTAREA' &&
isContentEditable !== true)
);
}
// TODO: account for touch hit slop
function calculateResponderRegion(
context: ReactDOMResponderContext,
target: Element,
props: PressProps,
) {
const pressRetentionOffset = context.objectAssign(
{},
DEFAULT_PRESS_RETENTION_OFFSET,
props.pressRetentionOffset,
);
/**
* The lack of built-in composition for gesture responders means we have to
* selectively ignore callbacks from useKeyboard or useTap if the other is
* active.
*/
export function usePress(props: PressProps) {
const safeProps = props || emptyObject;
const {
disabled,
preventDefault,
onPress,
onPressChange,
onPressEnd,
onPressMove,
onPressStart,
} = safeProps;
let {left, right, bottom, top} = target.getBoundingClientRect();
const [active, updateActive] = React.useState(null);
if (pressRetentionOffset) {
if (pressRetentionOffset.bottom != null) {
bottom += pressRetentionOffset.bottom;
}
if (pressRetentionOffset.left != null) {
left -= pressRetentionOffset.left;
}
if (pressRetentionOffset.right != null) {
right += pressRetentionOffset.right;
}
if (pressRetentionOffset.top != null) {
top -= pressRetentionOffset.top;
}
}
return {
bottom,
top,
left,
right,
};
}
function getTouchFromPressEvent(nativeEvent: TouchEvent): null | Touch {
const targetTouches = nativeEvent.targetTouches;
if (targetTouches.length > 0) {
return targetTouches[0];
}
return null;
}
function unmountResponder(
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
if (state.isPressed) {
removeRootEventTypes(context, state);
dispatchPressEndEvents(null, context, props, state);
}
}
function addRootEventTypes(
context: ReactDOMResponderContext,
state: PressState,
): void {
if (!state.addedRootEvents) {
state.addedRootEvents = true;
context.addRootEventTypes(rootEventTypes);
}
}
function removeRootEventTypes(
context: ReactDOMResponderContext,
state: PressState,
): void {
if (state.addedRootEvents) {
state.addedRootEvents = false;
context.removeRootEventTypes(rootEventTypes);
}
}
function getTouchById(
nativeEvent: TouchEvent,
pointerId: null | number,
): null | Touch {
const changedTouches = nativeEvent.changedTouches;
for (let i = 0; i < changedTouches.length; i++) {
const touch = changedTouches[i];
if (touch.identifier === pointerId) {
return touch;
}
}
return null;
}
function getTouchTarget(context: ReactDOMResponderContext, touchEvent: Touch) {
const doc = context.getActiveDocument();
return doc.elementFromPoint(touchEvent.clientX, touchEvent.clientY);
}
function updateIsPressWithinResponderRegion(
nativeEventOrTouchEvent: Event | Touch,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
// Calculate the responder region we use for deactivation if not
// already done during move event.
if (state.responderRegionOnDeactivation == null) {
state.responderRegionOnDeactivation = calculateResponderRegion(
context,
((state.pressTarget: any): Element),
props,
);
}
const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
let left, top, right, bottom;
if (responderRegionOnActivation != null) {
left = responderRegionOnActivation.left;
top = responderRegionOnActivation.top;
right = responderRegionOnActivation.right;
bottom = responderRegionOnActivation.bottom;
if (responderRegionOnDeactivation != null) {
left = Math.min(left, responderRegionOnDeactivation.left);
top = Math.min(top, responderRegionOnDeactivation.top);
right = Math.max(right, responderRegionOnDeactivation.right);
bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
}
}
const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any);
state.isPressWithinResponderRegion =
left != null &&
right != null &&
top != null &&
bottom != null &&
x !== null &&
y !== null &&
(x >= left && x <= right && y >= top && y <= bottom);
}
// After some investigation work, screen reader virtual
// clicks (NVDA, Jaws, VoiceOver) do not have co-ords associated with the click
// event and "detail" is always 0 (where normal clicks are > 0)
function isScreenReaderVirtualClick(nativeEvent): boolean {
return (
nativeEvent.detail === 0 &&
nativeEvent.screenX === 0 &&
nativeEvent.screenY === 0 &&
nativeEvent.clientX === 0 &&
nativeEvent.clientY === 0
);
}
function targetIsDocument(target: null | Node): boolean {
// When target is null, it is the root
return target === null || target.nodeType === 9;
}
const pressResponderImpl = {
targetEventTypes,
getInitialState(): PressState {
return {
activationPosition: null,
addedRootEvents: false,
buttons: 0,
isActivePressed: false,
isActivePressStart: false,
isPressed: false,
isPressWithinResponderRegion: true,
pointerType: '',
pressTarget: null,
responderRegionOnActivation: null,
responderRegionOnDeactivation: null,
ignoreEmulatedMouseEvents: false,
activePointerId: null,
shouldPreventClick: false,
touchEvent: null,
};
},
onEvent(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
const {pointerType, type} = event;
if (props.disabled) {
removeRootEventTypes(context, state);
dispatchPressEndEvents(event, context, props, state);
state.ignoreEmulatedMouseEvents = false;
return;
}
const nativeEvent: any = event.nativeEvent;
const isPressed = state.isPressed;
switch (type) {
// START
case 'pointerdown':
case 'keydown':
case 'mousedown':
case 'touchstart': {
if (!isPressed) {
const isTouchEvent = type === 'touchstart';
const isPointerEvent = type === 'pointerdown';
const isKeyboardEvent = pointerType === 'keyboard';
const isMouseEvent = pointerType === 'mouse';
// Ignore emulated mouse events
if (type === 'mousedown' && state.ignoreEmulatedMouseEvents) {
return;
}
state.shouldPreventClick = false;
if (isTouchEvent) {
state.ignoreEmulatedMouseEvents = true;
} else if (isKeyboardEvent) {
// Ignore unrelated key events
if (isValidKeyboardEvent(nativeEvent)) {
const {
altKey,
ctrlKey,
metaKey,
shiftKey,
} = (nativeEvent: MouseEvent);
if (nativeEvent.key === ' ') {
nativeEvent.preventDefault();
} else if (
props.preventDefault !== false &&
!shiftKey &&
!metaKey &&
!ctrlKey &&
!altKey
) {
state.shouldPreventClick = true;
}
} else {
return;
}
}
// We set these here, before the button check so we have this
// data around for handling of the context menu
state.pointerType = pointerType;
const pressTarget = (state.pressTarget = context.getResponderNode());
if (isPointerEvent) {
state.activePointerId = nativeEvent.pointerId;
} else if (isTouchEvent) {
const touchEvent = getTouchFromPressEvent(nativeEvent);
if (touchEvent === null) {
return;
}
state.touchEvent = touchEvent;
state.activePointerId = touchEvent.identifier;
}
// Ignore any device buttons except primary/middle and touch/pen contact.
// Additionally we ignore primary-button + ctrl-key with Macs as that
// acts like right-click and opens the contextmenu.
if (
nativeEvent.buttons === 2 ||
nativeEvent.buttons > 4 ||
(isMac && isMouseEvent && nativeEvent.ctrlKey)
) {
return;
}
// Exclude document targets
if (!targetIsDocument(pressTarget)) {
state.responderRegionOnActivation = calculateResponderRegion(
context,
((pressTarget: any): Element),
props,
);
}
state.responderRegionOnDeactivation = null;
state.isPressWithinResponderRegion = true;
state.buttons = nativeEvent.buttons;
dispatchPressStartEvents(event, context, props, state);
addRootEventTypes(context, state);
} else {
// Prevent spacebar press from scrolling the window
if (isValidKeyboardEvent(nativeEvent) && nativeEvent.key === ' ') {
nativeEvent.preventDefault();
}
const tap = useTap({
disabled: disabled || active === 'keyboard',
preventDefault,
onTapStart(e) {
if (active == null) {
updateActive('tap');
if (onPressStart != null) {
onPressStart(createGestureState(e, 'pressstart'));
}
break;
}
case 'click': {
if (state.shouldPreventClick) {
nativeEvent.preventDefault();
},
onTapChange: onPressChange,
onTapUpdate(e) {
if (active === 'tap') {
if (onPressMove != null) {
onPressMove(createGestureState(e, 'pressmove'));
}
const onPress = props.onPress;
if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) {
state.pointerType = 'keyboard';
state.pressTarget = context.getResponderNode();
const preventDefault = props.preventDefault;
if (preventDefault !== false) {
nativeEvent.preventDefault();
}
dispatchEvent(event, onPress, context, state, 'press', DiscreteEvent);
}
break;
}
}
},
onRootEvent(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
let {pointerType, target, type} = event;
const nativeEvent: any = event.nativeEvent;
const isPressed = state.isPressed;
const activePointerId = state.activePointerId;
const previousPointerType = state.pointerType;
switch (type) {
// MOVE
case 'pointermove':
case 'mousemove':
case 'touchmove': {
let touchEvent;
// Ignore emulated events (pointermove will dispatch touch and mouse events)
// Ignore pointermove events during a keyboard press.
if (previousPointerType !== pointerType) {
return;
},
onTapEnd(e) {
if (active === 'tap') {
if (onPressEnd != null) {
onPressEnd(createGestureState(e, 'pressend'));
}
if (
type === 'pointermove' &&
activePointerId !== nativeEvent.pointerId
) {
return;
} else if (type === 'touchmove') {
touchEvent = getTouchById(nativeEvent, activePointerId);
if (touchEvent === null) {
return;
}
state.touchEvent = touchEvent;
if (onPress != null && e.buttons !== 4) {
onPress(createGestureState(e, 'press'));
}
const pressTarget = state.pressTarget;
if (pressTarget !== null && !targetIsDocument(pressTarget)) {
if (
pointerType === 'mouse' &&
context.isTargetWithinNode(target, pressTarget)
) {
state.isPressWithinResponderRegion = true;
} else {
// Calculate the responder region we use for deactivation, as the
// element dimensions may have changed since activation.
updateIsPressWithinResponderRegion(
touchEvent || nativeEvent,
context,
props,
state,
);
}
}
if (state.isPressWithinResponderRegion) {
if (isPressed) {
const onPressMove = props.onPressMove;
if (isFunction(onPressMove)) {
dispatchEvent(
event,
onPressMove,
context,
state,
'pressmove',
UserBlockingEvent,
);
}
} else {
dispatchPressStartEvents(event, context, props, state);
}
} else {
dispatchPressEndEvents(event, context, props, state);
}
break;
updateActive(null);
}
// END
case 'pointerup':
case 'keyup':
case 'mouseup':
case 'touchend': {
if (isPressed) {
const buttons = state.buttons;
let isKeyboardEvent = false;
let touchEvent;
if (
type === 'pointerup' &&
activePointerId !== nativeEvent.pointerId
) {
return;
} else if (type === 'touchend') {
touchEvent = getTouchById(nativeEvent, activePointerId);
if (touchEvent === null) {
return;
}
state.touchEvent = touchEvent;
target = getTouchTarget(context, touchEvent);
} else if (type === 'keyup') {
// Ignore unrelated keyboard events
if (!isValidKeyboardEvent(nativeEvent)) {
return;
}
isKeyboardEvent = true;
removeRootEventTypes(context, state);
} else if (buttons === 4) {
// Remove the root events here as no 'click' event is dispatched when this 'button' is pressed.
removeRootEventTypes(context, state);
}
// Determine whether to call preventDefault on subsequent native events.
if (
context.isTargetWithinResponder(target) &&
context.isTargetWithinHostComponent(target, 'a')
) {
const {
altKey,
ctrlKey,
metaKey,
shiftKey,
} = (nativeEvent: MouseEvent);
// Check "open in new window/tab" and "open context menu" key modifiers
const preventDefault = props.preventDefault;
if (
preventDefault !== false &&
!shiftKey &&
!metaKey &&
!ctrlKey &&
!altKey
) {
state.shouldPreventClick = true;
}
}
const pressTarget = state.pressTarget;
dispatchPressEndEvents(event, context, props, state);
const onPress = props.onPress;
if (pressTarget !== null && isFunction(onPress)) {
if (
!isKeyboardEvent &&
pressTarget !== null &&
!targetIsDocument(pressTarget)
) {
if (
pointerType === 'mouse' &&
context.isTargetWithinNode(target, pressTarget)
) {
state.isPressWithinResponderRegion = true;
} else {
// If the event target isn't within the press target, check if we're still
// within the responder region. The region may have changed if the
// element's layout was modified after activation.
updateIsPressWithinResponderRegion(
touchEvent || nativeEvent,
context,
props,
state,
);
}
}
if (state.isPressWithinResponderRegion && buttons !== 4) {
dispatchEvent(
event,
onPress,
context,
state,
'press',
DiscreteEvent,
);
}
}
state.touchEvent = null;
} else if (type === 'mouseup') {
state.ignoreEmulatedMouseEvents = false;
},
onTapCancel(e) {
if (active === 'tap') {
if (onPressEnd != null) {
onPressEnd(createGestureState(e, 'pressend'));
}
break;
updateActive(null);
}
},
});
case 'click': {
// "keyup" occurs after "click"
if (previousPointerType !== 'keyboard') {
removeRootEventTypes(context, state);
const keyboard = useKeyboard({
disabled: disabled || active === 'tap',
preventClick: preventDefault !== false,
preventKeys: preventDefault !== false ? [' ', 'Enter'] : [],
onClick(e) {
if (active == null && onPress != null) {
onPress(createGestureState(e, 'press'));
}
},
onKeyDown(e) {
if (active == null && isValidKey(e)) {
updateActive('keyboard');
if (onPressStart != null) {
onPressStart(createGestureState(e, 'pressstart'));
}
break;
}
// CANCEL
case 'scroll': {
// We ignore incoming scroll events when using mouse events
if (previousPointerType === 'mouse') {
return;
if (onPressChange != null) {
onPressChange(true);
}
const pressTarget = state.pressTarget;
const scrollTarget = nativeEvent.target;
const doc = context.getActiveDocument();
// If the scroll target is the document or if the press target
// is inside the scroll target, then this a scroll that should
// trigger a cancel.
if (
pressTarget !== null &&
(scrollTarget === doc ||
context.isTargetWithinNode(pressTarget, scrollTarget))
) {
dispatchCancel(event, context, props, state);
// stop propagation
return false;
}
},
onKeyUp(e) {
if (active === 'keyboard' && isValidKey(e)) {
if (onPressChange != null) {
onPressChange(false);
}
break;
if (onPressEnd != null) {
onPressEnd(createGestureState(e, 'pressend'));
}
if (onPress != null) {
onPress(createGestureState(e, 'press'));
}
updateActive(null);
// stop propagation
return false;
}
case 'pointercancel':
case 'touchcancel':
case 'dragstart': {
dispatchCancel(event, context, props, state);
}
}
},
onUnmount(
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
) {
unmountResponder(context, props, state);
},
};
},
});
export const PressResponder = React.unstable_createResponder(
'Press',
pressResponderImpl,
);
export function usePress(
props: PressProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(PressResponder, props);
return [tap, keyboard];
}

View File

@@ -0,0 +1,864 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {
ReactDOMResponderEvent,
ReactDOMResponderContext,
PointerType,
} from 'shared/ReactDOMTypes';
import type {
EventPriority,
ReactEventResponderListener,
} from 'shared/ReactTypes';
import React from 'react';
import {DiscreteEvent, UserBlockingEvent} from 'shared/ReactTypes';
type PressProps = {|
disabled: boolean,
pressRetentionOffset: {
top: number,
right: number,
bottom: number,
left: number,
},
preventDefault: boolean,
onPress: (e: PressEvent) => void,
onPressChange: boolean => void,
onPressEnd: (e: PressEvent) => void,
onPressMove: (e: PressEvent) => void,
onPressStart: (e: PressEvent) => void,
|};
type PressState = {
activationPosition: null | $ReadOnly<{|
x: number,
y: number,
|}>,
addedRootEvents: boolean,
buttons: 0 | 1 | 4,
isActivePressed: boolean,
isActivePressStart: boolean,
isPressed: boolean,
isPressWithinResponderRegion: boolean,
pointerType: PointerType,
pressTarget: null | Element | Document,
responderRegionOnActivation: null | $ReadOnly<{|
bottom: number,
left: number,
right: number,
top: number,
|}>,
responderRegionOnDeactivation: null | $ReadOnly<{|
bottom: number,
left: number,
right: number,
top: number,
|}>,
ignoreEmulatedMouseEvents: boolean,
activePointerId: null | number,
shouldPreventClick: boolean,
touchEvent: null | Touch,
};
type PressEventType =
| 'press'
| 'pressmove'
| 'pressstart'
| 'pressend'
| 'presschange';
type PressEvent = {|
altKey: boolean,
buttons: 0 | 1 | 4,
clientX: null | number,
clientY: null | number,
ctrlKey: boolean,
defaultPrevented: boolean,
metaKey: boolean,
pageX: null | number,
pageY: null | number,
pointerType: PointerType,
screenX: null | number,
screenY: null | number,
shiftKey: boolean,
target: Element | Document,
timeStamp: number,
type: PressEventType,
x: null | number,
y: null | number,
|};
const hasPointerEvents =
typeof window !== 'undefined' && window.PointerEvent !== undefined;
const isMac =
typeof window !== 'undefined' && window.navigator != null
? /^Mac/.test(window.navigator.platform)
: false;
const DEFAULT_PRESS_RETENTION_OFFSET = {
bottom: 20,
top: 20,
left: 20,
right: 20,
};
const targetEventTypes = hasPointerEvents
? ['keydown_active', 'pointerdown', 'click_active']
: ['keydown_active', 'touchstart', 'mousedown', 'click_active'];
const rootEventTypes = hasPointerEvents
? ['pointerup', 'pointermove', 'pointercancel', 'click', 'keyup', 'scroll']
: [
'click',
'keyup',
'scroll',
'mousemove',
'touchmove',
'touchcancel',
// Used as a 'cancel' signal for mouse interactions
'dragstart',
'mouseup',
'touchend',
];
function isFunction(obj): boolean {
return typeof obj === 'function';
}
function createPressEvent(
context: ReactDOMResponderContext,
type: PressEventType,
target: Element | Document,
pointerType: PointerType,
event: ?ReactDOMResponderEvent,
touchEvent: null | Touch,
defaultPrevented: boolean,
state: PressState,
): PressEvent {
const timeStamp = context.getTimeStamp();
let clientX = null;
let clientY = null;
let pageX = null;
let pageY = null;
let screenX = null;
let screenY = null;
let altKey = false;
let ctrlKey = false;
let metaKey = false;
let shiftKey = false;
if (event) {
const nativeEvent = (event.nativeEvent: any);
({altKey, ctrlKey, metaKey, shiftKey} = nativeEvent);
// Only check for one property, checking for all of them is costly. We can assume
// if clientX exists, so do the rest.
let eventObject;
eventObject = (touchEvent: any) || (nativeEvent: any);
if (eventObject) {
({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject);
}
}
return {
altKey,
buttons: state.buttons,
clientX,
clientY,
ctrlKey,
defaultPrevented,
metaKey,
pageX,
pageY,
pointerType,
screenX,
screenY,
shiftKey,
target,
timeStamp,
type,
x: clientX,
y: clientY,
};
}
function dispatchEvent(
event: ?ReactDOMResponderEvent,
listener: any => void,
context: ReactDOMResponderContext,
state: PressState,
name: PressEventType,
eventPriority: EventPriority,
): void {
const target = ((state.pressTarget: any): Element | Document);
const pointerType = state.pointerType;
const defaultPrevented =
(event != null && event.nativeEvent.defaultPrevented === true) ||
(name === 'press' && state.shouldPreventClick);
const touchEvent = state.touchEvent;
const syntheticEvent = createPressEvent(
context,
name,
target,
pointerType,
event,
touchEvent,
defaultPrevented,
state,
);
context.dispatchEvent(syntheticEvent, listener, eventPriority);
}
function dispatchPressChangeEvent(
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
const onPressChange = props.onPressChange;
if (isFunction(onPressChange)) {
const bool = state.isActivePressed;
context.dispatchEvent(bool, onPressChange, DiscreteEvent);
}
}
function dispatchPressStartEvents(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
state.isPressed = true;
if (!state.isActivePressStart) {
state.isActivePressStart = true;
const nativeEvent: any = event.nativeEvent;
const {clientX: x, clientY: y} = state.touchEvent || nativeEvent;
const wasActivePressed = state.isActivePressed;
state.isActivePressed = true;
if (x !== undefined && y !== undefined) {
state.activationPosition = {x, y};
}
const onPressStart = props.onPressStart;
if (isFunction(onPressStart)) {
dispatchEvent(
event,
onPressStart,
context,
state,
'pressstart',
DiscreteEvent,
);
}
if (!wasActivePressed) {
dispatchPressChangeEvent(context, props, state);
}
}
}
function dispatchPressEndEvents(
event: ?ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
state.isActivePressStart = false;
state.isPressed = false;
if (state.isActivePressed) {
state.isActivePressed = false;
const onPressEnd = props.onPressEnd;
if (isFunction(onPressEnd)) {
dispatchEvent(
event,
onPressEnd,
context,
state,
'pressend',
DiscreteEvent,
);
}
dispatchPressChangeEvent(context, props, state);
}
state.responderRegionOnDeactivation = null;
}
function dispatchCancel(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
state.touchEvent = null;
if (state.isPressed) {
state.ignoreEmulatedMouseEvents = false;
dispatchPressEndEvents(event, context, props, state);
}
removeRootEventTypes(context, state);
}
function isValidKeyboardEvent(nativeEvent: Object): boolean {
const {key, target} = nativeEvent;
const {tagName, isContentEditable} = target;
// Accessibility for keyboards. Space and Enter only.
// "Spacebar" is for IE 11
return (
(key === 'Enter' || key === ' ' || key === 'Spacebar') &&
(tagName !== 'INPUT' &&
tagName !== 'TEXTAREA' &&
isContentEditable !== true)
);
}
// TODO: account for touch hit slop
function calculateResponderRegion(
context: ReactDOMResponderContext,
target: Element,
props: PressProps,
) {
const pressRetentionOffset = context.objectAssign(
{},
DEFAULT_PRESS_RETENTION_OFFSET,
props.pressRetentionOffset,
);
let {left, right, bottom, top} = target.getBoundingClientRect();
if (pressRetentionOffset) {
if (pressRetentionOffset.bottom != null) {
bottom += pressRetentionOffset.bottom;
}
if (pressRetentionOffset.left != null) {
left -= pressRetentionOffset.left;
}
if (pressRetentionOffset.right != null) {
right += pressRetentionOffset.right;
}
if (pressRetentionOffset.top != null) {
top -= pressRetentionOffset.top;
}
}
return {
bottom,
top,
left,
right,
};
}
function getTouchFromPressEvent(nativeEvent: TouchEvent): null | Touch {
const targetTouches = nativeEvent.targetTouches;
if (targetTouches.length > 0) {
return targetTouches[0];
}
return null;
}
function unmountResponder(
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
if (state.isPressed) {
removeRootEventTypes(context, state);
dispatchPressEndEvents(null, context, props, state);
}
}
function addRootEventTypes(
context: ReactDOMResponderContext,
state: PressState,
): void {
if (!state.addedRootEvents) {
state.addedRootEvents = true;
context.addRootEventTypes(rootEventTypes);
}
}
function removeRootEventTypes(
context: ReactDOMResponderContext,
state: PressState,
): void {
if (state.addedRootEvents) {
state.addedRootEvents = false;
context.removeRootEventTypes(rootEventTypes);
}
}
function getTouchById(
nativeEvent: TouchEvent,
pointerId: null | number,
): null | Touch {
const changedTouches = nativeEvent.changedTouches;
for (let i = 0; i < changedTouches.length; i++) {
const touch = changedTouches[i];
if (touch.identifier === pointerId) {
return touch;
}
}
return null;
}
function getTouchTarget(context: ReactDOMResponderContext, touchEvent: Touch) {
const doc = context.getActiveDocument();
return doc.elementFromPoint(touchEvent.clientX, touchEvent.clientY);
}
function updateIsPressWithinResponderRegion(
nativeEventOrTouchEvent: Event | Touch,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
// Calculate the responder region we use for deactivation if not
// already done during move event.
if (state.responderRegionOnDeactivation == null) {
state.responderRegionOnDeactivation = calculateResponderRegion(
context,
((state.pressTarget: any): Element),
props,
);
}
const {responderRegionOnActivation, responderRegionOnDeactivation} = state;
let left, top, right, bottom;
if (responderRegionOnActivation != null) {
left = responderRegionOnActivation.left;
top = responderRegionOnActivation.top;
right = responderRegionOnActivation.right;
bottom = responderRegionOnActivation.bottom;
if (responderRegionOnDeactivation != null) {
left = Math.min(left, responderRegionOnDeactivation.left);
top = Math.min(top, responderRegionOnDeactivation.top);
right = Math.max(right, responderRegionOnDeactivation.right);
bottom = Math.max(bottom, responderRegionOnDeactivation.bottom);
}
}
const {clientX: x, clientY: y} = (nativeEventOrTouchEvent: any);
state.isPressWithinResponderRegion =
left != null &&
right != null &&
top != null &&
bottom != null &&
x !== null &&
y !== null &&
(x >= left && x <= right && y >= top && y <= bottom);
}
// After some investigation work, screen reader virtual
// clicks (NVDA, Jaws, VoiceOver) do not have co-ords associated with the click
// event and "detail" is always 0 (where normal clicks are > 0)
function isScreenReaderVirtualClick(nativeEvent): boolean {
return (
nativeEvent.detail === 0 &&
nativeEvent.screenX === 0 &&
nativeEvent.screenY === 0 &&
nativeEvent.clientX === 0 &&
nativeEvent.clientY === 0
);
}
function targetIsDocument(target: null | Node): boolean {
// When target is null, it is the root
return target === null || target.nodeType === 9;
}
const pressResponderImpl = {
targetEventTypes,
getInitialState(): PressState {
return {
activationPosition: null,
addedRootEvents: false,
buttons: 0,
isActivePressed: false,
isActivePressStart: false,
isPressed: false,
isPressWithinResponderRegion: true,
pointerType: '',
pressTarget: null,
responderRegionOnActivation: null,
responderRegionOnDeactivation: null,
ignoreEmulatedMouseEvents: false,
activePointerId: null,
shouldPreventClick: false,
touchEvent: null,
};
},
onEvent(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
const {pointerType, type} = event;
if (props.disabled) {
removeRootEventTypes(context, state);
dispatchPressEndEvents(event, context, props, state);
state.ignoreEmulatedMouseEvents = false;
return;
}
const nativeEvent: any = event.nativeEvent;
const isPressed = state.isPressed;
switch (type) {
// START
case 'pointerdown':
case 'keydown':
case 'mousedown':
case 'touchstart': {
if (!isPressed) {
const isTouchEvent = type === 'touchstart';
const isPointerEvent = type === 'pointerdown';
const isKeyboardEvent = pointerType === 'keyboard';
const isMouseEvent = pointerType === 'mouse';
// Ignore emulated mouse events
if (type === 'mousedown' && state.ignoreEmulatedMouseEvents) {
return;
}
state.shouldPreventClick = false;
if (isTouchEvent) {
state.ignoreEmulatedMouseEvents = true;
} else if (isKeyboardEvent) {
// Ignore unrelated key events
if (isValidKeyboardEvent(nativeEvent)) {
const {
altKey,
ctrlKey,
metaKey,
shiftKey,
} = (nativeEvent: MouseEvent);
if (nativeEvent.key === ' ') {
nativeEvent.preventDefault();
} else if (
props.preventDefault !== false &&
!shiftKey &&
!metaKey &&
!ctrlKey &&
!altKey
) {
state.shouldPreventClick = true;
}
} else {
return;
}
}
// We set these here, before the button check so we have this
// data around for handling of the context menu
state.pointerType = pointerType;
const pressTarget = (state.pressTarget = context.getResponderNode());
if (isPointerEvent) {
state.activePointerId = nativeEvent.pointerId;
} else if (isTouchEvent) {
const touchEvent = getTouchFromPressEvent(nativeEvent);
if (touchEvent === null) {
return;
}
state.touchEvent = touchEvent;
state.activePointerId = touchEvent.identifier;
}
// Ignore any device buttons except primary/middle and touch/pen contact.
// Additionally we ignore primary-button + ctrl-key with Macs as that
// acts like right-click and opens the contextmenu.
if (
nativeEvent.buttons === 2 ||
nativeEvent.buttons > 4 ||
(isMac && isMouseEvent && nativeEvent.ctrlKey)
) {
return;
}
// Exclude document targets
if (!targetIsDocument(pressTarget)) {
state.responderRegionOnActivation = calculateResponderRegion(
context,
((pressTarget: any): Element),
props,
);
}
state.responderRegionOnDeactivation = null;
state.isPressWithinResponderRegion = true;
state.buttons = nativeEvent.buttons;
dispatchPressStartEvents(event, context, props, state);
addRootEventTypes(context, state);
} else {
// Prevent spacebar press from scrolling the window
if (isValidKeyboardEvent(nativeEvent) && nativeEvent.key === ' ') {
nativeEvent.preventDefault();
}
}
break;
}
case 'click': {
if (state.shouldPreventClick) {
nativeEvent.preventDefault();
}
const onPress = props.onPress;
if (isFunction(onPress) && isScreenReaderVirtualClick(nativeEvent)) {
state.pointerType = 'keyboard';
state.pressTarget = context.getResponderNode();
const preventDefault = props.preventDefault;
if (preventDefault !== false) {
nativeEvent.preventDefault();
}
dispatchEvent(event, onPress, context, state, 'press', DiscreteEvent);
}
break;
}
}
},
onRootEvent(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
): void {
let {pointerType, target, type} = event;
const nativeEvent: any = event.nativeEvent;
const isPressed = state.isPressed;
const activePointerId = state.activePointerId;
const previousPointerType = state.pointerType;
switch (type) {
// MOVE
case 'pointermove':
case 'mousemove':
case 'touchmove': {
let touchEvent;
// Ignore emulated events (pointermove will dispatch touch and mouse events)
// Ignore pointermove events during a keyboard press.
if (previousPointerType !== pointerType) {
return;
}
if (
type === 'pointermove' &&
activePointerId !== nativeEvent.pointerId
) {
return;
} else if (type === 'touchmove') {
touchEvent = getTouchById(nativeEvent, activePointerId);
if (touchEvent === null) {
return;
}
state.touchEvent = touchEvent;
}
const pressTarget = state.pressTarget;
if (pressTarget !== null && !targetIsDocument(pressTarget)) {
if (
pointerType === 'mouse' &&
context.isTargetWithinNode(target, pressTarget)
) {
state.isPressWithinResponderRegion = true;
} else {
// Calculate the responder region we use for deactivation, as the
// element dimensions may have changed since activation.
updateIsPressWithinResponderRegion(
touchEvent || nativeEvent,
context,
props,
state,
);
}
}
if (state.isPressWithinResponderRegion) {
if (isPressed) {
const onPressMove = props.onPressMove;
if (isFunction(onPressMove)) {
dispatchEvent(
event,
onPressMove,
context,
state,
'pressmove',
UserBlockingEvent,
);
}
} else {
dispatchPressStartEvents(event, context, props, state);
}
} else {
dispatchPressEndEvents(event, context, props, state);
}
break;
}
// END
case 'pointerup':
case 'keyup':
case 'mouseup':
case 'touchend': {
if (isPressed) {
const buttons = state.buttons;
let isKeyboardEvent = false;
let touchEvent;
if (
type === 'pointerup' &&
activePointerId !== nativeEvent.pointerId
) {
return;
} else if (type === 'touchend') {
touchEvent = getTouchById(nativeEvent, activePointerId);
if (touchEvent === null) {
return;
}
state.touchEvent = touchEvent;
target = getTouchTarget(context, touchEvent);
} else if (type === 'keyup') {
// Ignore unrelated keyboard events
if (!isValidKeyboardEvent(nativeEvent)) {
return;
}
isKeyboardEvent = true;
removeRootEventTypes(context, state);
} else if (buttons === 4) {
// Remove the root events here as no 'click' event is dispatched when this 'button' is pressed.
removeRootEventTypes(context, state);
}
// Determine whether to call preventDefault on subsequent native events.
if (
context.isTargetWithinResponder(target) &&
context.isTargetWithinHostComponent(target, 'a')
) {
const {
altKey,
ctrlKey,
metaKey,
shiftKey,
} = (nativeEvent: MouseEvent);
// Check "open in new window/tab" and "open context menu" key modifiers
const preventDefault = props.preventDefault;
if (
preventDefault !== false &&
!shiftKey &&
!metaKey &&
!ctrlKey &&
!altKey
) {
state.shouldPreventClick = true;
}
}
const pressTarget = state.pressTarget;
dispatchPressEndEvents(event, context, props, state);
const onPress = props.onPress;
if (pressTarget !== null && isFunction(onPress)) {
if (
!isKeyboardEvent &&
pressTarget !== null &&
!targetIsDocument(pressTarget)
) {
if (
pointerType === 'mouse' &&
context.isTargetWithinNode(target, pressTarget)
) {
state.isPressWithinResponderRegion = true;
} else {
// If the event target isn't within the press target, check if we're still
// within the responder region. The region may have changed if the
// element's layout was modified after activation.
updateIsPressWithinResponderRegion(
touchEvent || nativeEvent,
context,
props,
state,
);
}
}
if (state.isPressWithinResponderRegion && buttons !== 4) {
dispatchEvent(
event,
onPress,
context,
state,
'press',
DiscreteEvent,
);
}
}
state.touchEvent = null;
} else if (type === 'mouseup') {
state.ignoreEmulatedMouseEvents = false;
}
break;
}
case 'click': {
// "keyup" occurs after "click"
if (previousPointerType !== 'keyboard') {
removeRootEventTypes(context, state);
}
break;
}
// CANCEL
case 'scroll': {
// We ignore incoming scroll events when using mouse events
if (previousPointerType === 'mouse') {
return;
}
const pressTarget = state.pressTarget;
const scrollTarget = nativeEvent.target;
const doc = context.getActiveDocument();
// If the scroll target is the document or if the press target
// is inside the scroll target, then this a scroll that should
// trigger a cancel.
if (
pressTarget !== null &&
(scrollTarget === doc ||
context.isTargetWithinNode(pressTarget, scrollTarget))
) {
dispatchCancel(event, context, props, state);
}
break;
}
case 'pointercancel':
case 'touchcancel':
case 'dragstart': {
dispatchCancel(event, context, props, state);
}
}
},
onUnmount(
context: ReactDOMResponderContext,
props: PressProps,
state: PressState,
) {
unmountResponder(context, props, state);
},
};
export const PressResponder = React.unstable_createResponder(
'Press',
pressResponderImpl,
);
export function usePress(
props: PressProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(PressResponder, props);
}

View File

@@ -36,26 +36,6 @@ type TapProps = $ReadOnly<{|
onTapUpdate?: (e: TapEvent) => void,
|}>;
type TapState = {|
activePointerId: null | number,
buttons: 0 | 1 | 4,
gestureState: TapGestureState,
ignoreEmulatedEvents: boolean,
initialPosition: {|x: number, y: number|},
isActive: boolean,
pointerType: PointerType,
responderTarget: null | Element,
rootEvents: null | Array<string>,
shouldPreventClick: boolean,
|};
type TapEventType =
| 'tap-cancel'
| 'tap-change'
| 'tap-end'
| 'tap-start'
| 'tap-update';
type TapGestureState = {|
altKey: boolean,
buttons: 0 | 1 | 4,
@@ -80,10 +60,31 @@ type TapGestureState = {|
y: number,
|};
type TapEvent = $ReadOnly<{|
type TapState = {|
activePointerId: null | number,
buttons: 0 | 1 | 4,
gestureState: TapGestureState,
ignoreEmulatedEvents: boolean,
initialPosition: {|x: number, y: number|},
isActive: boolean,
pointerType: PointerType,
responderTarget: null | Element,
rootEvents: null | Array<string>,
shouldPreventClick: boolean,
|};
type TapEventType =
| 'tap:cancel'
| 'tap:change'
| 'tap:end'
| 'tap:start'
| 'tap:update';
type TapEvent = {|
...TapGestureState,
defaultPrevented: boolean,
type: TapEventType,
|}>;
|};
/**
* Native event dependencies
@@ -380,6 +381,7 @@ function dispatchStart(
const payload = context.objectAssign({}, state.gestureState, {type});
dispatchDiscreteEvent(context, payload, onTapStart);
}
dispatchChange(context, props, state);
}
function dispatchChange(
@@ -414,8 +416,13 @@ function dispatchEnd(
): void {
const type = 'tap:end';
const onTapEnd = props.onTapEnd;
dispatchChange(context, props, state);
if (onTapEnd != null) {
const payload = context.objectAssign({}, state.gestureState, {type});
const defaultPrevented = state.shouldPreventClick === true;
const payload = context.objectAssign({}, state.gestureState, {
defaultPrevented,
type,
});
dispatchDiscreteEvent(context, payload, onTapEnd);
}
}
@@ -427,6 +434,7 @@ function dispatchCancel(
): void {
const type = 'tap:cancel';
const onTapCancel = props.onTapCancel;
dispatchChange(context, props, state);
if (onTapCancel != null) {
const payload = context.objectAssign({}, state.gestureState, {type});
dispatchDiscreteEvent(context, payload, onTapCancel);
@@ -451,8 +459,8 @@ const responderImpl = {
if (props.disabled) {
removeRootEventTypes(context, state);
if (state.isActive) {
dispatchCancel(context, props, state);
state.isActive = false;
dispatchCancel(context, props, state);
}
return;
}
@@ -498,7 +506,6 @@ const responderImpl = {
state.initialPosition.y = gestureState.y;
dispatchStart(context, props, state);
dispatchChange(context, props, state);
addRootEventTypes(rootEventTypes, context, state);
if (!hasPointerEvents) {
@@ -522,7 +529,7 @@ const responderImpl = {
const hitTarget = getHitTarget(event, context, state);
switch (eventType) {
// MOVE
// UPDATE
case 'pointermove':
case 'mousemove':
case 'touchmove': {
@@ -557,7 +564,6 @@ const responderImpl = {
dispatchUpdate(context, props, state);
} else {
state.isActive = false;
dispatchChange(context, props, state);
dispatchCancel(context, props, state);
}
}
@@ -578,7 +584,6 @@ const responderImpl = {
state.gestureState = createGestureState(context, props, state, event);
state.isActive = false;
dispatchChange(context, props, state);
if (context.isTargetWithinResponder(hitTarget)) {
// Determine whether to call preventDefault on subsequent native events.
if (hasModifierKey(event)) {
@@ -606,7 +611,6 @@ const responderImpl = {
if (state.isActive && isActivePointer(event, state)) {
state.gestureState = createGestureState(context, props, state, event);
state.isActive = false;
dispatchChange(context, props, state);
dispatchCancel(context, props, state);
}
break;
@@ -625,7 +629,6 @@ const responderImpl = {
) {
state.gestureState = createGestureState(context, props, state, event);
state.isActive = false;
dispatchChange(context, props, state);
dispatchCancel(context, props, state);
}
break;
@@ -647,8 +650,8 @@ const responderImpl = {
): void {
removeRootEventTypes(context, state);
if (state.isActive) {
dispatchCancel(context, props, state);
state.isActive = false;
dispatchCancel(context, props, state);
}
},
};

View File

@@ -158,7 +158,7 @@ describe('Keyboard responder', () => {
});
// e.g, "Enter" on link
test('keyboard click is between key events', () => {
test('click is between key events', () => {
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'Enter'});
@@ -168,7 +168,7 @@ describe('Keyboard responder', () => {
expect.objectContaining({
altKey: false,
ctrlKey: false,
defaultPrevented: false,
defaultPrevented: true,
metaKey: false,
pointerType: 'keyboard',
shiftKey: false,
@@ -180,7 +180,7 @@ describe('Keyboard responder', () => {
});
// e.g., "Spacebar" on button
test('keyboard click is after key events', () => {
test('click is after key events', () => {
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'Enter'});
@@ -190,7 +190,27 @@ describe('Keyboard responder', () => {
expect.objectContaining({
altKey: false,
ctrlKey: false,
defaultPrevented: false,
defaultPrevented: true,
metaKey: false,
pointerType: 'keyboard',
shiftKey: false,
target: target.node,
timeStamp: expect.any(Number),
type: 'keyboard:click',
}),
);
});
// e.g, generated by a screen-reader
test('click is orphan', () => {
const target = createEventTarget(ref.current);
target.virtualclick();
expect(onClick).toHaveBeenCalledTimes(1);
expect(onClick).toHaveBeenCalledWith(
expect.objectContaining({
altKey: false,
ctrlKey: false,
defaultPrevented: true,
metaKey: false,
pointerType: 'keyboard',
shiftKey: false,
@@ -326,6 +346,51 @@ describe('Keyboard responder', () => {
});
});
describe('preventClick', () => {
function render(props) {
const ref = React.createRef();
const Component = () => {
const listener = useKeyboard(props);
return <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
return ref;
}
test('prevents native click by default', () => {
const onClick = jest.fn();
const preventDefault = jest.fn();
const ref = render({onClick});
const target = createEventTarget(ref.current);
target.virtualclick({preventDefault});
expect(preventDefault).toBeCalled();
expect(onClick).toHaveBeenCalledTimes(1);
expect(onClick).toHaveBeenCalledWith(
expect.objectContaining({
defaultPrevented: true,
}),
);
});
test('allows native behaviour if false', () => {
const onClick = jest.fn();
const preventDefault = jest.fn();
const ref = render({onClick, preventClick: false});
const target = createEventTarget(ref.current);
target.virtualclick({preventDefault});
expect(preventDefault).not.toBeCalled();
expect(onClick).toHaveBeenCalledTimes(1);
expect(onClick).toHaveBeenCalledWith(
expect.objectContaining({
defaultPrevented: false,
}),
);
});
});
describe('preventKeys', () => {
function render(props) {
const ref = React.createRef();
@@ -347,9 +412,8 @@ describe('Keyboard responder', () => {
target.keydown({key: 'Tab', preventDefault});
target.virtualclick({preventDefault: preventDefaultClick});
expect(onKeyDown).toHaveBeenCalledTimes(1);
expect(preventDefault).toBeCalled();
expect(preventDefaultClick).toBeCalled();
expect(onKeyDown).toHaveBeenCalledTimes(1);
expect(onKeyDown).toHaveBeenCalledWith(
expect.objectContaining({
defaultPrevented: true,
@@ -362,19 +426,12 @@ describe('Keyboard responder', () => {
test('key config matches (modifier keys)', () => {
const onKeyDown = jest.fn();
const preventDefault = jest.fn();
const preventDefaultClick = jest.fn();
const ref = render({onKeyDown, preventKeys: [['Tab', {shiftKey: true}]]});
const target = createEventTarget(ref.current);
target.keydown({key: 'Tab', preventDefault, shiftKey: true});
target.virtualclick({
preventDefault: preventDefaultClick,
shiftKey: true,
});
expect(onKeyDown).toHaveBeenCalledTimes(1);
expect(preventDefault).toBeCalled();
expect(preventDefaultClick).toBeCalled();
expect(onKeyDown).toHaveBeenCalledTimes(1);
expect(onKeyDown).toHaveBeenCalledWith(
expect.objectContaining({
defaultPrevented: true,
@@ -388,19 +445,12 @@ describe('Keyboard responder', () => {
test('key config does not match (modifier keys)', () => {
const onKeyDown = jest.fn();
const preventDefault = jest.fn();
const preventDefaultClick = jest.fn();
const ref = render({onKeyDown, preventKeys: [['Tab', {shiftKey: true}]]});
const target = createEventTarget(ref.current);
target.keydown({key: 'Tab', preventDefault, shiftKey: false});
target.virtualclick({
preventDefault: preventDefaultClick,
shiftKey: false,
});
expect(onKeyDown).toHaveBeenCalledTimes(1);
expect(preventDefault).not.toBeCalled();
expect(preventDefaultClick).not.toBeCalled();
expect(onKeyDown).toHaveBeenCalledTimes(1);
expect(onKeyDown).toHaveBeenCalledWith(
expect.objectContaining({
defaultPrevented: false,

View File

@@ -35,7 +35,7 @@ describe('mixing responders with the heritage event system', () => {
});
it('should properly only flush sync once when the event systems are mixed', () => {
const usePress = require('react-ui/events/press').usePress;
const useTap = require('react-ui/events/tap').useTap;
const ref = React.createRef();
let renderCounts = 0;
@@ -43,12 +43,12 @@ describe('mixing responders with the heritage event system', () => {
const [, updateCounter] = React.useState(0);
renderCounts++;
function handlePress() {
function handleTap() {
updateCounter(count => count + 1);
}
const listener = usePress({
onPress: handlePress,
const listener = useTap({
onTapEnd: handleTap,
});
return (
@@ -104,7 +104,7 @@ describe('mixing responders with the heritage event system', () => {
});
it('should properly flush sync when the event systems are mixed with unstable_flushDiscreteUpdates', () => {
const usePress = require('react-ui/events/press').usePress;
const useTap = require('react-ui/events/tap').useTap;
const ref = React.createRef();
let renderCounts = 0;
@@ -112,12 +112,12 @@ describe('mixing responders with the heritage event system', () => {
const [, updateCounter] = React.useState(0);
renderCounts++;
function handlePress() {
function handleTap() {
updateCounter(count => count + 1);
}
const listener = usePress({
onPress: handlePress,
const listener = useTap({
onTapEnd: handleTap,
});
return (
@@ -177,7 +177,7 @@ describe('mixing responders with the heritage event system', () => {
'event systems',
async () => {
const {useState} = React;
const usePress = require('react-ui/events/press').usePress;
const useTap = require('react-ui/events/tap').useTap;
const button = React.createRef();
@@ -187,7 +187,7 @@ describe('mixing responders with the heritage event system', () => {
const [pressesCount, updatePressesCount] = useState(0);
const [clicksCount, updateClicksCount] = useState(0);
function handlePress() {
function handleTap() {
// This dispatches a synchronous, discrete event in the legacy event
// system. However, because it's nested inside the new event system,
// its updates should not flush until the end of the outer handler.
@@ -198,14 +198,14 @@ describe('mixing responders with the heritage event system', () => {
updatePressesCount(pressesCount + 1);
}
const listener = usePress({
onPress: handlePress,
const tap = useTap({
onTapEnd: handleTap,
});
return (
<div>
<button
listeners={listener}
listeners={tap}
ref={button}
onClick={() => updateClicksCount(clicksCount + 1)}>
Presses: {pressesCount}, Clicks: {clicksCount}
@@ -237,37 +237,35 @@ describe('mixing responders with the heritage event system', () => {
it('is async for non-input events', () => {
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
ReactFeatureFlags.enableUserBlockingEvents = true;
const usePress = require('react-ui/events/press').usePress;
const useTap = require('react-ui/events/tap').useTap;
const useInput = require('react-ui/events/input').useInput;
const root = ReactDOM.unstable_createRoot(container);
let input;
let ops = [];
function Component({innerRef, onChange, controlledValue, pressListener}) {
const inputListener = useInput({
onChange,
});
function Component({innerRef, onChange, controlledValue, listeners}) {
const inputListener = useInput({onChange});
return (
<input
type="text"
ref={innerRef}
value={controlledValue}
listeners={[inputListener, pressListener]}
listeners={[inputListener, listeners]}
/>
);
}
function PressWrapper({innerRef, onPress, onChange, controlledValue}) {
const pressListener = usePress({
onPress,
function PressWrapper({innerRef, onTap, onChange, controlledValue}) {
const tap = useTap({
onTapEnd: onTap,
});
return (
<Component
onChange={onChange}
innerRef={el => (input = el)}
controlledValue={controlledValue}
pressListener={pressListener}
listeners={tap}
/>
);
}
@@ -284,7 +282,7 @@ describe('mixing responders with the heritage event system', () => {
this.state.value === 'changed' ? 'changed [!]' : this.state.value;
return (
<PressWrapper
onPress={this.reset}
onTap={this.reset}
onChange={this.onChange}
innerRef={el => (input = el)}
controlledValue={controlledValue}
@@ -307,10 +305,18 @@ describe('mixing responders with the heritage event system', () => {
// Trigger a click event
input.dispatchEvent(
new MouseEvent('mousedown', {bubbles: true, cancelable: true}),
new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
buttons: 1,
}),
);
input.dispatchEvent(
new MouseEvent('mouseup', {bubbles: true, cancelable: true}),
new MouseEvent('mouseup', {
bubbles: true,
cancelable: true,
buttons: 0,
}),
);
// Nothing should have changed
expect(ops).toEqual([]);

View File

@@ -12,13 +12,13 @@
import {
buttonsType,
createEventTarget,
describeWithPointerEvent,
setPointerEvent,
} from '../testing-library';
let React;
let ReactFeatureFlags;
let ReactDOM;
let PressResponder;
let usePress;
function initializeModules(hasPointerEvents) {
@@ -28,23 +28,12 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
PressResponder = require('react-ui/events/press').PressResponder;
usePress = require('react-ui/events/press').usePress;
}
function removePressMoveStrings(eventString) {
if (eventString === 'onPressMove') {
return false;
}
return true;
}
const forcePointerEvents = true;
const environmentTable = [[forcePointerEvents], [!forcePointerEvents]];
const pointerTypesTable = [['mouse'], ['touch']];
describe.each(environmentTable)('Press responder', hasPointerEvents => {
describeWithPointerEvent('Press responder', hasPointerEvents => {
let container;
beforeEach(() => {
@@ -60,33 +49,49 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
});
describe('disabled', () => {
let onPressStart, onPress, onPressEnd, ref;
let onPressStart, onPressChange, onPressMove, onPressEnd, onPress, ref;
beforeEach(() => {
onPressStart = jest.fn();
onPress = jest.fn();
onPressChange = jest.fn();
onPressMove = jest.fn();
onPressEnd = jest.fn();
onPress = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = usePress({
disabled: true,
onPressStart,
onPress,
onPressChange,
onPressMove,
onPressEnd,
onPress,
});
return <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
document.elementFromPoint = () => ref.current;
});
it('does not call callbacks', () => {
test('does not call callbacks for pointers', () => {
const target = createEventTarget(ref.current);
target.pointerdown();
target.pointerup();
expect(onPressStart).not.toBeCalled();
expect(onPress).not.toBeCalled();
expect(onPressChange).not.toBeCalled();
expect(onPressMove).not.toBeCalled();
expect(onPressEnd).not.toBeCalled();
expect(onPress).not.toBeCalled();
});
test('does not call callbacks for keyboard', () => {
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.keyup({key: 'Enter'});
expect(onPressStart).not.toBeCalled();
expect(onPressChange).not.toBeCalled();
expect(onPressMove).not.toBeCalled();
expect(onPressEnd).not.toBeCalled();
expect(onPress).not.toBeCalled();
});
});
@@ -131,17 +136,6 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
it('is not called after pointer move following middle-button press', () => {
const node = ref.current;
const target = createEventTarget(node);
target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
target.pointerup({pointerType: 'mouse'});
target.pointerhover({x: 110, y: 110});
target.pointerhover({x: 50, y: 50});
expect(onPressStart).toHaveBeenCalledTimes(1);
});
it('ignores any events not caused by primary/middle-click or touch/pen contact', () => {
const target = createEventTarget(ref.current);
target.pointerdown({buttons: buttonsType.secondary});
@@ -231,7 +225,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
// click occurs before keyup
target.click();
target.virtualclick();
target.keyup({key: 'Enter'});
expect(onPressEnd).toHaveBeenCalledTimes(1);
expect(onPressEnd).toHaveBeenCalledWith(
@@ -399,28 +393,6 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
);
});
it('is called if target rect is not right but the target is (for mouse events)', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
const Component = () => {
const listener = usePress({onPress});
return (
<div ref={divRef} listeners={listener}>
<button ref={buttonRef} />
</div>
);
};
ReactDOM.render(<Component />, container);
const target = createEventTarget(divRef.current);
target.setBoundingClientRect({x: 0, y: 0, width: 0, height: 0});
const innerTarget = createEventTarget(buttonRef.current);
innerTarget.pointerdown({pointerType: 'mouse'});
innerTarget.pointerup({pointerType: 'mouse'});
expect(onPress).toBeCalled();
});
it('is called once after virtual screen reader "click" event', () => {
const target = createEventTarget(ref.current);
const preventDefault = jest.fn();
@@ -488,386 +460,6 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
});
});
describe.each(pointerTypesTable)('press with movement: %s', pointerType => {
let events, ref, outerRef;
beforeEach(() => {
events = [];
ref = React.createRef();
outerRef = React.createRef();
const createEventHandler = msg => () => {
events.push(msg);
};
const Component = () => {
const listener = usePress({
onPress: createEventHandler('onPress'),
onPressChange: createEventHandler('onPressChange'),
onPressMove: createEventHandler('onPressMove'),
onPressStart: createEventHandler('onPressStart'),
onPressEnd: createEventHandler('onPressEnd'),
});
return (
<div ref={outerRef}>
<div ref={ref} listeners={listener} />
</div>
);
};
ReactDOM.render(<Component />, container);
document.elementFromPoint = () => ref.current;
});
const rectMock = {width: 100, height: 100, x: 50, y: 50};
const pressRectOffset = 20;
const coordinatesInside = {
x: rectMock.x - pressRectOffset,
y: rectMock.y - pressRectOffset,
};
const coordinatesOutside = {
x: rectMock.x - pressRectOffset - 1,
y: rectMock.y - pressRectOffset - 1,
};
describe('within bounds of hit rect', () => {
/** ┌──────────────────┐
* │ ┌────────────┐ │
* │ │ VisualRect │ │
* │ └────────────┘ │
* │ HitRect X │ <= Move to X and release
* └──────────────────┘
*/
it('"onPress*" events are called immediately', () => {
const target = createEventTarget(ref.current);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
target.pointermove({pointerType, ...coordinatesInside});
target.pointerup({pointerType, ...coordinatesInside});
expect(events).toEqual([
'onPressStart',
'onPressChange',
'onPressMove',
'onPressEnd',
'onPressChange',
'onPress',
]);
});
it('"onPress*" events are correctly called with target change', () => {
const target = createEventTarget(ref.current);
const outerTarget = createEventTarget(outerRef.current);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
target.pointermove({pointerType, ...coordinatesInside});
// TODO: this sequence may differ in the future between PointerEvent and mouse fallback when
// use 'setPointerCapture'.
if (pointerType === 'touch') {
target.pointermove({pointerType, ...coordinatesOutside});
} else {
outerTarget.pointermove({pointerType, ...coordinatesOutside});
}
target.pointermove({pointerType, ...coordinatesInside});
target.pointerup({pointerType, ...coordinatesInside});
expect(events.filter(removePressMoveStrings)).toEqual([
'onPressStart',
'onPressChange',
'onPressEnd',
'onPressChange',
'onPressStart',
'onPressChange',
'onPressEnd',
'onPressChange',
'onPress',
]);
});
it('press retention offset can be configured', () => {
let localEvents = [];
const localRef = React.createRef();
const createEventHandler = msg => () => {
localEvents.push(msg);
};
const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40};
const Component = () => {
const listener = usePress({
onPress: createEventHandler('onPress'),
onPressChange: createEventHandler('onPressChange'),
onPressMove: createEventHandler('onPressMove'),
onPressStart: createEventHandler('onPressStart'),
onPressEnd: createEventHandler('onPressEnd'),
pressRetentionOffset,
});
return <div ref={localRef} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
const target = createEventTarget(localRef.current);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
target.pointermove({
pointerType,
x: rectMock.x,
y: rectMock.y,
});
target.pointerup({pointerType, ...coordinatesInside});
expect(localEvents).toEqual([
'onPressStart',
'onPressChange',
'onPressMove',
'onPressEnd',
'onPressChange',
'onPress',
]);
});
it('responder region accounts for decrease in element dimensions', () => {
const target = createEventTarget(ref.current);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
// emulate smaller dimensions change on activation
target.setBoundingClientRect({width: 80, height: 80, y: 60, x: 60});
const coordinates = {x: rectMock.x, y: rectMock.y};
// move to an area within the pre-activation region
target.pointermove({pointerType, ...coordinates});
target.pointerup({pointerType, ...coordinates});
expect(events).toEqual([
'onPressStart',
'onPressChange',
'onPressMove',
'onPressEnd',
'onPressChange',
'onPress',
]);
});
it('responder region accounts for increase in element dimensions', () => {
const target = createEventTarget(ref.current);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
// emulate larger dimensions change on activation
target.setBoundingClientRect({width: 200, height: 200, y: 0, x: 0});
const coordinates = {x: rectMock.x - 50, y: rectMock.y - 50};
// move to an area within the post-activation region
target.pointermove({pointerType, ...coordinates});
target.pointerup({pointerType, ...coordinates});
expect(events).toEqual([
'onPressStart',
'onPressChange',
'onPressMove',
'onPressEnd',
'onPressChange',
'onPress',
]);
});
});
describe('beyond bounds of hit rect', () => {
/** ┌──────────────────┐
* │ ┌────────────┐ │
* │ │ VisualRect │ │
* │ └────────────┘ │
* │ HitRect │
* └──────────────────┘
* X <= Move to X and release
*/
it('"onPress" is not called on release', () => {
const target = createEventTarget(ref.current);
const targetContainer = createEventTarget(container);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
target.pointermove({pointerType, ...coordinatesInside});
if (pointerType === 'mouse') {
// TODO: use setPointerCapture so this is only true for fallback mouse events.
targetContainer.pointermove({pointerType, ...coordinatesOutside});
targetContainer.pointerup({pointerType, ...coordinatesOutside});
} else {
target.pointermove({pointerType, ...coordinatesOutside});
target.pointerup({pointerType, ...coordinatesOutside});
}
expect(events.filter(removePressMoveStrings)).toEqual([
'onPressStart',
'onPressChange',
'onPressEnd',
'onPressChange',
]);
});
});
it('"onPress" is called on re-entry to hit rect', () => {
const target = createEventTarget(ref.current);
const targetContainer = createEventTarget(container);
target.setBoundingClientRect(rectMock);
target.pointerdown({pointerType});
target.pointermove({pointerType, ...coordinatesInside});
if (pointerType === 'mouse') {
// TODO: use setPointerCapture so this is only true for fallback mouse events.
targetContainer.pointermove({pointerType, ...coordinatesOutside});
} else {
target.pointermove({pointerType, ...coordinatesOutside});
}
target.pointermove({pointerType, ...coordinatesInside});
target.pointerup({pointerType, ...coordinatesInside});
expect(events).toEqual([
'onPressStart',
'onPressChange',
'onPressMove',
'onPressEnd',
'onPressChange',
'onPressStart',
'onPressChange',
'onPressEnd',
'onPressChange',
'onPress',
]);
});
});
describe('nested responders', () => {
if (hasPointerEvents) {
it('dispatch events in the correct order', () => {
const events = [];
const ref = React.createRef();
const createEventHandler = msg => () => {
events.push(msg);
};
const Inner = () => {
const listener = usePress({
onPress: createEventHandler('inner: onPress'),
onPressChange: createEventHandler('inner: onPressChange'),
onPressMove: createEventHandler('inner: onPressMove'),
onPressStart: createEventHandler('inner: onPressStart'),
onPressEnd: createEventHandler('inner: onPressEnd'),
});
return (
<div
ref={ref}
listeners={listener}
onPointerDown={createEventHandler('pointerdown')}
onPointerUp={createEventHandler('pointerup')}
onKeyDown={createEventHandler('keydown')}
onKeyUp={createEventHandler('keyup')}
/>
);
};
const Outer = () => {
const listener = usePress({
onPress: createEventHandler('outer: onPress'),
onPressChange: createEventHandler('outer: onPressChange'),
onPressMove: createEventHandler('outer: onPressMove'),
onPressStart: createEventHandler('outer: onPressStart'),
onPressEnd: createEventHandler('outer: onPressEnd'),
});
return (
<div listeners={listener}>
<Inner />
</div>
);
};
ReactDOM.render(<Outer />, container);
const target = createEventTarget(ref.current);
target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
target.pointerdown();
target.pointerup();
expect(events).toEqual([
'inner: onPressStart',
'inner: onPressChange',
'pointerdown',
'inner: onPressEnd',
'inner: onPressChange',
'inner: onPress',
'pointerup',
]);
});
}
describe('correctly not propagate', () => {
it('for onPress', () => {
const ref = React.createRef();
const onPress = jest.fn();
const Inner = () => {
const listener = usePress({onPress});
return <div ref={ref} listeners={listener} />;
};
const Outer = () => {
const listener = usePress({onPress});
return (
<div listeners={listener}>
<Inner />
</div>
);
};
ReactDOM.render(<Outer />, container);
const target = createEventTarget(ref.current);
target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
target.pointerdown();
target.pointerup();
expect(onPress).toHaveBeenCalledTimes(1);
});
it('for onPressStart/onPressEnd', () => {
const ref = React.createRef();
const onPressStart = jest.fn();
const onPressEnd = jest.fn();
const Inner = () => {
const listener = usePress({onPressStart, onPressEnd});
return <div ref={ref} listeners={listener} />;
};
const Outer = () => {
const listener = usePress({onPressStart, onPressEnd});
return (
<div listeners={listener}>
<Inner />
</div>
);
};
ReactDOM.render(<Outer />, container);
const target = createEventTarget(ref.current);
target.pointerdown();
expect(onPressStart).toHaveBeenCalledTimes(1);
expect(onPressEnd).toHaveBeenCalledTimes(0);
target.pointerup();
expect(onPressStart).toHaveBeenCalledTimes(1);
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
it('for onPressChange', () => {
const ref = React.createRef();
const onPressChange = jest.fn();
const Inner = () => {
const listener = usePress({onPressChange});
return <div ref={ref} listeners={listener} />;
};
const Outer = () => {
const listener = usePress({onPressChange});
return (
<div listeners={listener}>
<Inner />
</div>
);
};
ReactDOM.render(<Outer />, container);
const target = createEventTarget(ref.current);
target.pointerdown();
expect(onPressChange).toHaveBeenCalledTimes(1);
target.pointerup();
expect(onPressChange).toHaveBeenCalledTimes(2);
});
});
});
describe('link components', () => {
it('prevents native behavior by default', () => {
const onPress = jest.fn();
@@ -891,7 +483,8 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
it('prevents native behaviour for keyboard events by default', () => {
const onPress = jest.fn();
const preventDefault = jest.fn();
const preventDefaultClick = jest.fn();
const preventDefaultKeyDown = jest.fn();
const ref = React.createRef();
const Component = () => {
@@ -901,10 +494,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
ReactDOM.render(<Component />, container);
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.click({preventDefault});
target.keydown({key: 'Enter', preventDefault: preventDefaultKeyDown});
target.virtualclick({preventDefault: preventDefaultClick});
target.keyup({key: 'Enter'});
expect(preventDefault).toBeCalled();
expect(preventDefaultKeyDown).toBeCalled();
expect(preventDefaultClick).toBeCalled();
expect(onPress).toHaveBeenCalledTimes(1);
expect(onPress).toHaveBeenCalledWith(
expect.objectContaining({defaultPrevented: true}),
);
@@ -1010,99 +605,16 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const target = createEventTarget(ref.current);
target.keydown({key: 'Enter'});
target.click({preventDefault});
target.virtualclick({preventDefault});
target.keyup({key: 'Enter'});
expect(preventDefault).not.toBeCalled();
expect(onPress).toHaveBeenCalledTimes(1);
expect(onPress).toHaveBeenCalledWith(
expect.objectContaining({defaultPrevented: false}),
);
});
});
describe('responder cancellation', () => {
it.each(pointerTypesTable)('ends on pointer cancel', pointerType => {
const onPressEnd = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = usePress({onPressEnd});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
const target = createEventTarget(ref.current);
target.pointerdown({pointerType});
target.pointercancel({pointerType});
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
});
it('does end on "scroll" to document (not mouse)', () => {
const onPressEnd = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = usePress({onPressEnd});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
const target = createEventTarget(ref.current);
const targetDocument = createEventTarget(document);
target.pointerdown({pointerType: 'touch'});
targetDocument.scroll();
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
it('does end on "scroll" to a parent container (not mouse)', () => {
const onPressEnd = jest.fn();
const ref = React.createRef();
const containerRef = React.createRef();
const Component = () => {
const listener = usePress({onPressEnd});
return (
<div ref={containerRef}>
<a ref={ref} listeners={listener} />
</div>
);
};
ReactDOM.render(<Component />, container);
const target = createEventTarget(ref.current);
const targetContainer = createEventTarget(containerRef.current);
target.pointerdown({pointerType: 'touch'});
targetContainer.scroll();
expect(onPressEnd).toHaveBeenCalledTimes(1);
});
it('does not end on "scroll" to an element outside', () => {
const onPressEnd = jest.fn();
const ref = React.createRef();
const outsideRef = React.createRef();
const Component = () => {
const listener = usePress({onPressEnd});
return (
<div>
<a ref={ref} listeners={listener} />
<span ref={outsideRef} />
</div>
);
};
ReactDOM.render(<Component />, container);
const target = createEventTarget(ref.current);
const targetOutside = createEventTarget(outsideRef.current);
target.pointerdown();
targetOutside.scroll();
expect(onPressEnd).not.toBeCalled();
});
it('expect displayName to show up for event component', () => {
expect(PressResponder.displayName).toBe('Press');
});
it('should not trigger an invariant in addRootEventTypes()', () => {
const ref = React.createRef();

File diff suppressed because it is too large Load Diff

View File

@@ -703,7 +703,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
target.pointerdown();
target.pointerup({preventDefault});
expect(preventDefault).toBeCalled();
expect(onTapEnd).toBeCalled();
expect(onTapEnd).toHaveBeenCalledWith(
expect.objectContaining({defaultPrevented: true}),
);
});
test('prevents native behaviour by default (inner target)', () => {
@@ -711,7 +713,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
innerTarget.pointerdown();
innerTarget.pointerup({preventDefault});
expect(preventDefault).toBeCalled();
expect(onTapEnd).toBeCalled();
expect(onTapEnd).toHaveBeenCalledWith(
expect.objectContaining({defaultPrevented: true}),
);
});
test('allows native behaviour by default (modifier keys)', () => {
@@ -720,7 +724,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
target.pointerdown({[modifierKey]: true});
target.pointerup({[modifierKey]: true, preventDefault});
expect(preventDefault).not.toBeCalled();
expect(onTapEnd).toBeCalled();
expect(onTapEnd).toHaveBeenCalledWith(
expect.objectContaining({defaultPrevented: false}),
);
});
});
@@ -731,7 +737,9 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
target.pointerdown();
target.pointerup({preventDefault});
expect(preventDefault).not.toBeCalled();
expect(onTapEnd).toBeCalled();
expect(onTapEnd).toHaveBeenCalledWith(
expect.objectContaining({defaultPrevented: false}),
);
});
});
});

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-drag.production.min.js');
module.exports = require('./cjs/react-ui-events/drag.production.min.js');
} else {
module.exports = require('./cjs/react-events-drag.development.js');
module.exports = require('./cjs/react-ui-events/drag.development.js');
}

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-focus.production.min.js');
module.exports = require('./cjs/react-ui-events/focus.production.min.js');
} else {
module.exports = require('./cjs/react-events-focus.development.js');
module.exports = require('./cjs/react-ui-events/focus.development.js');
}

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-hover.production.min.js');
module.exports = require('./cjs/react-ui-events/hover.production.min.js');
} else {
module.exports = require('./cjs/react-events-hover.development.js');
module.exports = require('./cjs/react-ui-events/hover.development.js');
}

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-input.production.min.js');
module.exports = require('./cjs/react-ui-events/input.production.min.js');
} else {
module.exports = require('./cjs/react-events-input.development.js');
module.exports = require('./cjs/react-ui-events/input.development.js');
}

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-keyboard.production.min.js');
module.exports = require('./cjs/react-ui-events/keyboard.production.min.js');
} else {
module.exports = require('./cjs/react-events-keyboard.development.js');
module.exports = require('./cjs/react-ui-events/keyboard.development.js');
}

7
packages/react-ui/npm/press-legacy.js vendored Normal file
View File

@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-ui-events/press-legacy.production.min.js');
} else {
module.exports = require('./cjs/react-ui-events/press-legacy.development.js');
}

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-press.production.min.js');
module.exports = require('./cjs/react-ui-events/press.production.min.js');
} else {
module.exports = require('./cjs/react-events-press.development.js');
module.exports = require('./cjs/react-ui-events/press.development.js');
}

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-scroll.production.min.js');
module.exports = require('./cjs/react-ui-events/scroll.production.min.js');
} else {
module.exports = require('./cjs/react-events-scroll.development.js');
module.exports = require('./cjs/react-ui-events/scroll.development.js');
}

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-swipe.production.min.js');
module.exports = require('./cjs/react-ui-events/swipe.production.min.js');
} else {
module.exports = require('./cjs/react-events-swipe.development.js');
module.exports = require('./cjs/react-ui-events/swipe.development.js');
}

View File

@@ -1,7 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-events-tap.production.min.js');
module.exports = require('./cjs/react-ui-events/tap.production.min.js');
} else {
module.exports = require('./cjs/react-events-tap.development.js');
module.exports = require('./cjs/react-ui-events/tap.development.js');
}

View File

@@ -20,6 +20,7 @@
"events/input.js",
"events/keyboard.js",
"events/press.js",
"events/press-legacy.js",
"events/scroll.js",
"events/swipe.js",
"events/tap.js",

View File

@@ -597,6 +597,21 @@ const bundles = [
moduleType: NON_FIBER_RENDERER,
entry: 'react-ui/events/press',
global: 'ReactEventsPress',
externals: ['react', 'react-ui/events/tap', 'react-ui/events/keyboard'],
},
{
bundleTypes: [
UMD_DEV,
UMD_PROD,
NODE_DEV,
NODE_PROD,
FB_WWW_DEV,
FB_WWW_PROD,
],
moduleType: NON_FIBER_RENDERER,
entry: 'react-ui/events/press-legacy',
global: 'ReactEventsPressLegacy',
externals: ['react'],
},

View File

@@ -23,6 +23,8 @@ const importSideEffects = Object.freeze({
const knownGlobals = Object.freeze({
react: 'React',
'react-dom': 'ReactDOM',
'react-ui/events/keyboard': 'ReactEventsKeyboard',
'react-ui/events/tap': 'ReactEventsTap',
scheduler: 'Scheduler',
'scheduler/tracing': 'SchedulerTracing',
'scheduler/unstable_mock': 'SchedulerMock',

View File

@@ -12,7 +12,8 @@ const esNextPaths = [
'packages/*/*.js',
// Source files
'packages/*/src/**/*.js',
'packages/react-ui/*/src/**/*.js',
'packages/react-ui/**/*.js',
'packages/react-ui/**/*.js',
'packages/legacy-events/**/*.js',
'packages/shared/**/*.js',
// Shims and Flow environment