Skip to content

Commit 9d1d00a

Browse files
erikkkwuNelsonYong
andauthored
feat: add use-long-press (#150)
* feat: add use-long-press * fix: cannot get correct dom target when target is vue component instance * docs: add useLongPress docs router path * style: optimized code line In alphabetical order * style: optimized code line In alphabetical order * docs: update long-press zh-cn docs * feat: supported point event on use-long-press --------- Co-authored-by: YongGit <1013588891@qq.com>
1 parent c436195 commit 9d1d00a

File tree

8 files changed

+374
-4
lines changed

8 files changed

+374
-4
lines changed

packages/hooks/docs/.vitepress/router.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const Router = [
6767
{ text: 'useScroll', link: '/useScroll/' },
6868
{ text: 'useTitle', link: '/useTitle/' },
6969
{ text: 'useWinResize', link: '/useWinResize/' },
70+
{ text: 'useLongPress', link: '/useLongPress/' },
7071
],
7172
},
7273
{

packages/hooks/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import useInViewport from './useInViewport'
2525
import useKeyPress from './useKeyPress'
2626
import useLocalStorageState from './useLocalStorageState'
2727
import useLockFn from './useLockFn'
28+
import useLongPress from './useLongPress'
2829
import useMouse from './useMouse'
2930
import useMap from './useMap'
3031
import useMedia from './useMedia'
@@ -75,6 +76,7 @@ export {
7576
useKeyPress,
7677
useLocalStorageState,
7778
useLockFn,
79+
useLongPress,
7880
useMap,
7981
useMedia,
8082
useMouse,
@@ -97,5 +99,5 @@ export {
9799
useVirtualList,
98100
useWhyDidYouUpdate,
99101
useWinResize,
100-
useWebSocket,
102+
useWebSocket
101103
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2+
import useLongPress from '../index'
3+
import renderHook from 'test-utils/renderHook'
4+
5+
describe('useLongPressStatus tests', () => {
6+
let targetMock: HTMLElement
7+
const mouseDownEvent: PointerEvent = new PointerEvent('pointerdown')
8+
const mouseUpEvent: PointerEvent = new PointerEvent('pointerup')
9+
const mouseMoveEvent: PointerEvent = new PointerEvent('pointermove')
10+
beforeEach(() => {
11+
targetMock = document.createElement('div')
12+
vi.useFakeTimers({
13+
toFake: [ 'setTimeout', 'setInterval' ],
14+
});
15+
});
16+
afterEach(() => {
17+
vi.useRealTimers();
18+
});
19+
it('should change isPressed status when user longPress in 500ms', async () => {
20+
const [ result ] = renderHook(() => useLongPress(targetMock))
21+
22+
targetMock.dispatchEvent(mouseDownEvent);
23+
24+
expect(result.isPressing.value).toBeFalsy()
25+
26+
vi.advanceTimersByTime(500);
27+
28+
expect(result.isPressing.value).toBeTruthy()
29+
});
30+
31+
it('should record pressed every 100ms', () => {
32+
const [ result ] = renderHook(() => useLongPress(targetMock))
33+
targetMock.dispatchEvent(mouseDownEvent);
34+
35+
expect(result.pressingTime.value).toBe(0);
36+
37+
vi.advanceTimersByTime(500);
38+
expect(result.pressingTime.value).toBe(0);
39+
40+
vi.advanceTimersByTime(100);
41+
expect(result.pressingTime.value).toBe(100);
42+
43+
vi.advanceTimersByTime(100);
44+
expect(result.pressingTime.value).toBe(200);
45+
});
46+
47+
it('should reset pressingTime and isPressing when user mouseUp', async () => {
48+
const [ result ] = renderHook(() => useLongPress(targetMock))
49+
50+
targetMock.dispatchEvent(mouseDownEvent);
51+
vi.advanceTimersByTime(600);
52+
53+
expect(result.pressingTime.value).toBe(100);
54+
expect(result.isPressing.value).toBeTruthy()
55+
56+
targetMock.dispatchEvent(mouseUpEvent)
57+
58+
expect(result.pressingTime.value).toBe(0);
59+
expect(result.isPressing.value).toBeFalsy()
60+
});
61+
62+
it('should reset pressingTime and isPressing when user mouseMove', async () => {
63+
const [ result ] = renderHook(() => useLongPress(targetMock))
64+
65+
targetMock.dispatchEvent(mouseDownEvent);
66+
vi.advanceTimersByTime(600);
67+
68+
expect(result.pressingTime.value).toBe(100);
69+
expect(result.isPressing.value).toBeTruthy()
70+
71+
targetMock.dispatchEvent(mouseMoveEvent);
72+
73+
expect(result.pressingTime.value).toBe(0);
74+
expect(result.isPressing.value).toBeFalsy()
75+
});
76+
//
77+
it('should not cancel event on mouseLeave when cancelOnMove toggle is false', async () => {
78+
const [ { pressingTime, isPressing } ] = renderHook(() => useLongPress(targetMock, {
79+
cancelOnMove: false,
80+
}))
81+
82+
targetMock.dispatchEvent(mouseDownEvent);
83+
84+
vi.advanceTimersByTime(600);
85+
expect(pressingTime.value).toBe(100);
86+
expect(isPressing.value).toBeTruthy()
87+
88+
targetMock.dispatchEvent(mouseMoveEvent);
89+
90+
expect(pressingTime.value).toBe(100);
91+
expect(isPressing.value).toBeTruthy()
92+
});
93+
94+
it('should stop all event listener when component unmounted', async () => {
95+
const elementRemoveEventListenerSpy = vi.spyOn(targetMock, 'removeEventListener');
96+
const [ , app ] = renderHook(() => useLongPress(targetMock))
97+
98+
app.unmount()
99+
100+
expect(elementRemoveEventListenerSpy).toHaveBeenCalledWith(mouseDownEvent.type, expect.any(Function));
101+
expect(elementRemoveEventListenerSpy).toHaveBeenCalledWith(mouseUpEvent.type, expect.any(Function));
102+
expect(elementRemoveEventListenerSpy).toHaveBeenCalledWith(mouseMoveEvent.type, expect.any(Function));
103+
});
104+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<template>
2+
<div>
3+
<p>
4+
{{ pressingTime }} ms
5+
</p>
6+
<div>
7+
<vhp-button ref="pressButton"> {{ isPressing ? 'Pressing' : 'Click' }}</vhp-button>
8+
</div>
9+
</div>
10+
</template>
11+
12+
<script lang="ts" setup>
13+
import { useLongPress } from 'vue-hooks-plus'
14+
import { ref } from 'vue'
15+
16+
const pressButton = ref(null)
17+
const { pressingTime , isPressing } = useLongPress(pressButton)
18+
19+
</script>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
map:
3+
# Path mapped to docs
4+
path: /useLongPress
5+
---
6+
7+
# useLongPress
8+
9+
Listen for a long press on an element.
10+
11+
## Code demonstration
12+
13+
<demo src="./demo/demo.vue"
14+
language="vue"
15+
title="Basic usage"
16+
desc="listen for a long press on an element."> </demo>
17+
18+
## API
19+
20+
```typescript
21+
const { isPressing , pressingTime } = useLongPress(target: BasicTarget , options?: LongPressOptions)
22+
```
23+
24+
## Result
25+
26+
| Property | Description | Type |
27+
|--------------|------------------------------------------------------------------------------------------------------------------------------|--------------------------|
28+
| isPressing | Indicates the current pressing state. If pressing, the value is true; otherwise it's false. | `Readonly<Ref<boolean>>` |
29+
| pressingTime | Represents the duration of pressing (possibly in milliseconds). This value will only be updated during the pressing period. | `Readonly<Ref<number>>` |
30+
31+
## Params
32+
33+
| Property | Description | Type | Default |
34+
|----------|------------------------|-------------------------------------------------------------|---------|
35+
| target | DOM element or ref | `() => Element` \| `Element` \| `MutableRefObject<Element>` | - |
36+
| options | Additional config | `UseLongPressOptions` | - |
37+
38+
### DropOptions
39+
40+
| Property | Description | Type | Default |
41+
|---------------|------------------------------------------------------------------|-----------------------|---------|
42+
| delay | Time in ms till `longpress` gets called | `number` | 500 |
43+
| minUpdateTime | Minimum time interval in ms for updating the `longpress` event | `number` | 100 |
44+
| cancelOnMove | Whether to cancel the longpress event when mouse move | `boolean` | true |
45+
| modifiers | `longpress` event modifiers | `LongPressModifiers` | - |
46+
47+
### LongPressModifiers
48+
49+
| Property | Description | Type | Default |
50+
|------------|-----------------------------------------|--------------|---------|
51+
| stop | stopPropagation event | `boolean` | - |
52+
| once | eventListener once option | `boolean` | - |
53+
| prevent | preventDefault event | `boolean` | - |
54+
| capture | eventListener capture option | `boolean` | - |
55+
| self | check event target element same as self | `boolean` | - |
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { BasicTarget, getTargetElement } from '../utils/domTarget'
2+
import { DeepReadonly, readonly, Ref, ref } from 'vue'
3+
import useEffectWithTarget from '../utils/useEffectWithTarget'
4+
5+
export interface UseLongPressOptions {
6+
delay?: number;
7+
minUpdateTime?: number;
8+
cancelOnMove?: boolean;
9+
modifiers?: LongPressModifiers;
10+
}
11+
12+
export interface LongPressModifiers {
13+
stop?: boolean;
14+
once?: boolean;
15+
prevent?: boolean;
16+
capture?: boolean;
17+
self?: boolean;
18+
}
19+
20+
type MouseDownEvents = 'pointerdown' | 'touchstart' | 'mousedown';
21+
type MouseUpEvents = 'pointerup' | 'touchend' | 'mouseup';
22+
type MouseMoveEvents = 'pointermove' | 'touchmove' | 'mousemove';
23+
24+
const getSupportedMouseEvents = (): {
25+
mouseMove: MouseMoveEvents;
26+
mouseUp: MouseUpEvents;
27+
mouseDown: MouseDownEvents
28+
} => {
29+
const hasPointEvent = 'PointerEvent' in window;
30+
if (hasPointEvent) {
31+
return {
32+
mouseDown: 'pointerdown',
33+
mouseUp: 'pointerup',
34+
mouseMove: 'pointermove'
35+
}
36+
}
37+
38+
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
39+
40+
if (isTouch) {
41+
return {
42+
mouseDown: 'touchstart',
43+
mouseUp: 'touchend',
44+
mouseMove: 'touchmove'
45+
}
46+
}
47+
48+
return {
49+
mouseDown: 'mousedown',
50+
mouseUp: 'mouseup',
51+
mouseMove: 'mousemove'
52+
}
53+
}
54+
55+
const useLongPress: ( target: BasicTarget, options?: UseLongPressOptions ) => {
56+
pressingTime: DeepReadonly<Ref<number>>;
57+
isPressing: DeepReadonly<Ref<boolean>>
58+
} = ( target: BasicTarget, options: UseLongPressOptions = {} ) => {
59+
const DEFAULT_DELAY_TIME = 500
60+
const DEFAULT_UPDATE_TIME = 100
61+
62+
const isPressing = ref<boolean>(false)
63+
const pressingTime = ref(0);
64+
65+
let pressingTimer: ReturnType<typeof setTimeout> | undefined;
66+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
67+
const eventOptions = {
68+
capture: options?.modifiers?.capture,
69+
once: options?.modifiers?.once,
70+
};
71+
72+
useEffectWithTarget(
73+
() => {
74+
const targetElement = getTargetElement(target)
75+
if (!targetElement) {
76+
return
77+
}
78+
79+
const { mouseDown, mouseUp, mouseMove } = getSupportedMouseEvents()
80+
81+
function clear() {
82+
if (timeoutTimer) {
83+
clearTimeout(timeoutTimer);
84+
timeoutTimer = undefined;
85+
}
86+
87+
if (pressingTimer) {
88+
clearInterval(pressingTimer);
89+
pressingTimer = undefined;
90+
}
91+
pressingTime.value = 0;
92+
isPressing.value = false;
93+
}
94+
95+
function onDown( ev: Event ): void {
96+
if (options.modifiers?.self && ev.target !== targetElement) return;
97+
98+
clear();
99+
100+
if (options.modifiers?.prevent) ev.preventDefault();
101+
102+
if (options.modifiers?.stop) ev.stopPropagation();
103+
104+
timeoutTimer = setTimeout(() => {
105+
isPressing.value = true;
106+
pressingTimer = setInterval(() => {
107+
pressingTime.value += options.minUpdateTime || DEFAULT_UPDATE_TIME;
108+
}, options.minUpdateTime || DEFAULT_UPDATE_TIME)
109+
}, options.delay || DEFAULT_DELAY_TIME);
110+
}
111+
112+
113+
targetElement.addEventListener(mouseDown, onDown, eventOptions);
114+
targetElement.addEventListener(mouseUp, clear, eventOptions);
115+
if (options.cancelOnMove ?? true) {
116+
targetElement.addEventListener(mouseMove, clear, eventOptions);
117+
}
118+
119+
return () => {
120+
targetElement.removeEventListener(mouseDown, onDown);
121+
targetElement.removeEventListener(mouseUp, clear);
122+
if (options.cancelOnMove ?? true) {
123+
targetElement.removeEventListener(mouseMove, clear);
124+
}
125+
}
126+
},
127+
[],
128+
target,
129+
)
130+
131+
return { isPressing: readonly(isPressing), pressingTime: readonly(pressingTime) }
132+
}
133+
134+
export default useLongPress

0 commit comments

Comments
 (0)