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-events] Tap: add maximumDistance prop #16689

Merged
merged 1 commit into from
Sep 10, 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
88 changes: 46 additions & 42 deletions packages/react-events/src/dom/Tap.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,33 @@ import {
isMac,
dispatchDiscreteEvent,
dispatchUserBlockingEvent,
getTouchById,
hasModifierKey,
} from './shared';

type TapProps = {|
disabled: boolean,
preventDefault: boolean,
onTapCancel: (e: TapEvent) => void,
onTapChange: boolean => void,
onTapEnd: (e: TapEvent) => void,
onTapStart: (e: TapEvent) => void,
onTapUpdate: (e: TapEvent) => void,
|};

type TapState = {
type TapProps = $ReadOnly<{|
disabled?: boolean,
maximumDistance?: number,
preventDefault?: boolean,
onTapCancel?: (e: TapEvent) => void,
onTapChange?: boolean => void,
onTapEnd?: (e: TapEvent) => void,
onTapStart?: (e: TapEvent) => void,
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'
Expand Down Expand Up @@ -76,10 +80,10 @@ type TapGestureState = {|
y: number,
|};

type TapEvent = {|
type TapEvent = $ReadOnly<{|
...TapGestureState,
type: TapEventType,
|};
|}>;

/**
* Native event dependencies
Expand Down Expand Up @@ -120,6 +124,7 @@ function createInitialState(): TapState {
buttons: 0,
ignoreEmulatedEvents: false,
isActive: false,
initialPosition: {x: 0, y: 0},
pointerType: '',
responderTarget: null,
rootEvents: null,
Expand Down Expand Up @@ -299,23 +304,6 @@ function removeRootEventTypes(
* Managing pointers
*/

function getTouchById(
nativeEvent: TouchEvent,
pointerId: null | number,
): null | Touch {
if (pointerId != null) {
const changedTouches = nativeEvent.changedTouches;
for (let i = 0; i < changedTouches.length; i++) {
const touch = changedTouches[i];
if (touch.identifier === pointerId) {
return touch;
}
}
return null;
}
return null;
}

function getHitTarget(
event: ReactDOMResponderEvent,
context: ReactDOMResponderContext,
Expand Down Expand Up @@ -362,14 +350,6 @@ function isActivePointer(
}
}

function isModifiedTap(event: ReactDOMResponderEvent): boolean {
const nativeEvent: any = event.nativeEvent;
const {altKey, ctrlKey, metaKey, shiftKey} = nativeEvent;
return (
altKey === true || ctrlKey === true || metaKey === true || shiftKey === true
);
}

function shouldActivate(event: ReactDOMResponderEvent): boolean {
const nativeEvent: any = event.nativeEvent;
const pointerType = event.pointerType;
Expand Down Expand Up @@ -511,7 +491,12 @@ const responderImpl = {
state.pointerType = event.pointerType;
state.responderTarget = context.getResponderNode();
state.shouldPreventClick = props.preventDefault !== false;
state.gestureState = createGestureState(context, props, state, event);

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

dispatchStart(context, props, state);
dispatchChange(context, props, state);
addRootEventTypes(rootEventTypes, context, state);
Expand Down Expand Up @@ -549,7 +534,26 @@ const responderImpl = {

if (state.isActive && isActivePointer(event, state)) {
state.gestureState = createGestureState(context, props, state, event);
if (context.isTargetWithinResponder(hitTarget)) {
let shouldUpdate = true;

if (!context.isTargetWithinResponder(hitTarget)) {
shouldUpdate = false;
} else if (
props.maximumDistance != null &&
props.maximumDistance >= 10
) {
const maxDistance = props.maximumDistance;
const initialPosition = state.initialPosition;
const currentPosition = state.gestureState;
const moveX = initialPosition.x - currentPosition.x;
const moveY = initialPosition.y - currentPosition.y;
const moveDistance = Math.sqrt(moveX * moveX + moveY * moveY);
if (moveDistance > maxDistance) {
shouldUpdate = false;
}
}

if (shouldUpdate) {
dispatchUpdate(context, props, state);
} else {
state.isActive = false;
Expand All @@ -575,7 +579,7 @@ const responderImpl = {

if (context.isTargetWithinResponder(hitTarget)) {
// Determine whether to call preventDefault on subsequent native events.
if (isModifiedTap(event)) {
if (hasModifierKey(event)) {
state.shouldPreventClick = false;
}
dispatchEnd(context, props, state);
Expand Down
61 changes: 59 additions & 2 deletions packages/react-events/src/dom/__tests__/Tap-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,60 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
});
});

describe('maximumDistance', () => {
let onTapCancel, onTapUpdate, ref;

function render(props) {
const Component = () => {
const listener = useTap(props);
return <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
document.elementFromPoint = () => ref.current;
}

beforeEach(() => {
onTapCancel = jest.fn();
onTapUpdate = jest.fn();
ref = React.createRef();
render({
maximumDistance: 20,
onTapCancel,
onTapUpdate,
});
});

test('ignores values less than 10', () => {
render({
maximumDistance: 5,
onTapCancel,
onTapUpdate,
});
const target = createEventTarget(ref.current);
const pointerType = 'mouse';
target.pointerdown({pointerType, x: 0, y: 0});
target.pointermove({pointerType, x: 10, y: 10});
expect(onTapUpdate).toHaveBeenCalledTimes(1);
expect(onTapCancel).toHaveBeenCalledTimes(0);
});

testWithPointerType('below threshold', pointerType => {
const target = createEventTarget(ref.current);
target.pointerdown({pointerType, x: 0, y: 0});
target.pointermove({pointerType, x: 10, y: 10});
expect(onTapUpdate).toHaveBeenCalledTimes(1);
expect(onTapCancel).toHaveBeenCalledTimes(0);
});

testWithPointerType('above threshold', pointerType => {
const target = createEventTarget(ref.current);
target.pointerdown({pointerType, x: 0, y: 0});
target.pointermove({pointerType, x: 15, y: 14});
expect(onTapUpdate).toHaveBeenCalledTimes(0);
expect(onTapCancel).toHaveBeenCalledTimes(1);
});
});

describe('onTapStart', () => {
let onTapStart, ref;

Expand Down Expand Up @@ -496,15 +550,16 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
});

describe('onTapCancel', () => {
let onTapCancel, parentRef, ref, siblingRef;
let onTapCancel, onTapUpdate, parentRef, ref, siblingRef;

beforeEach(() => {
onTapCancel = jest.fn();
onTapUpdate = jest.fn();
parentRef = React.createRef();
ref = React.createRef();
siblingRef = React.createRef();
const Component = () => {
const listener = useTap({onTapCancel});
const listener = useTap({onTapCancel, onTapUpdate});
return (
<div ref={parentRef}>
<div ref={ref} listeners={listener} />
Expand Down Expand Up @@ -546,6 +601,8 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
y: 0,
}),
);
target.pointermove({pointerType, x: 5, y: 5});
expect(onTapUpdate).not.toBeCalled();
});

test('long press context menu', () => {
Expand Down
25 changes: 25 additions & 0 deletions packages/react-events/src/dom/shared/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,28 @@ export function dispatchUserBlockingEvent(
) {
context.dispatchEvent(payload, callback, UserBlockingEvent);
}

export function getTouchById(
nativeEvent: TouchEvent,
pointerId: null | number,
): null | Touch {
if (pointerId != null) {
const changedTouches = nativeEvent.changedTouches;
for (let i = 0; i < changedTouches.length; i++) {
const touch = changedTouches[i];
if (touch.identifier === pointerId) {
return touch;
}
}
return null;
}
return null;
}

export function hasModifierKey(event: ReactDOMResponderEvent): boolean {
const nativeEvent: any = event.nativeEvent;
const {altKey, ctrlKey, metaKey, shiftKey} = nativeEvent;
return (
altKey === true || ctrlKey === true || metaKey === true || shiftKey === true
);
}