81 lines
2.0 KiB
TypeScript
81 lines
2.0 KiB
TypeScript
import { useCallback, useRef, useState } from "react"
|
|
|
|
const DEFAULT_DELAY_MS = 500
|
|
const ACK_MS = 280
|
|
|
|
/** Applied briefly when a long-press action fires (touch feedback). */
|
|
export const LONG_PRESS_ACK_CLASS = "long-press-ack"
|
|
|
|
export function useLongPress(
|
|
onLongPress: () => void,
|
|
options?: { delay?: number; disabled?: boolean; ack?: boolean }
|
|
) {
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
const ackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
const firedRef = useRef(false)
|
|
const delay = options?.delay ?? DEFAULT_DELAY_MS
|
|
const disabled = options?.disabled ?? false
|
|
const withAck = options?.ack ?? true
|
|
const [ack, setAck] = useState(false)
|
|
|
|
const clearAck = useCallback(() => {
|
|
if (ackTimerRef.current) {
|
|
clearTimeout(ackTimerRef.current)
|
|
ackTimerRef.current = null
|
|
}
|
|
setAck(false)
|
|
}, [])
|
|
|
|
const clear = useCallback(() => {
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current)
|
|
timerRef.current = null
|
|
}
|
|
}, [])
|
|
|
|
const pulseAck = useCallback(() => {
|
|
if (!withAck) return
|
|
clearAck()
|
|
setAck(true)
|
|
ackTimerRef.current = setTimeout(() => {
|
|
setAck(false)
|
|
ackTimerRef.current = null
|
|
}, ACK_MS)
|
|
}, [clearAck, withAck])
|
|
|
|
const onPointerDown = useCallback(
|
|
(e: React.PointerEvent) => {
|
|
if (disabled) return
|
|
if (e.pointerType === "mouse" && e.button !== 0) return
|
|
firedRef.current = false
|
|
clear()
|
|
timerRef.current = setTimeout(() => {
|
|
firedRef.current = true
|
|
pulseAck()
|
|
onLongPress()
|
|
}, delay)
|
|
},
|
|
[clear, delay, disabled, onLongPress, pulseAck]
|
|
)
|
|
|
|
const onClickCapture = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
if (!firedRef.current) return
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
firedRef.current = false
|
|
},
|
|
[]
|
|
)
|
|
|
|
return {
|
|
onPointerDown,
|
|
onPointerUp: clear,
|
|
onPointerLeave: clear,
|
|
onPointerCancel: clear,
|
|
onClickCapture,
|
|
ackActive: ack,
|
|
ackClassName: ack ? LONG_PRESS_ACK_CLASS : undefined,
|
|
}
|
|
}
|