问题
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 fire event to delete that address followed by a confirm box.
回答1:
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>
);
}
}
回答2:
With hooks in react 16.8 you could rewrite class with functions and hooks.
import { useState, useEffect } from 'react';
export default function useLongPress(callback = () => {}, ms = 300) {
const [startLongPress, setStartLongPress] = useState(false);
useEffect(() => {
let timerId;
if (startLongPress) {
timerId = setTimeout(callback, ms);
} else {
clearTimeout(timerId);
}
return () => {
clearTimeout(timerId);
};
}, [callback, ms, startLongPress]);
return {
onMouseDown: () => setStartLongPress(true),
onMouseUp: () => setStartLongPress(false),
onMouseLeave: () => setStartLongPress(false),
onTouchStart: () => setStartLongPress(true),
onTouchEnd: () => setStartLongPress(false),
};
}
import useLongPress from './useLongPress';
function MyComponent (props) {
const backspaceLongPress = useLongPress(props.longPressBackspaceCallback, 500);
return (
<Page>
<Button {...backspaceLongPress}>
Click me
</Button>
</Page>
);
};
回答3:
Nice hook! But I would like make a small improvement. Using useCallback
to wrap event handlers. This ensures these will not changed on every render.
import { useState, useEffect, useCallback } from 'react';
export default function useLongPress(callback = () => {}, ms = 300) {
const [startLongPress, setStartLongPress] = useState(false);
useEffect(() => {
let timerId;
if (startLongPress) {
timerId = setTimeout(callback, ms);
} else {
clearTimeout(timerId);
}
return () => {
clearTimeout(timerId);
};
}, [callback, ms, startLongPress]);
const start = useCallback(() => {
setStartLongPress(true);
}, []);
const stop = useCallback(() => {
setStartLongPress(false);
}, []);
return {
onMouseDown: start,
onMouseUp: stop,
onMouseLeave: stop,
onTouchStart: start,
onTouchEnd: stop,
};
}
回答4:
Here's a component that provides onClick and onHold events - adapt as needed...
CodeSandbox: https://codesandbox.io/s/hold-press-event-r8q9w
Usage:
import React from 'react'
import Holdable from './holdable'
function App() {
function onClick(evt) {
alert('click ' + evt.currentTarget.id)
}
function onHold(evt) {
alert('hold ' + evt.currentTarget.id)
}
const ids = 'Label1,Label2,Label3'.split(',')
return (
<div className="App">
{ids.map(id => (
<Holdable
onClick={onClick}
onHold={onHold}
id={id}
key={id}
>
{id}
</Holdable>
))}
</div>
)
}
holdable.jsx:
import React from 'react'
const holdTime = 500 // ms
const holdDistance = 3**2 // pixels squared
export default function Holdable({id, onClick, onHold, children}) {
const [timer, setTimer] = React.useState(null)
const [pos, setPos] = React.useState([0,0])
function onPointerDown(evt) {
setPos([evt.clientX, evt.clientY]) // save position for later
const event = { ...evt } // convert synthetic event to real object
const timeoutId = window.setTimeout(timesup.bind(null, event), holdTime)
setTimer(timeoutId)
}
function onPointerUp(evt) {
if (timer) {
window.clearTimeout(timer)
setTimer(null)
onClick(evt)
}
}
function onPointerMove(evt) {
// cancel hold operation if moved too much
if (timer) {
const d = (evt.clientX - pos[0])**2 + (evt.clientY - pos[1])**2
if (d > holdDistance) {
setTimer(null)
window.clearTimeout(timer)
}
}
}
function timesup(evt) {
setTimer(null)
onHold(evt)
}
return (
<div
onPointerDown={onPointerDown}
onPointerUp={onPointerUp}
onPointerMove={onPointerMove}
id={id}
>
{children}
</div>
)
}
Note: this doesn't work with Safari yet - pointer events are coming in v13 though - https://caniuse.com/#feat=pointer
回答5:
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>);
回答6:
Brian's solution allows you to pass params to the children which I think is not doable with the Hook. Still, if I may suggest a bit cleaner solution for most common case where you want to add onHold behavior to a single component and you also want to be able to change the onHold timeout.
Material-UI example with Chip component:
'use strict';
const {
Chip
} = MaterialUI
function ChipHoldable({
onClick = () => {},
onHold = () => {},
hold = 500,
...props
}) {
const [timer, setTimer] = React.useState(null);
function onPointerDown(evt) {
const event = { ...evt
}; // convert synthetic event to real object
const timeoutId = window.setTimeout(timesup.bind(null, event), hold);
setTimer(timeoutId);
}
function onPointerUp(evt) {
if (timer) {
window.clearTimeout(timer);
setTimer(null);
onClick(evt);
}
}
const onContextMenu = e => e.preventDefault();
const preventDefault = e => e.preventDefault(); // so that ripple effect would be triggered
function timesup(evt) {
setTimer(null);
onHold(evt);
}
return React.createElement(Chip, {
onPointerUp,
onPointerDown,
onContextMenu,
onClick: preventDefault,
...props
});
}
const App = () => <div> {[1,2,3,4].map(i => < ChipHoldable style={{margin:"10px"}}label = {`chip${i}`}
onClick = {
() => console.log(`chip ${i} clicked`)
}
onHold = {
() => console.log(`chip ${i} long pressed`)
}
/>)}
</div>
ReactDOM.render( <App/>, document.querySelector('#root'));
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js"></script>
</body>
</html>
回答7:
An adaptation of David's solution: a React hook for when you want to repeatedly fire the event. It uses setInterval
instead.
export function useHoldPress(callback = () => {}, ms = 300) {
const [startHoldPress, setStartHoldPress] = useState(false);
useEffect(() => {
let timerId;
if (startHoldPress) {
timerId = setInterval(callback, ms);
} else {
clearTimeout(timerId);
}
return () => {
clearTimeout(timerId);
};
}, [startHoldPress]);
return {
onMouseDown: () => setStartHoldPress(true),
onMouseUp: () => setStartHoldPress(false),
onMouseLeave: () => setStartHoldPress(false),
onTouchStart: () => setStartHoldPress(true),
onTouchEnd: () => setStartHoldPress(false)
};
}
来源:https://stackoverflow.com/questions/48048957/react-long-press-event