Skip to content

Commit

Permalink
Merge pull request #407 from antonioru/feat/use-long-press
Browse files Browse the repository at this point in the history
feat(hook): introduces useLongPress hook
  • Loading branch information
antonioru committed Mar 18, 2023
2 parents bc24aa4 + d94b915 commit 25cbcb7
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 42 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ module.exports = {
'import/no-named-as-default-member': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/no-non-null-assertion': 'off'
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-invalid-void-type': 'off'
},
overrides: [
{
Expand Down
14 changes: 5 additions & 9 deletions .github/workflows/branch-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@ jobs:
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [ 18.14 ]

steps:
- uses: actions/checkout@v2
- name: Testing branch on Node version ${{ matrix.node-version }}
uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install --force
node-version: 18.14

- run: npm install
- run: npm run lint
- run: npm run build
- run: npm test
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,31 @@ jobs:
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18.14
registry-url: https://registry.npmjs.org/

- name: NPM CI
- name: NPM Install
run: npm install

- name: Repository build
- name: Build
run: npm run build

- name: Tests (with coverage)
run: npm test -- --coverage

- name: Coveralls GitHub Action
uses: coverallsapp/github-action@v1.1.1
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Build website (Github pages)
run: npm run build-doc --if-present

- name: Publish website on GitHub Pages
uses: crazy-max/ghaction-github-pages@v2.1.2
uses: crazy-max/ghaction-github-pages@v3
with:
build_dir: dist-ghpages
env:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1020,3 +1020,13 @@ Errored release
### Fixes

- wrong dependency in package.json

## [4.2.0] - 2023-03-18

### Adds

- `useLongPress` hook

### Fixes

- Deprecated github actions version
87 changes: 87 additions & 0 deletions docs/useLongPress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# useLongPress

A hook that facilitates the implementation of a long press functionality on a given target, supporting both mouse and touch events.

### Why? 💡

- Provides an easy way to add long-press functionality to a specific target element
- Automatically adds mouse event listeners to the specified target element
- Automatically removes the listeners when the component unmounts
- Enables abstractions on mouse-related and touch-related events

### Basic Usage:

```jsx harmony
import { useRef, useState } from 'react';
import { Tag, Space, Typography, Alert } from 'antd';
import useLongPress from 'beautiful-react-hooks/useLongPress';

const MyComponent = () => {
const [coordinates, setCoordinates] = useState([0, 0]);
const ref = useRef();
const [longPressCount, setLongPressCount] = useState(0)
const { isLongPressing, onLongPressStart, onLongPressEnd } = useLongPress(ref);

onLongPressStart(() => {
setLongPressCount(() => {
return longPressCount + 1
});
});

onLongPressEnd(() => {
setLongPressCount(() => {
return longPressCount + 1
});
})

return (
<DisplayDemo title="useLongPress">
<div ref={ref}>
<Space direction="vertical">
<Alert message="Long press this box to get information on the long press event" type="info" showIcon />
<Tag color={isLongPressing ? 'green' : 'red'}>isLongPressing: {isLongPressing ? 'yes' : 'no'}</Tag>
{!!longPressCount && (
<Typography.Paragraph>
Long press events count:
<Tag color="green">{longPressCount}</Tag>
</Typography.Paragraph>
)}
</Space>
</div>
</DisplayDemo>
);
};

<MyComponent />
```

### Press duration:

You can specify the duration of the long press by passing a number as the second argument to the hook.

```jsx harmony
import { useRef, useState } from 'react';
import { Tag, Space, Typography, Alert } from 'antd';
import useLongPress from 'beautiful-react-hooks/useLongPress';

const MyComponent = () => {
const [coordinates, setCoordinates] = useState([0, 0]);
const ref = useRef();
const { isLongPressing } = useLongPress(ref, 1000);

return (
<DisplayDemo title="useLongPress">
<div ref={ref}>
<Space direction="vertical">
<Alert message="Long press this box to get information on the long press event" type="info" showIcon />
<Tag color={isLongPressing ? 'green' : 'red'}>isLongPressing: {isLongPressing ? 'yes' : 'no'}</Tag>
</Space>
</div>
</DisplayDemo>
);
};

<MyComponent />
```

<!-- Types -->
40 changes: 21 additions & 19 deletions docs/useMouseEvents.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ const MyComponent = () => {
});

return (
<DisplayDemo title="useMouseEvent">
<div ref={ref}>
<Space direction="vertical">
<Alert message="Move mouse over this box to get its current coordinates" type="info" showIcon />
<Tag color="green">ClientX: {coordinates[0]}</Tag>
<Tag color="green">ClientY: {coordinates[1]}</Tag>
</Space>
</div>
</DisplayDemo>
<DisplayDemo title="useMouseEvent">
<div ref={ref}>
<Space direction="vertical">
<Alert message="Move mouse over this box to get its current coordinates" type="info" showIcon />
<Tag color="green">ClientX: {coordinates[0]}</Tag>
<Tag color="green">ClientY: {coordinates[1]}</Tag>
</Space>
</div>
</DisplayDemo>
);
};

Expand All @@ -73,13 +73,13 @@ const MyComponent = () => {
});

