forked from ultraworkers/claw-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseDebouncedDigitInput.ts
More file actions
82 lines (75 loc) · 2.66 KB
/
Copy pathuseDebouncedDigitInput.ts
File metadata and controls
82 lines (75 loc) · 2.66 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
import { useEffect, useRef } from 'react'
import { normalizeFullWidthDigits } from '../../utils/stringUtils.js'
// Delay before accepting a digit as a response, to prevent accidental
// submissions when users start messages with numbers (e.g., numbered lists).
// Short enough to feel instant for intentional presses, long enough to
// cancel when the user types more characters.
const DEFAULT_DEBOUNCE_MS = 400
/**
* Detects when the user types a single valid digit into the prompt input,
* debounces to avoid accidental submissions (e.g., "1. First item"),
* trims the digit from the input, and fires a callback.
*
* Used by survey components that accept numeric responses typed directly
* into the main prompt input.
*/
export function useDebouncedDigitInput<T extends string = string>({
inputValue,
setInputValue,
isValidDigit,
onDigit,
enabled = true,
once = false,
debounceMs = DEFAULT_DEBOUNCE_MS,
}: {
inputValue: string
setInputValue: (value: string) => void
isValidDigit: (char: string) => char is T
onDigit: (digit: T) => void
enabled?: boolean
once?: boolean
debounceMs?: number
}): void {
const initialInputValue = useRef(inputValue)
const hasTriggeredRef = useRef(false)
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
// Latest-ref pattern so callers can pass inline callbacks without causing
// the effect to re-run (which would reset the debounce timer every render).
const callbacksRef = useRef({ setInputValue, isValidDigit, onDigit })
callbacksRef.current = { setInputValue, isValidDigit, onDigit }
useEffect(() => {
if (!enabled || (once && hasTriggeredRef.current)) {
return
}
if (debounceRef.current !== null) {
clearTimeout(debounceRef.current)
debounceRef.current = null
}
if (inputValue !== initialInputValue.current) {
const lastChar = normalizeFullWidthDigits(inputValue.slice(-1))
if (callbacksRef.current.isValidDigit(lastChar)) {
const trimmed = inputValue.slice(0, -1)
debounceRef.current = setTimeout(
(debounceRef, hasTriggeredRef, callbacksRef, trimmed, lastChar) => {
debounceRef.current = null
hasTriggeredRef.current = true
callbacksRef.current.setInputValue(trimmed)
callbacksRef.current.onDigit(lastChar)
},
debounceMs,
debounceRef,
hasTriggeredRef,
callbacksRef,
trimmed,
lastChar,
)
}
}
return () => {
if (debounceRef.current !== null) {
clearTimeout(debounceRef.current)
debounceRef.current = null
}
}
}, [inputValue, enabled, once, debounceMs])
}