Skip to content

Commit e22857c

Browse files
committed
feat(hook): introduces useLongPress hook
1 parent 9631dc6 commit e22857c

7 files changed

+205
-27
lines changed

.eslintrc.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ module.exports = {
3434
'import/no-named-as-default-member': 'off',
3535
'@typescript-eslint/explicit-function-return-type': 'off',
3636
'@typescript-eslint/strict-boolean-expressions': 'off',
37-
'@typescript-eslint/no-non-null-assertion': 'off'
37+
'@typescript-eslint/no-non-null-assertion': 'off',
38+
'@typescript-eslint/no-invalid-void-type': 'off'
3839
},
3940
overrides: [
4041
{

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1020,3 +1020,9 @@ Errored release
10201020
### Fixes
10211021

10221022
- wrong dependency in package.json
1023+
1024+
## [4.2.0] - 2023-03-18
1025+
1026+
### Adds
1027+
1028+
- `useLongPress` hook

docs/useLongPress.md

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# useLongPress
2+
3+
A hook that facilitates the implementation of a long press functionality on a given target, supporting both mouse and touch events.
4+
5+
### Why? 💡
6+
7+
- Provides an easy way to add long-press functionality to a specific target element
8+
- Automatically adds mouse event listeners to the specified target element
9+
- Automatically removes the listeners when the component unmounts
10+
- Enables abstractions on mouse-related and touch-related events
11+
12+
### Basic Usage:
13+
14+
```jsx harmony
15+
import { useRef, useState } from 'react';
16+
import { Tag, Space, Typography, Alert } from 'antd';
17+
import useLongPress from 'beautiful-react-hooks/useLongPress';
18+
19+
const MyComponent = () => {
20+
const [coordinates, setCoordinates] = useState([0, 0]);
21+
const ref = useRef();
22+
const [longPressCount, setLongPressCount] = useState(0)
23+
const { isLongPressing, onLongPressStart, onLongPressEnd } = useLongPress(ref);
24+
25+
onLongPressStart(() => {
26+
setLongPressCount(() => {
27+
return longPressCount + 1
28+
});
29+
});
30+
31+
onLongPressEnd(() => {
32+
setLongPressCount(() => {
33+
return longPressCount + 1
34+
});
35+
})
36+
37+
return (
38+
<DisplayDemo title="useLongPress">
39+
<div ref={ref}>
40+
<Space direction="vertical">
41+
<Alert message="Long press this box to get information on the long press event" type="info" showIcon />
42+
<Tag color={isLongPressing ? 'green' : 'red'}>isLongPressing: {isLongPressing ? 'yes' : 'no'}</Tag>
43+
{!!longPressCount && (
44+
<Typography.Paragraph>
45+
Long press events count:
46+
<Tag color="green">{longPressCount}</Tag>
47+
</Typography.Paragraph>
48+
)}
49+
</Space>
50+
</div>
51+
</DisplayDemo>
52+
);
53+
};
54+
55+
<MyComponent />
56+
```
57+
58+
### Press duration:
59+
60+
You can specify the duration of the long press by passing a number as the second argument to the hook.
61+
62+
```jsx harmony
63+
import { useRef, useState } from 'react';
64+
import { Tag, Space, Typography, Alert } from 'antd';
65+
import useLongPress from 'beautiful-react-hooks/useLongPress';
66+
67+
const MyComponent = () => {
68+
const [coordinates, setCoordinates] = useState([0, 0]);
69+
const ref = useRef();
70+
const { isLongPressing } = useLongPress(ref, 1000);
71+
72+
return (
73+
<DisplayDemo title="useLongPress">
74+
<div ref={ref}>
75+
<Space direction="vertical">
76+
<Alert message="Long press this box to get information on the long press event" type="info" showIcon />
77+
<Tag color={isLongPressing ? 'green' : 'red'}>isLongPressing: {isLongPressing ? 'yes' : 'no'}</Tag>
78+
</Space>
79+
</div>
80+
</DisplayDemo>
81+
);
82+
};
83+
84+
<MyComponent />
85+
```
86+
87+
<!-- Types -->

docs/useMouseEvents.md

+21-19
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ const MyComponent = () => {
3939
});
4040

4141
return (
42-
<DisplayDemo title="useMouseEvent">
43-
<div ref={ref}>
44-
<Space direction="vertical">
45-
<Alert message="Move mouse over this box to get its current coordinates" type="info" showIcon />
46-
<Tag color="green">ClientX: {coordinates[0]}</Tag>
47-
<Tag color="green">ClientY: {coordinates[1]}</Tag>
48-
</Space>
49-
</div>
50-
</DisplayDemo>
42+
<DisplayDemo title="useMouseEvent">
43+
<div ref={ref}>
44+
<Space direction="vertical">
45+
<Alert message="Move mouse over this box to get its current coordinates" type="info" showIcon />
46+
<Tag color="green">ClientX: {coordinates[0]}</Tag>
47+
<Tag color="green">ClientY: {coordinates[1]}</Tag>
48+
</Space>
49+
</div>
50+
</DisplayDemo>
5151
);
5252
};
5353