return (
<DisplayDemo title="useMouseEvent">
<Space direction="vertical">
<Alert message="Move mouse around to get its current global coordinates" type="info" showIcon />
<Tag color="green">ClientX: {coordinates[0]}</Tag>
<Tag color="green">ClientY: {coordinates[1]}</Tag>
</Space>
</DisplayDemo>
<DisplayDemo title="useMouseEvent">
<Space direction="vertical">
<Alert message="Move mouse around to get its current global coordinates" type="info" showIcon />
<Tag color="green">ClientX: {coordinates[0]}</Tag>
<Tag color="green">ClientY: {coordinates[1]}</Tag>
</Space>
</DisplayDemo>
);
};

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

return (
<div onMouseDown={mouseDownHandler} />
<div onMouseDown={mouseDownHandler} />
);
};
```

<!-- Types -->

### Types

```typescript static
import { type RefObject } from 'react';
/**
Expand Down Expand Up @@ -141,4 +142,5 @@ declare const useMouseEvents: <TElement extends HTMLElement>(targetRef?: RefObje
export default useMouseEvents;

```
<!-- Types:end -->

<!-- Types:end -->
14 changes: 7 additions & 7 deletions src/useEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import safeHasOwnProperty from './shared/safeHasOwnProperty'
* when fired from that HTML Element.
*/
const useEvent = <TEvent extends Event, TElement extends HTMLElement = HTMLElement>
(ref: RefObject<TElement>, eventName: string, options?: AddEventListenerOptions) => {
(target: RefObject<TElement>, eventName: string, options?: AddEventListenerOptions) => {
const [handler, setHandler] = createHandlerSetter<TEvent>()

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

Expand All @@ -21,16 +21,16 @@ const useEvent = <TEvent extends Event, TElement extends HTMLElement = HTMLEleme
}
}

if (ref.current?.addEventListener && handler.current) {
ref.current.addEventListener(eventName, cb, options)
if (target.current?.addEventListener && handler.current) {
target.current.addEventListener(eventName, cb, options)
}

return () => {
if (ref.current?.addEventListener && handler.current) {
ref.current.removeEventListener(eventName, cb, options)
if (target.current?.addEventListener && handler.current) {
target.current.removeEventListener(eventName, cb, options)
}
}
}, [eventName, ref.current, options])
}, [eventName, target.current, options])

return setHandler
}
Expand Down
64 changes: 64 additions & 0 deletions src/useLongPress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { type RefObject, useCallback, useState } from 'react'
import useMouseEvents from './useMouseEvents'
import useConditionalTimeout from './useConditionalTimeout'
import createHandlerSetter from './factory/createHandlerSetter'
import useTouchEvents from './useTouchEvents'
import { type CallbackSetter } from './shared/types'

/**
* A hook that facilitates the implementation of the long press functionality on a given target, supporting both mouse and touch events.
*/
const useLongPress = <TElement extends HTMLElement>(target: RefObject<TElement>, duration = 500) => {
const { onMouseDown, onMouseUp, onMouseLeave } = useMouseEvents<TElement>(target, false)
const { onTouchStart, onTouchEnd } = useTouchEvents(target, false)
const [isLongPressing, setIsLongPressing] = useState(false)
const [timerOn, startTimer] = useState(false)
const [onLongPressStart, setOnLongPressStart] = createHandlerSetter<void>()
const [onLongPressEnd, setOnLongPressEnd] = createHandlerSetter<void>()

const longPressStart = useCallback((event: MouseEvent | TouchEvent) => {
event.preventDefault()
startTimer(true)
}, [])

const longPressStop = useCallback((event: MouseEvent | TouchEvent) => {
if (!isLongPressing) return
clearTimeout()
setIsLongPressing(false)
startTimer(false)
event.preventDefault()

if (onLongPressEnd?.current) {
onLongPressEnd.current()
}
}, [isLongPressing])

const [, clearTimeout] = useConditionalTimeout(() => {
setIsLongPressing(true)

if (onLongPressStart?.current) {
onLongPressStart.current()
}
}, duration, timerOn)

onMouseDown(longPressStart)
onMouseLeave(longPressStop)
onMouseUp(longPressStop)

onTouchStart(longPressStart)
onTouchEnd(longPressStop)

return Object.freeze<UseLongPressResult>({
isLongPressing,
onLongPressStart: setOnLongPressStart,
onLongPressEnd: setOnLongPressEnd
})
}

export interface UseLongPressResult {
isLongPressing: boolean
onLongPressStart: CallbackSetter<void>
onLongPressEnd: CallbackSetter<void>
}

export default useLongPress
18 changes: 18 additions & 0 deletions test/useLongPress.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { cleanup, renderHook } from '@testing-library/react-hooks'
import useLongPress from '../dist/useLongPress'
import assertHook from './utils/assertHook'

describe('useLongPress', () => {
beforeEach(() => cleanup())

assertHook(useLongPress)

it('should return a boolean value reporting whether the long-press event is happening as well as the handlers setters', () => {
const ref = { current: document.createElement('div') }
const { result } = renderHook(() => useLongPress(ref))

expect(result.current.isLongPressing).to.be.a('boolean')
expect(result.current.onLongPressStart).to.be.a('function')
expect(result.current.onLongPressEnd).to.be.a('function')
})
})

0 comments on commit 25cbcb7

Please sign in to comment.