Skip to content

Commit

Permalink
[react-ui/events] Tap responder API changes (#16827)
Browse files Browse the repository at this point in the history
This patch limits the `onTap*` callbacks to the primary pointer button.
Auxiliary button and modified primary button interactions call
`onAuxiliaryTap`, cancel any active tap, and preserve the native behavior.
  • Loading branch information
necolas committed Sep 19, 2019
1 parent 4ddcb8e commit fd870e6
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 143 deletions.
19 changes: 16 additions & 3 deletions packages/react-ui/events/src/dom/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type PressEventType =

type PressEvent = {|
altKey: boolean,
buttons: null | 0 | 1 | 4,
buttons: null | 1 | 4,
ctrlKey: boolean,
defaultPrevented: boolean,
key: null | string,
Expand All @@ -53,7 +53,7 @@ type PressEvent = {|
function createGestureState(e: any, type: PressEventType): PressEvent {
return {
altKey: e.altKey,
buttons: e.buttons,
buttons: e.type === 'tap:auxiliary' ? 4 : 1,
ctrlKey: e.ctrlKey,
defaultPrevented: e.defaultPrevented,
key: e.key,
Expand Down Expand Up @@ -103,6 +103,19 @@ export function usePress(props: PressProps) {
const tap = useTap({
disabled: disabled || active === 'keyboard',
preventDefault,
onAuxiliaryTap(e) {
if (onPressStart != null) {
onPressStart(createGestureState(e, 'pressstart'));
}
if (onPressEnd != null) {
onPressEnd(createGestureState(e, 'pressend'));
}
// Here we rely on Tap only calling 'onAuxiliaryTap' with modifiers when
// the primary button is pressed
if (onPress != null && (e.metaKey || e.shiftKey)) {
onPress(createGestureState(e, 'press'));
}
},
onTapStart(e) {
if (active == null) {
updateActive('tap');
Expand All @@ -124,7 +137,7 @@ export function usePress(props: PressProps) {
if (onPressEnd != null) {
onPressEnd(createGestureState(e, 'pressend'));
}
if (onPress != null && e.buttons !== 4) {
if (onPress != null) {
onPress(createGestureState(e, 'press'));
}
updateActive(null);
Expand Down
144 changes: 93 additions & 51 deletions packages/react-ui/events/src/dom/Tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ import type {ReactEventResponderListener} from 'shared/ReactTypes';
import React from 'react';
import {
buttonsEnum,
hasPointerEvents,
isMac,
dispatchDiscreteEvent,
dispatchUserBlockingEvent,
getTouchById,
hasModifierKey,
hasPointerEvents,
} from './shared';

type TapProps = $ReadOnly<{|
disabled?: boolean,
maximumDistance?: number,
preventDefault?: boolean,
onAuxiliaryTap?: (e: TapEvent) => void,
onTapCancel?: (e: TapEvent) => void,
onTapChange?: boolean => void,
onTapEnd?: (e: TapEvent) => void,
Expand All @@ -38,7 +38,6 @@ type TapProps = $ReadOnly<{|

type TapGestureState = {|
altKey: boolean,
buttons: 0 | 1 | 4,
ctrlKey: boolean,
height: number,
metaKey: boolean,
Expand Down Expand Up @@ -67,13 +66,15 @@ type TapState = {|
ignoreEmulatedEvents: boolean,
initialPosition: {|x: number, y: number|},
isActive: boolean,
isAuxiliaryActive: boolean,
pointerType: PointerType,
responderTarget: null | Element,
rootEvents: null | Array<string>,
shouldPreventClick: boolean,
shouldPreventDefault: boolean,
|};

type TapEventType =
| 'tap:auxiliary'
| 'tap:cancel'
| 'tap:change'
| 'tap:end'
Expand Down Expand Up @@ -125,14 +126,14 @@ function createInitialState(): TapState {
buttons: 0,
ignoreEmulatedEvents: false,
isActive: false,
isAuxiliaryActive: false,
initialPosition: {x: 0, y: 0},
pointerType: '',
responderTarget: null,
rootEvents: null,
shouldPreventClick: true,
shouldPreventDefault: true,
gestureState: {
altKey: false,
buttons: 0,
ctrlKey: false,
height: 1,
metaKey: false,
Expand Down Expand Up @@ -187,7 +188,6 @@ function createPointerEventGestureState(

return {
altKey,
buttons: state.buttons,
ctrlKey,
height,
metaKey,
Expand Down Expand Up @@ -249,7 +249,6 @@ function createFallbackGestureState(

return {
altKey,
buttons: state.buttons != null ? state.buttons : 1,
ctrlKey,
height: !isCancelType && radiusY != null ? radiusY * 2 : 1,
metaKey,
Expand Down Expand Up @@ -351,19 +350,23 @@ function isActivePointer(
}
}

function isAuxiliary(buttons: number, nativeEvent: any): boolean {
return (
// middle-click
buttons === buttonsEnum.auxiliary ||
// open-in-new-tab
(buttons === buttonsEnum.primary && nativeEvent.metaKey) ||
// open-in-new-window
(buttons === buttonsEnum.primary && nativeEvent.shiftKey)
);
}

function shouldActivate(event: ReactDOMResponderEvent): boolean {
const nativeEvent: any = event.nativeEvent;
const pointerType = event.pointerType;
const buttons = nativeEvent.buttons;
const isContextMenu = pointerType === 'mouse' && nativeEvent.ctrlKey && isMac;
const isValidButton =
buttons === buttonsEnum.primary || buttons === buttonsEnum.middle;

if (pointerType === 'touch' || (isValidButton && !isContextMenu)) {
return true;
} else {
return false;
}
const isValidButton = buttons === buttonsEnum.primary;
return pointerType === 'touch' || (isValidButton && !hasModifierKey(event));
}

/**
Expand Down Expand Up @@ -418,7 +421,7 @@ function dispatchEnd(
const onTapEnd = props.onTapEnd;
dispatchChange(context, props, state);
if (onTapEnd != null) {
const defaultPrevented = state.shouldPreventClick === true;
const defaultPrevented = state.shouldPreventDefault === true;
const payload = context.objectAssign({}, state.gestureState, {
defaultPrevented,
type,
Expand All @@ -441,6 +444,22 @@ function dispatchCancel(
}
}

function dispatchAuxiliaryTap(
context: ReactDOMResponderContext,
props: TapProps,
state: TapState,
): void {
const type = 'tap:auxiliary';
const onAuxiliaryTap = props.onAuxiliaryTap;
if (onAuxiliaryTap != null) {
const payload = context.objectAssign({}, state.gestureState, {
defaultPrevented: false,
type,
});
dispatchDiscreteEvent(context, payload, onAuxiliaryTap);
}
}

/**
* Responder implementation
*/
Expand Down Expand Up @@ -493,26 +512,41 @@ const responderImpl = {
}
}

if (!state.isActive && shouldActivate(event)) {
state.isActive = true;
state.buttons = nativeEvent.buttons;
state.pointerType = event.pointerType;
state.responderTarget = context.getResponderNode();
state.shouldPreventClick = props.preventDefault !== false;

const gestureState = createGestureState(context, props, state, event);
state.gestureState = gestureState;
state.initialPosition.x = gestureState.x;
state.initialPosition.y = gestureState.y;

dispatchStart(context, props, state);
addRootEventTypes(rootEventTypes, context, state);

if (!hasPointerEvents) {
if (eventType === 'touchstart') {
state.ignoreEmulatedEvents = true;
if (!state.isActive) {
const activate = shouldActivate(event);
const activateAuxiliary = isAuxiliary(
nativeEvent.buttons,
nativeEvent,
);

if (activate || activateAuxiliary) {
state.buttons = nativeEvent.buttons;
state.pointerType = event.pointerType;
state.responderTarget = context.getResponderNode();
addRootEventTypes(rootEventTypes, context, state);
if (!hasPointerEvents) {
if (eventType === 'touchstart') {
state.ignoreEmulatedEvents = true;
}
}
}

if (activate) {
const gestureState = createGestureState(
context,
props,
state,
event,
);
state.isActive = true;
state.shouldPreventDefault = props.preventDefault !== false;
state.gestureState = gestureState;
state.initialPosition.x = gestureState.x;
state.initialPosition.y = gestureState.y;
dispatchStart(context, props, state);
} else if (activateAuxiliary) {
state.isAuxiliaryActive = true;
}
}
break;
}
Expand Down Expand Up @@ -575,24 +609,30 @@ const responderImpl = {
case 'mouseup':
case 'touchend': {
if (state.isActive && isActivePointer(event, state)) {
if (state.buttons === buttonsEnum.middle) {
// Remove the root events here as no 'click' event is dispatched
// when this 'button' is pressed.
removeRootEventTypes(context, state);
}

state.gestureState = createGestureState(context, props, state, event);

state.isActive = false;
if (context.isTargetWithinResponder(hitTarget)) {
// Determine whether to call preventDefault on subsequent native events.
if (hasModifierKey(event)) {
state.shouldPreventClick = false;
}
dispatchEnd(context, props, state);
} else {
if (isAuxiliary(state.buttons, nativeEvent)) {
dispatchCancel(context, props, state);
dispatchAuxiliaryTap(context, props, state);
// Remove the root events here as no 'click' event is dispatched
removeRootEventTypes(context, state);
} else if (
!context.isTargetWithinResponder(hitTarget) ||
hasModifierKey(event)
) {
dispatchCancel(context, props, state);
} else {
dispatchEnd(context, props, state);
}
} else if (
state.isAuxiliaryActive &&
isAuxiliary(state.buttons, nativeEvent)
) {
state.isAuxiliaryActive = false;
state.gestureState = createGestureState(context, props, state, event);
dispatchAuxiliaryTap(context, props, state);
// Remove the root events here as no 'click' event is dispatched
removeRootEventTypes(context, state);
}

if (!hasPointerEvents) {
Expand All @@ -612,6 +652,7 @@ const responderImpl = {
state.gestureState = createGestureState(context, props, state, event);
state.isActive = false;
dispatchCancel(context, props, state);
removeRootEventTypes(context, state);
}
break;
}
Expand All @@ -630,12 +671,13 @@ const responderImpl = {
state.gestureState = createGestureState(context, props, state, event);
state.isActive = false;
dispatchCancel(context, props, state);
removeRootEventTypes(context, state);
}
break;
}

case 'click': {
if (state.shouldPreventClick) {
if (state.shouldPreventDefault) {
nativeEvent.preventDefault();
}
removeRootEventTypes(context, state);
Expand Down
18 changes: 13 additions & 5 deletions packages/react-ui/events/src/dom/__tests__/Press-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,13 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {

it('is called after middle-button pointer down', () => {
const target = createEventTarget(ref.current);
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
const pointerType = 'mouse';
target.pointerdown({buttons: buttonsType.auxiliary, pointerType});
target.pointerup({pointerType});
expect(onPressStart).toHaveBeenCalledTimes(1);
expect(onPressStart).toHaveBeenCalledWith(
expect.objectContaining({
buttons: buttonsType.middle,
buttons: buttonsType.auxiliary,
pointerType: 'mouse',
type: 'pressstart',
}),
Expand Down Expand Up @@ -209,12 +211,15 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {

it('is called after middle-button pointer up', () => {
const target = createEventTarget(ref.current);
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
target.pointerdown({
buttons: buttonsType.auxiliary,
pointerType: 'mouse',
});
target.pointerup({pointerType: 'mouse'});
expect(onPressEnd).toHaveBeenCalledTimes(1);
expect(onPressEnd).toHaveBeenCalledWith(
expect.objectContaining({
buttons: buttonsType.middle,
buttons: buttonsType.auxiliary,
pointerType: 'mouse',
type: 'pressend',
}),
Expand Down Expand Up @@ -350,7 +355,10 @@ describeWithPointerEvent('Press responder', hasPointerEvents => {

it('is not called after middle-button press', () => {
const target = createEventTarget(ref.current);
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
target.pointerdown({
buttons: buttonsType.auxiliary,
pointerType: 'mouse',
});
target.pointerup({pointerType: 'mouse'});
expect(onPress).not.toHaveBeenCalled();
});
Expand Down
Loading

0 comments on commit fd870e6

Please sign in to comment.