mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[react-events] Tap: add maximumDistance prop (#16689)
A prop for configuring the maximum distance that the active pointer can move before the tap is cancelled.
This commit is contained in:
committed by
GitHub
parent
2400400788
commit
41a78cd85c
Vendored
+45
-41
@@ -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 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 = {
|
||||
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'
|
||||
@@ -76,10 +80,10 @@ type TapGestureState = {|
|
||||
y: number,
|
||||
|};
|
||||
|
||||
type TapEvent = {|
|
||||
type TapEvent = $ReadOnly<{|
|
||||
...TapGestureState,
|
||||
type: TapEventType,
|
||||
|};
|
||||
|}>;
|
||||
|
||||
/**
|
||||
* Native event dependencies
|
||||
@@ -120,6 +124,7 @@ function createInitialState(): TapState {
|
||||
buttons: 0,
|
||||
ignoreEmulatedEvents: false,
|
||||
isActive: false,
|
||||
initialPosition: {x: 0, y: 0},
|
||||
pointerType: '',
|
||||
responderTarget: null,
|
||||
rootEvents: null,
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -577,7 +581,7 @@ const responderImpl = {
|
||||
dispatchChange(context, props, state);
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -512,15 +566,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} />
|
||||
@@ -562,6 +617,8 @@ describeWithPointerEvent('Tap responder', hasPointerEvents => {
|
||||
y: 0,
|
||||
}),
|
||||
);
|
||||
target.pointermove({pointerType, x: 5, y: 5});
|
||||
expect(onTapUpdate).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('long press context menu', () => {
|
||||
|
||||
+25
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user