Skip to content

Commit

Permalink
feat: add useLongPress hook
Browse files Browse the repository at this point in the history
  • Loading branch information
KusStar committed Jan 23, 2020
1 parent 772e6cb commit 45681b8
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 0 deletions.
38 changes: 38 additions & 0 deletions docs/useLongPress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `useLongPress`

React sensor hook that fires a callback after long pressing.

## Usage

```jsx
import { useLongPress } from 'react-use';

const Demo = () => {
const onLongPress = () => {
console.log('calls callback after long pressing 300ms');
};

const defaultDelay = 300;
const longPressEvent = useLongPress(onLongPress, defaultDelay);

return <button {...longPressEvent}>useLongPress</button>;
};
```

## Reference

```ts
const {
onMouseDown,
onTouchStart,
onMouseUp,
onMouseLeave,
onTouchEnd
} = useLongPress(
callback: (e: TouchEvent | MouseEvent) => void,
delay?: number = 300
)
```

- `callback` &mdash; callback function.
- `delay` &mdash; delay in milliseconds after which to calls provided callback, defaults to `300`.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export { default as useLocalStorage } from './useLocalStorage';
export { default as useLocation } from './useLocation';
export { default as useLockBodyScroll } from './useLockBodyScroll';
export { default as useLogger } from './useLogger';
export { default as useLongPress } from './useLongPress';
export { default as useMap } from './useMap';
export { default as useMedia } from './useMedia';
export { default as useMediaDevices } from './useMediaDevices';
Expand Down
48 changes: 48 additions & 0 deletions src/useLongPress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useCallback, useRef } from 'react';

const isTouchEvent = (event: Event): event is TouchEvent => {
return 'touches' in event;
};

const preventDefault = (event: Event) => {
if (!isTouchEvent(event)) return;

if (event.touches.length < 2 && event.preventDefault) {
event.preventDefault();
}
};

const useLongPress = (callback: (e: TouchEvent | MouseEvent) => void, delay: number = 300) => {
const timeout = useRef<ReturnType<typeof setTimeout>>();
const target = useRef<EventTarget>();

const start = useCallback(
(event: TouchEvent | MouseEvent) => {
// prevent ghost click on mobile devices
if (event.target) {
target.current = event.target;
event.target.addEventListener('touchend', preventDefault, { passive: false });
}

timeout.current = setTimeout(() => callback(event), delay);
},
[callback, delay]
);

const clear = useCallback(() => {
// clearTimeout and removeEventListener
timeout.current && clearTimeout(timeout.current);

target.current && target.current.removeEventListener('touchend', preventDefault);
}, []);

return {
onMouseDown: (e: any) => start(e),
onTouchStart: (e: any) => start(e),
onMouseUp: clear,
onMouseLeave: clear,
onTouchEnd: clear,
} as const;
};

export default useLongPress;
19 changes: 19 additions & 0 deletions stories/useLongPress.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useLongPress } from '../src';
import ShowDocs from './util/ShowDocs';

const Demo = () => {
const onLongPress = () => {
console.log('calls callback after long pressing 300ms');
};

const defaultDelay = 300;
const longPressEvent = useLongPress(onLongPress, defaultDelay);

return <button {...longPressEvent}>useLongPress</button>;
};

storiesOf('Sensors|useLongPress', module)
.add('Docs', () => <ShowDocs md={require('../docs/useLongPress.md')} />)
.add('Demo', () => <Demo />);
125 changes: 125 additions & 0 deletions tests/useLongPress.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { renderHook } from '@testing-library/react-hooks';
import useLongPress from '../src/useLongPress';

const callback = jest.fn();
const defaultDelay = 300;
const mouseDown = new MouseEvent('mousedown');
const touchStart = new TouchEvent('touchstart');

beforeAll(() => {
jest.useFakeTimers();
});

afterEach(() => {
callback.mockRestore();
jest.clearAllTimers();
});

afterAll(() => {
jest.useRealTimers();
});

it('should not call provided callback without trigger any event', () => {
renderHook(() => useLongPress(callback));

expect(callback).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(defaultDelay);

expect(callback).toHaveBeenCalledTimes(0);
});

it('should call provided callback onMouseDown', () => {
const { result } = renderHook(() => useLongPress(callback));
const { onMouseDown } = result.current;

expect(callback).toHaveBeenCalledTimes(0);
onMouseDown(mouseDown);

jest.advanceTimersByTime(defaultDelay - 20);
expect(callback).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(20);
expect(callback).toHaveBeenCalledTimes(1);
});

it('should call provided callback with custom delay', () => {
const customDelay = 1000;
const { result } = renderHook(() => useLongPress(callback, customDelay));
const { onMouseDown } = result.current;

expect(callback).toHaveBeenCalledTimes(0);
onMouseDown(mouseDown);

jest.advanceTimersByTime(customDelay - 20);
expect(callback).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(20);
expect(callback).toHaveBeenCalledTimes(1);
});

it('should not call provided callback if interrupted by onMouseLeave', () => {
const { result } = renderHook(() => useLongPress(callback));
const { onMouseDown, onMouseLeave } = result.current;

expect(callback).toHaveBeenCalledTimes(0);
onMouseDown(mouseDown);

jest.advanceTimersByTime(defaultDelay - 20);
expect(callback).toHaveBeenCalledTimes(0);

onMouseLeave();

jest.advanceTimersByTime(20);
expect(callback).toHaveBeenCalledTimes(0);
expect(setTimeout).toHaveBeenCalledTimes(1);
});

it('should not call provided callback if interrupted by onMouseUp', () => {
const { result } = renderHook(() => useLongPress(callback));
const { onMouseDown, onMouseUp } = result.current;

expect(callback).toHaveBeenCalledTimes(0);
onMouseDown(mouseDown);

jest.advanceTimersByTime(defaultDelay - 20);
expect(callback).toHaveBeenCalledTimes(0);

onMouseUp();

jest.advanceTimersByTime(20);
expect(callback).toHaveBeenCalledTimes(0);
expect(setTimeout).toHaveBeenCalledTimes(1);
});

it('should call provided callback onTouchStart', () => {
const customDelay = 1000;
const { result } = renderHook(() => useLongPress(callback, customDelay));
const { onMouseDown } = result.current;

expect(callback).toHaveBeenCalledTimes(0);
onMouseDown(mouseDown);

jest.advanceTimersByTime(customDelay - 20);
expect(callback).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(20);
expect(callback).toHaveBeenCalledTimes(1);
});

it('should not call provided callback if interrupted by onTouchEnd', () => {
const { result } = renderHook(() => useLongPress(callback));
const { onTouchStart, onTouchEnd } = result.current;

expect(callback).toHaveBeenCalledTimes(0);
onTouchStart(touchStart);

jest.advanceTimersByTime(defaultDelay - 20);
expect(callback).toHaveBeenCalledTimes(0);

onTouchEnd();

jest.advanceTimersByTime(20);
expect(callback).toHaveBeenCalledTimes(0);
expect(setTimeout).toHaveBeenCalledTimes(1);
});

0 comments on commit 45681b8

Please sign in to comment.