React Long Press Hook
React hook for detecting click / tap / point and hold event
Main features
- Mouse, Touch and Pointer events support
- Pass custom context and access it in callback
- Cancel long press if moved too far from the target
- Flexible callbacks:
onStart
, onMove
, onFinish
, onCancel
- Disable hook when necessary
- Filter undesired events (like mouse right clicks)
Table of Contents
- Installation
- Basic Usage
- Advanced Usage
- Definition
- Callback
- Options
- Additional callbacks
- Result
- Context
- Handlers
- Examples
- Advanced usage example
- Live Examples
- Version 1
- Version 2
- Version 3
- Migration
- v1 to v2
- v2 to v3
- Long press outside element
- Changelog
- FAQ
- Support us
- License
Installation
yarn add use-long-press
or
npm install --save use-long-press
Basic Usage
import React from 'react';
import { useLongPress } from 'use-long-press';
const Example = () => {
const bind = useLongPress(() => {
console.log('Long pressed!');
});
return <button {...bind()}>Press me</button>;
};
Advanced usage
Hook definition
Pseudocode
useLongPress(callback [, options]): bindFn
TypeScript
declare function useLongPress<
Target extends Element = Element,
Context = unknown,
Callback extends LongPressCallback<Target, Context> = LongPressCallback<Target, Context>
>(
callback: Callback | null,
options?: LongPressOptions<Target, Context>
): LongPressResult<LongPressHandlers<Target>, Context>;
Callback
Hook first parameter, callback, can be either function or null
(if you want to disable the hook).
Options
You can supply options object as a hook second parameter. All options inside the object are optional.
Name | Type | Default | Description |
---|
threshold | number | 400 | Time user need to hold click or tap before long press callback is triggered |
captureEvent | boolean | false | If React MouseEvent (or TouchEvent) should be supplied as first argument to callbacks |
detect | 'mouse' | 'touch' | 'pointer' | 'pointer' | Which event handlers should be returned from bind function.
TS enum: LongPressEventType |
cancelOnMovement | boolean | number | false | If long press should be cancelled when detected movement while pressing. Use boolean value to turn it on / off or number value to specify move tolerance in pixels.
For more information on how this option work check JSDoc. |
cancelOutsideElement | boolean | true | If long press should be canceled when moving mouse / touch / pointer outside the element to which it was bound.
When cancelled returns LongPressCallbackReason.CancelledOutsideElement ('cancelled-outside-element') as a cancel reason in onCancel callback.
Works for mouse and pointer events, touch events will be supported in the future. |
filterEvents | (event) => boolean | undefined | If provided, it gives you the ability to ignore long press detection on specified conditions (e.g. on right mouse click).
When function returns false , it will prevent ANY callbacks from triggering (including onStart and onCancel) as well as capturing event. |
onStart | (event, meta) => void | undefined | Called when element is initially pressed (before starting timer which detects long press) |
onMove | (event, meta) => void | undefined | Called on move after pressing element. Since position is extracted from event after this callback is called, you can potentially make changes to event position.
Position is extracted using getCurrentPosition method from use-long-press.utils.ts |
onFinish | (event, meta) => void | undefined | Called when press is released AFTER threshold time elapses, therefore after long press occurs and callback is called. |
onCancel | (event, meta) => void | undefined | Called when press is released BEFORE threshold time elapses, therefore before long press could occur. |
Additional callbacks
All callbacks (including main callback function) has same structure.
Pseudocode
callbackFn(event, meta): void
TypeScript
type LongPressCallback<Target extends Element = Element, Context = unknown> = (
event: LongPressEvent<Target>,
meta: LongPressCallbackMeta<Context>
) => void
As a first argument callback receives event from proper handler (e.g. onMouseDown
) and as second receives meta object with following structure:
Pseudocode
{ [context: any], [reason: string] }
TypeScript
export type LongPressCallbackMeta<Context = unknown> = { context?: Context; reason?: LongPressCallbackReason };
Both object properties are optional.
context
will be present if you pass it to bind function. See context for more info.reason
will be present in onCancel callback to indicate why long press was cancelled
'cancelled-by-movement'
(TS: LongPressCallbackReason.CancelledByMovement
) - when cancelOnMovement option is enabled and moved outside specified tolerance'cancelled-by-release'
(TS: LongPressCallbackReason.CancelledByRelease
) - when press was released before threshold time elapsed
Result
As a result hook returns callable function (also referred as bind
) in order to pass context if necessary.
bind
function return object with various handlers.
Context
You can supply custom context to the bind
function like bind(context)
and then access it from callbacks (onStart
, onFinish
, onCancel
, onMove
) second argument e.g.: onStart: (event, { context }) => ...
.
Handlers
Handlers are returned from bind
function in a form of object which can be spread to react element. Contents of this object depend on detect option value:
'mouse'
onMouseDown
onMouseMove
onMouseUp
onMouseLeave
(only when cancelOutsideElement is enabled)
'touch'
onTouchStart
onTouchMove
onTouchEnd
'pointer'
onPointerDown
onPointerMove
onPointerUp
onPointerLeave
(only when cancelOutsideElement is enabled)
Examples
Advanced usage example
import React, { useState, useCallback } from 'react';
import { useLongPress } from 'use-long-press';
export default function AdvancedExample() {
const [enabled, setEnabled] = useState(true);
const callback = useCallback(event => {
alert('Long pressed!');
}, []);
const bind = useLongPress(enabled ? callback : null, {
onStart: event => console.log('Press started'),
onFinish: event => console.log('Long press finished'),
onCancel: event => console.log('Press cancelled'),
onMove: event => console.log('Detected mouse or touch movement'),
filterEvents: event => true,
threshold: 500,
captureEvent: true,
cancelOnMovement: 25,
cancelOutsideElement: true,
detect: 'pointer',
});
return (
<div>
<button {...bind()}>Press and hold</button>
<div>
<label htmlFor="enabled">
<input type="checkbox" id="enabled" checked={enabled} onChange={() => setEnabled(current => !current)} />
Hook enabled
</label>
</div>
</div>
);
}
Live Examples
Version 1 (deprecated)
Version 2 (deprecated)
Version 3
Migration
v1 to v2
[BREAKING CHANGE] Context support
Now hook returns function which can be called with any context in order to access it in callbacks
Before
const bind = useLongPress(() => console.log('Long pressed'));
return <button {...bind}>Click me</button>;
After
const bind = useLongPress((event, { context }) => console.log('Long pressed with', context));
return <button {...bind('I am context')}>Click me</button>;
return <button {...bind()}>Click me</button>;
[NEW] Reason for cancellation
Now onCancel
receives cancellation context which can be either:
LongPressEventReason.CANCELED_BY_TIMEOUT
('canceled-by-timeout'
)LongPressEventReason.CANCELED_BY_MOVEMENT
('canceled-by-movement'
)
You can access it like this:
const bind = useLongPress(() => console.log('Long pressed'), {
onCancel: (event, { reason }) => console.log('Cancellation reason:', reason)
})
v2 to v3
[BREAKING CHANGE] Drop support for 'both'
option in detect
param
Returning both mouse and touch handlers as a hook result caused unintended edge cases on touch devices that emulated clicks. Therefore 'both'
value was removed and hook is now using 'pointer'
as a default value for detect
param.
This also enables to support more type of events in the future.
Pointer events should be sufficient replacement for 'both'
option, but you can also programmatically detect if current device support touch events and set proper detect
value based on that.
Before
const bind = useLongPress(() => console.log('Long pressed'), {
detect: 'both',
})
After
const bind = useLongPress(() => console.log('Long pressed'), {
detect: 'pointer',
})
[BREAKING CHANGE] Typings and param values
TypeScript's typings were refactored to use more consistent and precise names. Also changed callback reason values (see LongPressEventReason
)
- Changed generics order from
useLongPress<Target, Callback, Context>
to useLongPress<Target, Context, Callback>
- Renamed
LongPressDetectEvents
enum to LongPressEventType
LongPressDetectEvents.MOUSE
-> LongPressEventType.Mouse
LongPressDetectEvents.TOUCH
-> LongPressEventType.Touch
- Added
LongPressEventType.Pointer
- Renamed
LongPressEventReason
enum to LongPressCallbackReason
LongPressEventReason.CANCELED_BY_MOVEMENT
('canceled-by-movement') -> LongPressCallbackReason.CancelledByMovement
('cancelled-by-movement')LongPressEventReason.CANCELED_BY_TIMEOUT
('canceled-by-timeout') -> LongPressCallbackReason.CancelledByRelease
('cancelled-by-release')
- Removed
Coordinates
type - Renamed
EmptyObject
type to LongPressEmptyHandlers
- Renamed
CallableContextResult
type to LongPressResult
- Renamed
LongPressResult
type to LongPressHandlers
- Added mouse and touch handlers types -
LongPressMouseHandlers
and LongPressTouchHandlers
⚠️ Mouse / pointer leaving element while pressing
Versions before 3.1.0 did not have cancelOutsideElement
option which when enabled make useLongPress behave in the same manner as v1 and v2 which is cancelling long press when mouse / pointer leave pressed element.
Therefore, when added in 3.1.0 its default value was set to true
, to restore previous versions behaviour.
Backstory behind it is that v3 was supposed to fix problem with detecting cancelling long press when finishing it outside component scope. It was achieved by detecting mouse / pointer up events on window and removing triggering cancel on mouse / pointer leaving the element. Unfortunately that solution was backward incompatible so adding cancelOutsideElement option was a way to fix that as well as new feature, hence why it was introduced as a minor version bump instead of a bugfix.
❗️It is recommended that you upgrade use-long-press
to at least 3.1.0 in order to seamlessly migrate from v1 / v2 to v3❗️
Changelog
List of changes made with each version can be found here
FAQ
Why deprecate v1 and v2 and move to new repo?
v1 and v2 deprecation
Using both mouse and touch handlers on same element was a good idea at the beginning to enable out of the box support for all device types without the need to manually control which events should be detected. After adding support for pointer events that is no longer necessary because they are better suited to handle this case.
All tests had to be rewritten because while supporting React 17 and 18, using Enzyme for tests was no longer possible due to the lack of official adapters. Therefore, every test was rewritten to react-testing-library
and generalised in order to be able to test each type of events (mouse, touch and pointer) without repeating the same code all over again.
Overall considering the reasons mentioned above, maintaining old versions was no longer a viable option hence why the deprecation. If you want to upgrade from v1 or v2 see migration guide.
Moving to new repository
Old repository structure was causing false positives on package vulnerabilities because of building / testing tools in dev dependencies. New monorepo architecture solves that problem by separating repository package.json from use-long-press
package.json
Using monorepo is much easier for maintaining multiple packages and I plan to move use-double-tap
and react-interval-hook
to this repository as well as add new packages in the future.
Support us
If you like my work, consider making a donation through Github Sponsors.
License
MIT © minwork