fix(chat): release input focus when keyboard closes

This commit is contained in:
2026-06-26 12:01:24 +08:00
parent 58236453ed
commit 09cbd54c79
2 changed files with 40 additions and 16 deletions
@@ -32,6 +32,7 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) {
useKeyboardHeight({ useKeyboardHeight({
targetRef: textareaRef, targetRef: textareaRef,
active: isFocused, active: isFocused,
onKeyboardDismiss: () => setIsFocused(false),
}); });
const handleInputChange = (value: string) => { const handleInputChange = (value: string) => {
+39 -16
View File
@@ -24,7 +24,7 @@ const FALLBACK_KEYBOARD_MIN = 260;
const FALLBACK_KEYBOARD_MAX = 380; const FALLBACK_KEYBOARD_MAX = 380;
const FALLBACK_CLOSE_GRACE_MS = 900; const FALLBACK_CLOSE_GRACE_MS = 900;
const RECHECK_DELAYS_MS = [0, 50, 120, 240, 420, 700]; 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", "resize",
"scroll", "scroll",
"geometrychange", "geometrychange",
@@ -39,16 +39,23 @@ const log = new Logger("HooksUseKeyboardHeight");
export interface UseKeyboardHeightOptions { export interface UseKeyboardHeightOptions {
targetRef?: RefObject<HTMLElement | null>; targetRef?: RefObject<HTMLElement | null>;
active?: boolean; active?: boolean;
onKeyboardDismiss?: () => void;
} }
export function useKeyboardHeight({ export function useKeyboardHeight({
targetRef, targetRef,
active = false, active = false,
onKeyboardDismiss,
}: UseKeyboardHeightOptions = {}): void { }: UseKeyboardHeightOptions = {}): void {
const activeRef = useRef(active); const activeRef = useRef(active);
const onKeyboardDismissRef = useRef(onKeyboardDismiss);
const fallbackAllowedRef = useRef(false); const fallbackAllowedRef = useRef(false);
const focusedAtRef = useRef(0); const focusedAtRef = useRef(0);
useEffect(() => {
onKeyboardDismissRef.current = onKeyboardDismiss;
}, [onKeyboardDismiss]);
useEffect(() => { useEffect(() => {
activeRef.current = active; activeRef.current = active;
fallbackAllowedRef.current = active; fallbackAllowedRef.current = active;
@@ -82,20 +89,14 @@ export function useKeyboardHeight({
rafId = null; rafId = null;
const metrics = readViewportMetrics(); const metrics = readViewportMetrics();
const activeNow = isKeyboardTargetActive(targetRef?.current, activeRef.current); let activeNow = isKeyboardTargetActive(targetRef?.current, activeRef.current);
if ( if (
activeNow && activeNow &&
fallbackAllowedRef.current &&
lastMayBeKeyboardClose && lastMayBeKeyboardClose &&
shouldReleaseFallback(metrics, focusedAtRef.current) shouldReleaseKeyboardState(metrics, focusedAtRef.current)
) { ) {
fallbackAllowedRef.current = false; releaseKeyboardState("keyboard-close-detected", metrics);
blurKeyboardTarget(targetRef?.current); activeNow = false;
log.debug("[keyboard] fallback released", {
reason: "keyboard-close-detected",
environment,
metrics: formatMetrics(metrics),
});
} }
lastMayBeKeyboardClose = false; lastMayBeKeyboardClose = false;
@@ -156,7 +157,13 @@ export function useKeyboardHeight({
}; };
const scheduleRechecks = (event?: Event) => { const scheduleRechecks = (event?: Event) => {
if (event && isPossibleKeyboardCloseEvent(event, fallbackWasApplied)) { if (
event &&
isPossibleKeyboardCloseEvent(
event,
fallbackWasApplied || lastInputLift > 0,
)
) {
lastMayBeKeyboardClose = true; lastMayBeKeyboardClose = true;
} }
timeoutIds.forEach((id) => window.clearTimeout(id)); 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; const vv = window.visualViewport;
if (vv) { if (vv) {
vv.addEventListener("resize", scheduleRechecks); vv.addEventListener("resize", scheduleRechecks);
@@ -270,7 +293,7 @@ function computeFallbackLift(metrics: ViewportMetrics): number {
); );
} }
function shouldReleaseFallback( function shouldReleaseKeyboardState(
metrics: ViewportMetrics, metrics: ViewportMetrics,
focusedAt: number, focusedAt: number,
): boolean { ): boolean {
@@ -288,12 +311,12 @@ function hasDetectedKeyboard(metrics: ViewportMetrics): boolean {
function isPossibleKeyboardCloseEvent( function isPossibleKeyboardCloseEvent(
event: Event, event: Event,
fallbackWasApplied: boolean, hadInputLift: boolean,
): boolean { ): boolean {
if (!fallbackWasApplied) { if (!hadInputLift) {
return event.type === "resize" || event.type === "geometrychange"; 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( function isKeyboardTargetActive(