-
-
Notifications
You must be signed in to change notification settings - Fork 225
Expand file tree
/
Copy pathHoldToConfirmButton.jsx
More file actions
88 lines (80 loc) · 2.48 KB
/
Copy pathHoldToConfirmButton.jsx
File metadata and controls
88 lines (80 loc) · 2.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
export function HoldToConfirmButton({
onConfirm,
holdDurationMs = 2000,
children,
className = '',
disabled = false,
fillClass = 'bg-error/20',
...props
}) {
const [isHolding, setIsHolding] = useState(false);
const timerRef = useRef(null);
const startHold = useCallback((e) => {
// Only respond to primary button (left click) or touch
if (e.button !== undefined && e.button !== 0) return;
if (disabled) return;
setIsHolding(true);
// Optional: add a slight haptic feedback on mobile if supported
if (typeof globalThis.navigator?.vibrate === 'function') {
globalThis.navigator.vibrate(50);
}
}, [disabled]);
const cancelHold = useCallback(() => {
setIsHolding(false);
}, []);
useEffect(() => {
if (isHolding) {
timerRef.current = setTimeout(() => {
setIsHolding(false);
if (typeof globalThis.navigator?.vibrate === 'function') {
globalThis.navigator.vibrate([50, 50, 50]); // Success vibration
}
if (onConfirm) onConfirm();
}, holdDurationMs);
} else if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [isHolding, holdDurationMs, onConfirm]);
// Context menu prevention to stop touch-and-hold from bringing up OS menus
const onContextMenu = useCallback((e) => {
if (isHolding || disabled) {
e.preventDefault();
return false;
}
}, [isHolding, disabled]);
return (
<button
{...props}
className={`relative overflow-hidden ${className} ${disabled ? 'opacity-50 cursor-not-allowed' : 'select-none'}`}
onPointerDown={startHold}
onPointerUp={cancelHold}
onPointerLeave={cancelHold}
onPointerCancel={cancelHold}
onContextMenu={onContextMenu}
disabled={disabled}
style={{
WebkitUserSelect: 'none',
userSelect: 'none',
WebkitTouchCallout: 'none',
touchAction: 'none'
}}
>
<div
className={`absolute inset-0 origin-left pointer-events-none rounded-[inherit] ${fillClass}`}
style={{
transform: isHolding ? 'scaleX(1)' : 'scaleX(0)',
transition: isHolding ? `transform ${holdDurationMs}ms linear` : 'transform 150ms ease-out',
willChange: 'transform'
}}
/>
{children}
</button>
);
}