@@ -73,13 +73,13 @@ const MyComponent = () => {
7373
});
7474

7575
return (
76-
<DisplayDemo title="useMouseEvent">
77-
<Space direction="vertical">
78-
<Alert message="Move mouse around to get its current global coordinates" type="info" showIcon />
79-
<Tag color="green">ClientX: {coordinates[0]}</Tag>
80-
<Tag color="green">ClientY: {coordinates[1]}</Tag>
81-
</Space>
82-
</DisplayDemo>
76+
<DisplayDemo title="useMouseEvent">
77+
<Space direction="vertical">
78+
<Alert message="Move mouse around to get its current global coordinates" type="info" showIcon />
79+
<Tag color="green">ClientX: {coordinates[0]}</Tag>
80+
<Tag color="green">ClientY: {coordinates[1]}</Tag>
81+
</Space>
82+
</DisplayDemo>
8383
);
8484
};
8585

@@ -105,14 +105,15 @@ const MyComponent = (props) => {
105105
const { mouseDownHandler } = props;
106106

107107
return (
108-
<div onMouseDown={mouseDownHandler} />
108+
<div onMouseDown={mouseDownHandler} />
109109
);
110110
};
111111
```
112112

113113
<!-- Types -->
114+
114115
### Types
115-
116+
116117
```typescript static
117118
import { type RefObject } from 'react';
118119
/**
@@ -141,4 +142,5 @@ declare const useMouseEvents: <TElement extends HTMLElement>(targetRef?: RefObje
141142
export default useMouseEvents;
142143

143144
```
144-
<!-- Types:end -->
145+
146+
<!-- Types:end -->

src/useEvent.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import safeHasOwnProperty from './shared/safeHasOwnProperty'
77
* when fired from that HTML Element.
88
*/
99
const useEvent = <TEvent extends Event, TElement extends HTMLElement = HTMLElement>
10-
(ref: RefObject<TElement>, eventName: string, options?: AddEventListenerOptions) => {
10+
(target: RefObject<TElement>, eventName: string, options?: AddEventListenerOptions) => {
1111
const [handler, setHandler] = createHandlerSetter<TEvent>()
1212

13-
if (!!ref && !safeHasOwnProperty(ref, 'current')) {
13+
if (!!target && !safeHasOwnProperty(target, 'current')) {
1414
throw new Error('Unable to assign any scroll event to the given ref')
1515
}
1616

@@ -21,16 +21,16 @@ const useEvent = <TEvent extends Event, TElement extends HTMLElement = HTMLEleme
2121
}
2222
}
2323

24-
if (ref.current?.addEventListener && handler.current) {
25-
ref.current.addEventListener(eventName, cb, options)
24+
if (target.current?.addEventListener && handler.current) {
25+
target.current.addEventListener(eventName, cb, options)
2626
}
2727

2828
return () => {
29-
if (ref.current?.addEventListener && handler.current) {
30-
ref.current.removeEventListener(eventName, cb, options)
29+
if (target.current?.addEventListener && handler.current) {
30+
target.current.removeEventListener(eventName, cb, options)
3131
}
3232
}
33-
}, [eventName, ref.current, options])
33+
}, [eventName, target.current, options])
3434

