mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-27 15:13:37 +08:00
fix: throttle onFocus callback during rapid arrow key navigation
Holding down the arrow key in /resume session selection causes key-repeat events to flood the render chain: dispatch → re-render → useEffect → onFocus → parent setState → re-render. With >4 sessions, Ink cannot flush output fast enough, producing visible option jitter and a long recovery time after key release. Add a leading+trailing edge throttle (40ms) on the onFocus callback in use-select-navigation.ts. The Select component's internal navigation state updates at full speed; only the expensive parent notification is throttled. The trailing edge schedules a setTimeout to deliver the final focus position after rapid scrolling stops, preventing state staleness. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5f46c5bb61
commit
3222e0d7cb
@ -608,12 +608,51 @@ export function useSelectNavigation<T>({
|
||||
return focusedOption?.type === 'input'
|
||||
}, [validatedFocusedValue, options])
|
||||
|
||||
// Throttle onFocus calls during rapid navigation (e.g., held arrow key)
|
||||
// to prevent parent re-renders from flooding Ink's render pipeline.
|
||||
// Leading edge: first event fires immediately. Trailing edge: after the
|
||||
// burst stops, a scheduled callback fires with the final value so the
|
||||
// parent always catches up.
|
||||
const lastFocusTimeRef = useRef(0)
|
||||
const trailingFocusRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const FOCUS_THROTTLE_MS = 40
|
||||
|
||||
// Cleanup trailing timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (trailingFocusRef.current !== null) {
|
||||
clearTimeout(trailingFocusRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Call onFocus with the validated value (what's actually displayed),
|
||||
// not the internal state value which may be stale if options changed.
|
||||
// Use ref to avoid re-running when callback reference changes.
|
||||
useEffect(() => {
|
||||
if (validatedFocusedValue !== undefined) {
|
||||
onFocusRef.current?.(validatedFocusedValue)
|
||||
const now = Date.now()
|
||||
if (now - lastFocusTimeRef.current >= FOCUS_THROTTLE_MS) {
|
||||
// Leading edge: fire immediately
|
||||
if (trailingFocusRef.current !== null) {
|
||||
clearTimeout(trailingFocusRef.current)
|
||||
trailingFocusRef.current = null
|
||||
}
|
||||
lastFocusTimeRef.current = now
|
||||
onFocusRef.current?.(validatedFocusedValue)
|
||||
} else {
|
||||
// Trailing edge: schedule callback with the latest value so the
|
||||
// parent eventually catches up after rapid navigation stops
|
||||
if (trailingFocusRef.current !== null) {
|
||||
clearTimeout(trailingFocusRef.current)
|
||||
}
|
||||
const remaining = FOCUS_THROTTLE_MS - (now - lastFocusTimeRef.current)
|
||||
trailingFocusRef.current = setTimeout(() => {
|
||||
trailingFocusRef.current = null
|
||||
lastFocusTimeRef.current = Date.now()
|
||||
onFocusRef.current?.(validatedFocusedValue)
|
||||
}, remaining)
|
||||
}
|
||||
}
|
||||
}, [validatedFocusedValue])
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user