Is there a way to add long press event in react-web application?
I have list of addresses. On long press on any address, I want to
I've created a codesandbox with a hook to handle long press and click. Basically, on mouse down, touch start events, a timer is created with setTimeout. When the provided time elapses, it triggers long press.
On mouse up, mouse leave, touchend, etc, the timer is cleared.
useLongPress.js
import { useCallback, useRef, useState } from "react";
const useLongPress = (
onLongPress,
onClick,
{ shouldPreventDefault = true, delay = 300 } = {}
) => {
const [longPressTriggered, setLongPressTriggered] = useState(false);
const timeout = useRef();
const target = useRef();
const start = useCallback(
event => {
if (shouldPreventDefault && event.target) {
event.target.addEventListener("touchend", preventDefault, {
passive: false
});
target.current = event.target;
}
timeout.current = setTimeout(() => {
onLongPress(event);
setLongPressTriggered(true);
}, delay);
},
[onLongPress, delay, shouldPreventDefault]
);
const clear = useCallback(
(event, shouldTriggerClick = true) => {
timeout.current && clearTimeout(timeout.current);
shouldTriggerClick && !longPressTriggered && onClick();
setLongPressTriggered(false);
if (shouldPreventDefault && target.current) {
target.current.removeEventListener("touchend", preventDefault);
}
},
[shouldPreventDefault, onClick, longPressTriggered]
);
return {
onMouseDown: e => start(e),
onTouchStart: e => start(e),
onMouseUp: e => clear(e),
onMouseLeave: e => clear(e, false),
onTouchEnd: e => clear(e)
};
};
const isTouchEvent = event => {
return "touches" in event;
};
const preventDefault = event => {
if (!isTouchEvent(event)) return;
if (event.touches.length < 2 && event.preventDefault) {
event.preventDefault();
}
};
export default useLongPress;
To use the hook,
App.js
import useLongPress from "./useLongPress";
export default function App() {
const onLongPress = () => {
console.log('longpress is triggered');
};
const onClick = () => {
console.log('click is triggered')
}
const defaultOptions = {
shouldPreventDefault: true,
delay: 500,
};
const longPressEvent = useLongPress(onLongPress, onClick, defaultOptions);
return (
<div className="App">
<button {...longPressEvent}>use Loooong Press</button>
</div>
);
}
Older answer for class components:
You can use MouseDown, MouseUp, TouchStart, TouchEnd events to control timers that can act as a long press event. Check out the code below
class App extends Component {
constructor() {
super()
this.handleButtonPress = this.handleButtonPress.bind(this)
this.handleButtonRelease = this.handleButtonRelease.bind(this)
}
handleButtonPress () {
this.buttonPressTimer = setTimeout(() => alert('long press activated'), 1500);
}
handleButtonRelease () {
clearTimeout(this.buttonPressTimer);
}
render() {
return (
<div
onTouchStart={this.handleButtonPress}
onTouchEnd={this.handleButtonRelease}
onMouseDown={this.handleButtonPress}
onMouseUp={this.handleButtonRelease}
onMouseLeave={this.handleButtonRelease}>
Button
</div>
);
}
}
Based on @Sublime me comment above about avoiding multiple re-renders, my version doesn't use anything that triggers renders:
export function useLongPress({
onClick = () => {},
onLongPress = () => {},
ms = 300,
} = {}) {
const timerRef = useRef(false);
const eventRef = useRef({});
const callback = useCallback(() => {
onLongPress(eventRef.current);
eventRef.current = {};
timerRef.current = false;
}, [onLongPress]);
const start = useCallback(
(ev) => {
ev.persist();
eventRef.current = ev;
timerRef.current = setTimeout(callback, ms);
},
[callback, ms]
);
const stop = useCallback(
(ev) => {
ev.persist();
eventRef.current = ev;
if (timerRef.current) {
clearTimeout(timerRef.current);
onClick(eventRef.current);
timerRef.current = false;
eventRef.current = {};
}
},
[onClick]
);
return useMemo(
() => ({
onMouseDown: start,
onMouseUp: stop,
onMouseLeave: stop,
onTouchStart: start,
onTouchEnd: stop,
}),
[start, stop]
);
}
It also provides both onLongPress and onClick and passes on the event object received.
Usage is mostly as described earlier, except arguments are now passed in an object, all are optional:
const longPressProps = useLongPress({
onClick: (ev) => console.log('on click', ev.button, ev.shiftKey),
onLongPress: (ev) => console.log('on long press', ev.button, ev.shiftKey),
});
// and later:
return (<button {...longPressProps}>click me</button>);
Just wanted to point out that hooks aren't a great solution here since you can't use them in a call back.
for example, if you wanted to add long press to a number of elements:
items.map(item => <button {...useLongPress(() => handle(item))}>{item}</button>)
gets you:
... React Hooks must be called in a React function component or a custom React Hook function
you could however use vanilla JS:
export default function longPressEvents(callback, ms = 500) {
let timeout = null
const start = () => timeout = setTimeout(callback, ms)
const stop = () => timeout && window.clearTimeout(timeout)
return callback ? {
onTouchStart: start,
onTouchMove: stop,
onTouchEnd: stop,
} : {}
}
then:
items.map(item => <button { ...longPressEvents(() => handle(item)) }>{item}</button>)
demo: https://codesandbox.io/s/long-press-hook-like-oru24?file=/src/App.js
just be aware that longPressEvents will run every render. Probably not a big deal, but something to keep in mind.