Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[react-ui/events] Tap responder API changes #16827

Merged
merged 1 commit into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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