From 09cbd54c79bb100878d3c31db61e83556fdcaed9 Mon Sep 17 00:00:00 2001 From: chenhang Date: Fri, 26 Jun 2026 12:01:24 +0800 Subject: [PATCH] fix(chat): release input focus when keyboard closes --- src/app/chat/components/chat-input-bar.tsx | 1 + src/hooks/use-keyboard-height.ts | 55 +++++++++++++++------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/app/chat/components/chat-input-bar.tsx b/src/app/chat/components/chat-input-bar.tsx index c7beff49..c4752882 100644 --- a/src/app/chat/components/chat-input-bar.tsx +++ b/src/app/chat/components/chat-input-bar.tsx @@ -32,6 +32,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) { useKeyboardHeight({ targetRef: textareaRef, active: isFocused, + onKeyboardDismiss: () => setIsFocused(false), }); const handleInputChange = (value: string) => { diff --git a/src/hooks/use-keyboard-height.ts b/src/hooks/use-keyboard-height.ts index 4f541d86..a60b61c8 100644 --- a/src/hooks/use-keyboard-height.ts +++ b/src/hooks/use-keyboard-height.ts @@ -24,7 +24,7 @@ const FALLBACK_KEYBOARD_MIN = 260; const FALLBACK_KEYBOARD_MAX = 380; const FALLBACK_CLOSE_GRACE_MS = 900; const RECHECK_DELAYS_MS = [0, 50, 120, 240, 420, 700]; -const FALLBACK_RELEASE_EVENT_TYPES = new Set([ +const KEYBOARD_RELEASE_EVENT_TYPES = new Set([ "resize", "scroll", "geometrychange", @@ -39,16 +39,23 @@ const log = new Logger("HooksUseKeyboardHeight"); export interface UseKeyboardHeightOptions { targetRef?: RefObject; active?: boolean; + onKeyboardDismiss?: () => void; } export function useKeyboardHeight({ targetRef, active = false, + onKeyboardDismiss, }: UseKeyboardHeightOptions = {}): void { const activeRef = useRef(active); + const onKeyboardDismissRef = useRef(onKeyboardDismiss); const fallbackAllowedRef = useRef(false); const focusedAtRef = useRef(0); + useEffect(() => { + onKeyboardDismissRef.current = onKeyboardDismiss; + }, [onKeyboardDismiss]); + useEffect(() => { activeRef.current = active; fallbackAllowedRef.current = active; @@ -82,20 +89,14 @@ export function useKeyboardHeight({ rafId = null; const metrics = readViewportMetrics(); - const activeNow = isKeyboardTargetActive(targetRef?.current, activeRef.current); + let activeNow = isKeyboardTargetActive(targetRef?.current, activeRef.current); if ( activeNow && - fallbackAllowedRef.current && lastMayBeKeyboardClose && - shouldReleaseFallback(metrics, focusedAtRef.current) + shouldReleaseKeyboardState(metrics, focusedAtRef.current) ) { - fallbackAllowedRef.current = false; - blurKeyboardTarget(targetRef?.current); - log.debug("[keyboard] fallback released", { - reason: "keyboard-close-detected", - environment, - metrics: formatMetrics(metrics), - }); + releaseKeyboardState("keyboard-close-detected", metrics); + activeNow = false; } lastMayBeKeyboardClose = false; @@ -156,7 +157,13 @@ export function useKeyboardHeight({ }; const scheduleRechecks = (event?: Event) => { - if (event && isPossibleKeyboardCloseEvent(event, fallbackWasApplied)) { + if ( + event && + isPossibleKeyboardCloseEvent( + event, + fallbackWasApplied || lastInputLift > 0, + ) + ) { lastMayBeKeyboardClose = true; } timeoutIds.forEach((id) => window.clearTimeout(id)); @@ -165,6 +172,22 @@ export function useKeyboardHeight({ ); }; + const releaseKeyboardState = ( + reason: string, + metrics: ViewportMetrics, + ): void => { + activeRef.current = false; + fallbackAllowedRef.current = false; + focusedAtRef.current = 0; + blurKeyboardTarget(targetRef?.current); + onKeyboardDismissRef.current?.(); + log.debug("[keyboard] released", { + reason, + environment, + metrics: formatMetrics(metrics), + }); + }; + const vv = window.visualViewport; if (vv) { vv.addEventListener("resize", scheduleRechecks); @@ -270,7 +293,7 @@ function computeFallbackLift(metrics: ViewportMetrics): number { ); } -function shouldReleaseFallback( +function shouldReleaseKeyboardState( metrics: ViewportMetrics, focusedAt: number, ): boolean { @@ -288,12 +311,12 @@ function hasDetectedKeyboard(metrics: ViewportMetrics): boolean { function isPossibleKeyboardCloseEvent( event: Event, - fallbackWasApplied: boolean, + hadInputLift: boolean, ): boolean { - if (!fallbackWasApplied) { + if (!hadInputLift) { return event.type === "resize" || event.type === "geometrychange"; } - return FALLBACK_RELEASE_EVENT_TYPES.has(event.type); + return KEYBOARD_RELEASE_EVENT_TYPES.has(event.type); } function isKeyboardTargetActive(