Skip to content

Commit

Permalink
React events: Press event fixes (#15386)
Browse files Browse the repository at this point in the history
1. Fix hiding context menu for longpress via touch.
2. Fix scrolling of viewport for longpress via spacebar key.
3. Add tests for anchor-related behaviour and preventDefault.
4. Add a deactivation delay for forced activation
5. Add pointerType to Press events.

NOTE: this currently extends pointerType to include `keyboard`.

NOTE: React Native doesn't have a deactivation delay for forced activation, but this is possibly because of the async bridge meaning that the events aren't dispatched sync.
  • Loading branch information
necolas authored Apr 11, 2019
1 parent 9672cf6 commit 45473c9
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 27 deletions.
3 changes: 2 additions & 1 deletion packages/react-events/src/Hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,9 @@ function dispatchHoverChangeEvent(
props: HoverProps,
state: HoverState,
): void {
const bool = state.isActiveHovered;
const listener = () => {
props.onHoverChange(state.isActiveHovered);
props.onHoverChange(bool);
};
const syntheticEvent = createHoverEvent(
'hoverchange',
Expand Down
104 changes: 83 additions & 21 deletions packages/react-events/src/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type PressProps = {
stopPropagation: boolean,
};

type PointerType = '' | 'mouse' | 'keyboard' | 'pen' | 'touch';

type PressState = {
didDispatchEvent: boolean,
isActivePressed: boolean,
Expand All @@ -45,6 +47,7 @@ type PressState = {
isPressed: boolean,
isPressWithinResponderRegion: boolean,
longPressTimeout: null | Symbol,
pointerType: PointerType,
pressTarget: null | Element | Document,
pressEndTimeout: null | Symbol,
pressStartTimeout: null | Symbol,
Expand All @@ -70,6 +73,7 @@ type PressEvent = {|
listener: PressEvent => void,
target: Element | Document,
type: PressEventType,
pointerType: PointerType,
|};

const DEFAULT_PRESS_END_DELAY_MS = 0;
Expand All @@ -85,9 +89,10 @@ const DEFAULT_PRESS_RETENTION_OFFSET = {
const targetEventTypes = [
{name: 'click', passive: false},
{name: 'keydown', passive: false},
{name: 'keypress', passive: false},
{name: 'contextmenu', passive: false},
'pointerdown',
'pointercancel',
'contextmenu',
];
const rootEventTypes = [
{name: 'keyup', passive: false},
Expand All @@ -110,11 +115,13 @@ function createPressEvent(
type: PressEventType,
target: Element | Document,
listener: PressEvent => void,
pointerType: PointerType,
): PressEvent {
return {
listener,
target,
type,
pointerType,
};
}

Expand All @@ -125,7 +132,8 @@ function dispatchEvent(
listener: (e: Object) => void,
): void {
const target = ((state.pressTarget: any): Element | Document);
const syntheticEvent = createPressEvent(name, target, listener);
const pointerType = state.pointerType;
const syntheticEvent = createPressEvent(name, target, listener, pointerType);
context.dispatchEvent(syntheticEvent, {
discrete: true,
});
Expand All @@ -137,8 +145,9 @@ function dispatchPressChangeEvent(
props: PressProps,
state: PressState,
): void {
const bool = state.isActivePressed;
const listener = () => {
props.onPressChange(state.isActivePressed);
props.onPressChange(bool);
};
dispatchEvent(context, state, 'presschange', listener);
}
Expand All @@ -148,8 +157,9 @@ function dispatchLongPressChangeEvent(
props: PressProps,
state: PressState,
): void {
const bool = state.isLongPressed;
const listener = () => {
props.onLongPressChange(state.isLongPressed);
props.onLongPressChange(bool);
};
dispatchEvent(context, state, 'longpresschange', listener);
}
Expand Down Expand Up @@ -251,6 +261,7 @@ function dispatchPressEndEvents(
state: PressState,
): void {
const wasActivePressStart = state.isActivePressStart;
let activationWasForced = false;

state.isActivePressStart = false;
state.isPressed = false;
Expand All @@ -267,13 +278,17 @@ function dispatchPressEndEvents(
if (state.isPressWithinResponderRegion) {
// if we haven't yet activated (due to delays), activate now
activate(context, props, state);
activationWasForced = true;
}
}

if (state.isActivePressed) {
const delayPressEnd = calculateDelayMS(
props.delayPressEnd,
0,
// if activation and deactivation occur during the same event there's no
// time for visual user feedback therefore a small delay is added before
// deactivating.
activationWasForced ? 10 : 0,
DEFAULT_PRESS_END_DELAY_MS,
);
if (delayPressEnd > 0) {
Expand Down Expand Up @@ -338,6 +353,23 @@ function calculateResponderRegion(target, props) {
};
}

function getPointerType(nativeEvent: any) {
const {type, pointerType} = nativeEvent;
if (pointerType != null) {
return pointerType;
}
if (type.indexOf('mouse') > -1) {
return 'mouse';
}
if (type.indexOf('touch') > -1) {
return 'touch';
}
if (type.indexOf('key') > -1) {
return 'keyboard';
}
return '';
}

function isPressWithinResponderRegion(
nativeEvent: $PropertyType<ReactResponderEvent, 'nativeEvent'>,
state: PressState,
Expand Down Expand Up @@ -377,6 +409,7 @@ const PressResponder = {
isPressed: false,
isPressWithinResponderRegion: true,
longPressTimeout: null,
pointerType: '',
pressEndTimeout: null,
pressStartTimeout: null,
pressTarget: null,
Expand All @@ -403,10 +436,10 @@ const PressResponder = {
!context.hasOwnership() &&
!state.shouldSkipMouseAfterTouch
) {
if (
(nativeEvent: any).pointerType === 'mouse' ||
type === 'mousedown'
) {
const pointerType = getPointerType(nativeEvent);
state.pointerType = pointerType;
if (pointerType === 'mouse' || type === 'mousedown') {
if (
// Ignore right- and middle-clicks
nativeEvent.button === 1 ||
Expand Down Expand Up @@ -436,6 +469,9 @@ const PressResponder = {
return;
}

const pointerType = getPointerType(nativeEvent);
state.pointerType = pointerType;

if (state.responderRegion == null) {
let currentTarget = (target: any);
while (
Expand Down Expand Up @@ -470,6 +506,9 @@ const PressResponder = {
return;
}

const pointerType = getPointerType(nativeEvent);
state.pointerType = pointerType;

const wasLongPressed = state.isLongPressed;

dispatchPressEndEvents(context, props, state);
Expand Down Expand Up @@ -506,6 +545,8 @@ const PressResponder = {
state.isAnchorTouched = true;
return;
}
const pointerType = getPointerType(nativeEvent);
state.pointerType = pointerType;
state.pressTarget = target;
state.isPressWithinResponderRegion = true;
dispatchPressStartEvents(context, props, state);
Expand All @@ -519,6 +560,9 @@ const PressResponder = {
return;
}
if (state.isPressed) {
const pointerType = getPointerType(nativeEvent);
state.pointerType = pointerType;

const wasLongPressed = state.isLongPressed;

dispatchPressEndEvents(context, props, state);
Expand Down Expand Up @@ -556,20 +600,24 @@ const PressResponder = {
* Keyboard interaction support
* TODO: determine UX for metaKey + validKeyPress interactions
*/
case 'keydown': {
case 'keydown':
case 'keypress': {
if (
!state.isPressed &&
!state.isLongPressed &&
!context.hasOwnership() &&
isValidKeyPress((nativeEvent: any).key)
) {
// Prevent spacebar press from scrolling the window
if ((nativeEvent: any).key === ' ') {
(nativeEvent: any).preventDefault();
if (state.isPressed) {
// Prevent spacebar press from scrolling the window
if ((nativeEvent: any).key === ' ') {
(nativeEvent: any).preventDefault();
}
} else {
const pointerType = getPointerType(nativeEvent);
state.pointerType = pointerType;
state.pressTarget = target;
dispatchPressStartEvents(context, props, state);
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
}
state.pressTarget = target;
dispatchPressStartEvents(context, props, state);
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
}
break;
}
Expand All @@ -593,7 +641,6 @@ const PressResponder = {
break;
}

case 'contextmenu':
case 'pointercancel':
case 'scroll':
case 'touchcancel': {
Expand All @@ -608,14 +655,29 @@ const PressResponder = {
case 'click': {
if (isAnchorTagElement(target)) {
const {ctrlKey, metaKey, shiftKey} = ((nativeEvent: any): MouseEvent);
// Check "open in new window/tab" and "open context menu" key modifiers
const preventDefault = props.preventDefault;
// Check "open in new window/tab" key modifiers
if (preventDefault !== false && !shiftKey && !ctrlKey && !metaKey) {
if (preventDefault !== false && !shiftKey && !metaKey && !ctrlKey) {
(nativeEvent: any).preventDefault();
}
}
break;
}

case 'contextmenu': {
if (state.isPressed) {
if (props.preventDefault !== false) {
(nativeEvent: any).preventDefault();
} else {
state.shouldSkipMouseAfterTouch = false;
dispatchPressEndEvents(context, props, state);
context.removeRootEventTypes(rootEventTypes);
}
}
break;
}
}

if (state.didDispatchEvent) {
const shouldStopPropagation =
props.stopPropagation === undefined ? true : props.stopPropagation;
Expand Down
Loading

0 comments on commit 45473c9

Please sign in to comment.