Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/dev/s2-docs/pages/react-aria/useLongPress.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ friendly alternative to all long press interactions if you are using this hook d

## Features

A long press is triggered when a user presses and holds their pointer over a target for a minimum period of time. If the user moves their pointer off of the target before the time threshold, the interaction is canceled. Once a long press event is triggered, other pointer interactions that may be active such as `usePress` and `useMove` will be canceled so that only the long press is activated.
A long press is triggered when a user presses and holds their pointer over a target for a minimum period of time. If the user moves their pointer off of the target before the time threshold, the interaction is canceled. On touch devices, a second finger touching the screen also cancels the interaction, matching native iOS and Android behavior. Once a long press event is triggered, other pointer interactions that may be active such as `usePress` and `useMove` will be canceled so that only the long press is activated.

* Handles mouse and touch events
* Prevents text selection on touch devices while long pressing
Expand Down
20 changes: 18 additions & 2 deletions packages/react-aria/src/interactions/useLongPress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,28 @@ export function useLongPress(props: LongPressProps): LongPressResult {
timeRef.current = undefined;
}, threshold);

// Prevent context menu, which may be opened on long press on touch devices
let ownerWindow = getOwnerWindow(e.target);

if (e.pointerType === 'touch') {
// Prevent context menu, which may be opened on long press on touch devices
addGlobalListener(e.target, 'contextmenu', e => e.preventDefault(), {once: true});

// A second finger cancels the long press, matching iOS and Android. The capture phase skips
// the pointerdown that started this press, and still sees the next one, which usePress
// stops bubbling.
addGlobalListener(
ownerWindow,
'pointerdown',
event => {
if (event.pointerType === 'touch' && timeRef.current) {
clearTimeout(timeRef.current);
timeRef.current = undefined;
}
},
true
);
}

let ownerWindow = getOwnerWindow(e.target);
addGlobalListener(
ownerWindow,
'pointerup',
Expand Down
244 changes: 244 additions & 0 deletions packages/react-aria/test/interactions/useLongPress.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,250 @@ describe('useLongPress', function () {
]);
});

it('should cancel the long press when a second touch begins', function () {
let events = [];
let addEvent = e => events.push(e);
let res = render(
<Example onLongPressStart={addEvent} onLongPressEnd={addEvent} onLongPress={addEvent} />
);

let el = res.getByText('test');

fireEvent.pointerDown(el, {pointerType: 'touch', pointerId: 1});
act(() => jest.advanceTimersByTime(100));
fireEvent.pointerDown(el, {pointerType: 'touch', pointerId: 2});
act(() => jest.advanceTimersByTime(600));
expect(events).toEqual([
{
type: 'longpressstart',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't longpress end have already happened by here? it's canceled on pointerDown

]);

fireEvent.pointerUp(el, {pointerType: 'touch', pointerId: 2});
fireEvent.pointerUp(el, {pointerType: 'touch', pointerId: 1});
act(() => jest.advanceTimersByTime(800));
expect(events).toEqual([
{
type: 'longpressstart',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpressend',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
}
]);
});

it('should not cancel the long press when a mouse pointer goes down elsewhere', function () {
let events = [];
let addEvent = e => events.push(e);
let res = render(
<Example onLongPressStart={addEvent} onLongPressEnd={addEvent} onLongPress={addEvent} />
);

let el = res.getByText('test');

fireEvent.pointerDown(el, {pointerType: 'touch', pointerId: 1});
act(() => jest.advanceTimersByTime(100));
fireEvent.pointerDown(document.body, {pointerType: 'mouse', pointerId: 2});
act(() => jest.advanceTimersByTime(600));
expect(events).toEqual([
{
type: 'longpressstart',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpressend',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpress',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
}
]);
});

it('should not cancel a mouse long press when a second pointer goes down', function () {
let events = [];
let addEvent = e => events.push(e);
let res = render(
<Example
pointerType="mouse"
onLongPressStart={addEvent}
onLongPressEnd={addEvent}
onLongPress={addEvent}
/>
);

let el = res.getByText('test');

fireEvent.pointerDown(el, {pointerType: 'mouse', pointerId: 1});
act(() => jest.advanceTimersByTime(100));
fireEvent.pointerDown(document.body, {pointerType: 'touch', pointerId: 2});
act(() => jest.advanceTimersByTime(600));
expect(events).toEqual([
{
type: 'longpressstart',
target: el,
pointerType: 'mouse',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpressend',
target: el,
pointerType: 'mouse',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpress',
target: el,
pointerType: 'mouse',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
}
]);
});

it('should perform a long press after a previous one has completed', function () {
let events = [];
let addEvent = e => events.push(e);
let res = render(
<Example onLongPressStart={addEvent} onLongPressEnd={addEvent} onLongPress={addEvent} />
);

let el = res.getByText('test');

fireEvent.pointerDown(el, {pointerType: 'touch', pointerId: 1});
act(() => jest.advanceTimersByTime(600));
fireEvent.pointerDown(el, {pointerType: 'touch', pointerId: 2});
act(() => jest.advanceTimersByTime(600));
expect(events).toEqual([
{
type: 'longpressstart',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpressend',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpress',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpressstart',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpressend',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
},
{
type: 'longpress',
target: el,
pointerType: 'touch',
ctrlKey: false,
metaKey: false,
shiftKey: false,
altKey: false,
x: 0,
y: 0
}
]);
});

it('should cancel other press events', function () {
let events = [];
let addEvent = e => events.push(e);
Expand Down