From 45681b88e3fd3d9337a38da07248c46ec6d5956e Mon Sep 17 00:00:00 2001
From: Kuss <2360314753@qq.com>
Date: Thu, 23 Jan 2020 13:03:45 +0800
Subject: [PATCH] feat: add useLongPress hook
---
docs/useLongPress.md | 38 ++++++++++
src/index.ts | 1 +
src/useLongPress.ts | 48 +++++++++++++
stories/useLongPress.story.tsx | 19 +++++
tests/useLongPress.test.tsx | 125 +++++++++++++++++++++++++++++++++
5 files changed, 231 insertions(+)
create mode 100644 docs/useLongPress.md
create mode 100644 src/useLongPress.ts
create mode 100644 stories/useLongPress.story.tsx
create mode 100644 tests/useLongPress.test.tsx
diff --git a/docs/useLongPress.md b/docs/useLongPress.md
new file mode 100644
index 0000000000..d70429cd11
--- /dev/null
+++ b/docs/useLongPress.md
@@ -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 ;
+};
+```
+
+## Reference
+
+```ts
+const {
+ onMouseDown,
+ onTouchStart,
+ onMouseUp,
+ onMouseLeave,
+ onTouchEnd
+} = useLongPress(
+ callback: (e: TouchEvent | MouseEvent) => void,
+ delay?: number = 300
+)
+```
+
+- `callback` — callback function.
+- `delay` — delay in milliseconds after which to calls provided callback, defaults to `300`.
diff --git a/src/index.ts b/src/index.ts
index d4730ca747..65b516ce00 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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';
diff --git a/src/useLongPress.ts b/src/useLongPress.ts
new file mode 100644
index 0000000000..6d4b4a6a0c
--- /dev/null
+++ b/src/useLongPress.ts
@@ -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>();
+ const target = useRef();
+
+ 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;
diff --git a/stories/useLongPress.story.tsx b/stories/useLongPress.story.tsx
new file mode 100644
index 0000000000..06d9eecbf4
--- /dev/null
+++ b/stories/useLongPress.story.tsx
@@ -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 ;
+};
+
+storiesOf('Sensors|useLongPress', module)
+ .add('Docs', () => )
+ .add('Demo', () => );
diff --git a/tests/useLongPress.test.tsx b/tests/useLongPress.test.tsx
new file mode 100644
index 0000000000..6feeadc01e
--- /dev/null
+++ b/tests/useLongPress.test.tsx
@@ -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);
+});