3535
return setHandler
3636
}

src/useLongPress.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { type RefObject, useCallback, useState } from 'react'
2+
import useMouseEvents from './useMouseEvents'
3+
import useConditionalTimeout from './useConditionalTimeout'
4+
import createHandlerSetter from './factory/createHandlerSetter'
5+
import useTouchEvents from './useTouchEvents'
6+
import { type CallbackSetter } from './shared/types'
7+
8+
/**
9+
* A hook that facilitates the implementation of the long press functionality on a given target, supporting both mouse and touch events.
10+
*/
11+
const useLongPress = <TElement extends HTMLElement>(target: RefObject<TElement>, duration = 500) => {
12+
const { onMouseDown, onMouseUp, onMouseLeave } = useMouseEvents<TElement>(target, false)
13+
const { onTouchStart, onTouchEnd } = useTouchEvents(target, false)
14+
const [isLongPressing, setIsLongPressing] = useState(false)
15+
const [timerOn, startTimer] = useState(false)
16+
const [onLongPressStart, setOnLongPressStart] = createHandlerSetter<void>()
17+
const [onLongPressEnd, setOnLongPressEnd] = createHandlerSetter<void>()
18+
19+
const longPressStart = useCallback((event: MouseEvent | TouchEvent) => {
20+
event.preventDefault()
21+
startTimer(true)
22+
}, [])
23+
24+
const longPressStop = useCallback((event: MouseEvent | TouchEvent) => {
25+
if (!isLongPressing) return
26+
clearTimeout()
27+
setIsLongPressing(false)
28+
startTimer(false)
29+
event.preventDefault()
30+
31+
if (onLongPressEnd?.current) {
32+
onLongPressEnd.current()
33+
}
34+
}, [isLongPressing])
35+
36+
const [, clearTimeout] = useConditionalTimeout(() => {
37+
setIsLongPressing(true)
38+
39+
if (onLongPressStart?.current) {
40+
onLongPressStart.current()
41+
}
42+
}, duration, timerOn)
43+
44+
onMouseDown(longPressStart)
45+
onMouseLeave(longPressStop)
46+
onMouseUp(longPressStop)
47+
48+
onTouchStart(longPressStart)
49+
onTouchEnd(longPressStop)
50+
51+
return Object.freeze<UseLongPressResult>({
52+
isLongPressing,
53+
onLongPressStart: setOnLongPressStart,
54+
onLongPressEnd: setOnLongPressEnd
55+
})
56+
}
57+
58+
export interface UseLongPressResult {
59+
isLongPressing: boolean
60+
onLongPressStart: CallbackSetter<void>
61+
onLongPressEnd: CallbackSetter<void>
62+
}
63+
64+
export default useLongPress

test/useLongPress.spec.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { cleanup, renderHook } from '@testing-library/react-hooks'
2+
import useLongPress from '../dist/useLongPress'
3+
import assertHook from './utils/assertHook'
4+
5+
describe('useLongPress', () => {
6+
beforeEach(() => cleanup())
7+
8+
assertHook(useLongPress)
9+
10+
it('should return a boolean value reporting whether the long-press event is happening as well as the handlers setters', () => {
11+
const ref = { current: document.createElement('div') }
12+
const { result } = renderHook(() => useLongPress(ref))
13+
14+
expect(result.current.isLongPressing).to.be.a('boolean')
15+
expect(result.current.onLongPressStart).to.be.a('function')
16+
expect(result.current.onLongPressEnd).to.be.a('function')
17+
})
18+
})

0 commit comments

Comments
 (0)