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'
|
return focusedOption?.type === 'input'
|
||||||
}, [validatedFocusedValue, options])
|
}, [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),
|
// Call onFocus with the validated value (what's actually displayed),
|
||||||
// not the internal state value which may be stale if options changed.
|
// not the internal state value which may be stale if options changed.
|
||||||
// Use ref to avoid re-running when callback reference changes.
|
// Use ref to avoid re-running when callback reference changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (validatedFocusedValue !== undefined) {
|
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])
|
}, [validatedFocusedValue])
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user