Skip to content
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
14 changes: 8 additions & 6 deletions packages/react-events/docs/Press.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ Called when the element changes press state (i.e., after `onPressStart` and

### onPressEnd: (e: PressEvent) => void

Called once the element is no longer pressed (because it was released, or moved
beyond the hit bounds). If the press starts again before the `delayPressEnd`
threshold is exceeded then the delay is reset to prevent `onPressEnd` being
called during a press.
Called once the element is no longer pressed (because the press was released,
cancelled, or moved beyond the hit bounds). If the press starts again before the
`delayPressEnd` threshold is exceeded then the delay is reset to prevent
`onPressEnd` being called during a press.

### onPressMove: (e: PressEvent) => void

Expand All @@ -120,8 +120,10 @@ Called once the element is pressed down. If the press is released before the
### pressRetentionOffset: PressOffset

Defines how far the pointer (while held down) may move outside the bounds of the
element before it is deactivated. Ensure you pass in a constant to reduce memory
allocations. Default is `20` for each offset.
element before it is deactivated. Once deactivated, the pointer (still held
down) can be moved back within the bounds of the element to reactivate it.
Ensure you pass in a constant to reduce memory allocations. Default is `20` for
each offset.

### preventDefault: boolean = true

Expand Down
61 changes: 35 additions & 26 deletions packages/react-events/src/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,26 @@ const targetEventTypes = [
// We need to preventDefault on pointerdown for mouse/pen events
// that are in hit target area but not the element area.
{name: 'pointerdown', passive: false},
];
const rootEventTypes = [
'keyup',
'pointerup',
'pointermove',
'scroll',
'pointercancel',
];
const rootEventTypes = ['keyup', 'pointerup', 'pointermove', 'scroll'];

// If PointerEvents is not supported (e.g., Safari), also listen to touch and mouse events.
if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
targetEventTypes.push('touchstart', 'touchcancel', 'mousedown');
targetEventTypes.push('touchstart', 'mousedown');
rootEventTypes.push(
{name: 'mouseup', passive: false},
'mousemove',
'touchmove',
'touchend',
'mousemove',
'touchcancel',
// Used as a 'cancel' signal for mouse interactions
'dragstart',
);
}

Expand Down Expand Up @@ -319,6 +327,28 @@ function dispatchPressEndEvents(
}
}

function dispatchCancel(
event: ReactResponderEvent,
context: ReactResponderContext,
props: PressProps,
state: PressState,
): void {
const nativeEvent: any = event.nativeEvent;
const type = event.type;

if (state.isPressed) {
if (type === 'contextmenu' && props.preventDefault !== false) {
nativeEvent.preventDefault();
} else {
state.ignoreEmulatedMouseEvents = false;
removeRootEventTypes(context, state);
dispatchPressEndEvents(event, context, props, state);
}
} else if (state.allowPressReentry) {
removeRootEventTypes(context, state);
}
}

function isAnchorTagElement(eventTarget: EventTarget): boolean {
return (eventTarget: any).nodeName === 'A';
}
Expand Down Expand Up @@ -415,28 +445,6 @@ function unmountResponder(
}
}

function dispatchCancel(
event: ReactResponderEvent,
context: ReactResponderContext,
props: PressProps,
state: PressState,
): void {
const nativeEvent: any = event.nativeEvent;
const type = event.type;

if (state.isPressed) {
if (type === 'contextmenu' && props.preventDefault !== false) {
nativeEvent.preventDefault();
} else {
state.ignoreEmulatedMouseEvents = false;
removeRootEventTypes(context, state);
dispatchPressEndEvents(event, context, props, state);
}
} else if (state.allowPressReentry) {
removeRootEventTypes(context, state);
}
}

function addRootEventTypes(
context: ReactResponderContext,
state: PressState,
Expand Down Expand Up @@ -710,7 +718,8 @@ const PressResponder = {
// CANCEL
case 'pointercancel':
case 'scroll':
case 'touchcancel': {
case 'touchcancel':
case 'dragstart': {
dispatchCancel(event, context, props, state);
}
}
Expand Down
50 changes: 50 additions & 0 deletions packages/react-events/src/__tests__/Press-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,56 @@ describe('Event responder: Press', () => {
});
});

describe('responder cancellation', () => {
it('ends on "pointercancel", "touchcancel", "scroll", and "dragstart"', () => {
const onLongPress = jest.fn();
const onPressEnd = jest.fn();
const ref = React.createRef();
const element = (
<Press onLongPress={onLongPress} onPressEnd={onPressEnd}>
<a href="#" ref={ref} />
</Press>
);
ReactDOM.render(element, container);

ref.current.dispatchEvent(createPointerEvent('pointerdown'));
ref.current.dispatchEvent(createPointerEvent('scroll'));
expect(onPressEnd).toHaveBeenCalledTimes(1);
jest.runAllTimers();
expect(onLongPress).not.toBeCalled();

onLongPress.mockReset();
onPressEnd.mockReset();

// When pointer events are supported
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
ref.current.dispatchEvent(createPointerEvent('pointercancel'));
expect(onPressEnd).toHaveBeenCalledTimes(1);
jest.runAllTimers();
expect(onLongPress).not.toBeCalled();

onLongPress.mockReset();
onPressEnd.mockReset();

// Touch fallback
ref.current.dispatchEvent(createPointerEvent('touchstart'));
ref.current.dispatchEvent(createPointerEvent('touchcancel'));
expect(onPressEnd).toHaveBeenCalledTimes(1);
jest.runAllTimers();
expect(onLongPress).not.toBeCalled();

onLongPress.mockReset();
onPressEnd.mockReset();

// Mouse fallback
ref.current.dispatchEvent(createPointerEvent('mousedown'));
ref.current.dispatchEvent(createPointerEvent('dragstart'));
expect(onPressEnd).toHaveBeenCalledTimes(1);
jest.runAllTimers();
expect(onLongPress).not.toBeCalled();
});
});

it('expect displayName to show up for event component', () => {
expect(Press.displayName).toBe('Press');
});
Expand Down