fix(chat): release keyboard fallback on close

This commit is contained in:
2026-06-24 15:48:29 +08:00
parent a229dcf7d1
commit 890eedf7d9
+81 -9
View File
@@ -8,7 +8,8 @@
* 1. 持续写入 --app-viewport-height,聊天页高度不再只依赖 100dvh。
* 2. 优先使用 visualViewport 计算键盘高度。
* 3. 输入框聚焦后测量目标元素是否越过可见底部,计算额外抬升。
* 4. 小米系 Android + Facebook IAB 若 viewport 未可靠变化,启用保守 fallback 抬升
* 4. 小米系 Android + Facebook IAB 若 viewport 未可靠变化,启用保守 fallback 抬升
* 键盘收起后若焦点仍停留在 textarea,也要主动释放 fallback。
*/
import { type RefObject, useEffect, useRef } from "react";
@@ -21,6 +22,7 @@ const FOCUS_GAP_PX = 14;
const FALLBACK_KEYBOARD_RATIO = 0.42;
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 RECHECK_EVENT = "cozsweet:keyboard-recheck";
@@ -36,9 +38,13 @@ export function useKeyboardHeight({
active = false,
}: UseKeyboardHeightOptions = {}): void {
const activeRef = useRef(active);
const fallbackAllowedRef = useRef(false);
const focusedAtRef = useRef(0);
useEffect(() => {
activeRef.current = active;
fallbackAllowedRef.current = active;
focusedAtRef.current = active ? performance.now() : 0;
window.dispatchEvent(new Event(RECHECK_EVENT));
}, [active]);
@@ -50,14 +56,27 @@ export function useKeyboardHeight({
let lastKeyboardHeight = -1;
let lastViewportHeight = -1;
let lastInputLift = -1;
let lastMayBeKeyboardClose = false;
const apply = () => {
rafId = null;
const metrics = readViewportMetrics();
if (
activeRef.current &&
fallbackAllowedRef.current &&
lastMayBeKeyboardClose &&
shouldReleaseFallback(metrics, focusedAtRef.current)
) {
fallbackAllowedRef.current = false;
}
lastMayBeKeyboardClose = false;
const keyboardHeight = Math.round(metrics.keyboardHeight);
const viewportHeight = Math.round(metrics.visibleHeight);
const fallbackLift = computeFallbackLift(metrics);
const fallbackLift = fallbackAllowedRef.current
? computeFallbackLift(metrics)
: 0;
const inputLift = Math.round(
activeRef.current
? Math.max(
@@ -95,7 +114,9 @@ export function useKeyboardHeight({
viewportHeight,
inputLift,
fallbackLift,
fallbackAllowed: fallbackAllowedRef.current,
visualViewport: metrics.hasVisualViewport,
virtualKeyboard: metrics.hasVirtualKeyboard,
});
}
};
@@ -105,7 +126,10 @@ export function useKeyboardHeight({
rafId = window.requestAnimationFrame(apply);
};
const scheduleRechecks = () => {
const scheduleRechecks = (event?: Event) => {
if (event && isPossibleKeyboardCloseEvent(event)) {
lastMayBeKeyboardClose = true;
}
timeoutIds.forEach((id) => window.clearTimeout(id));
timeoutIds = RECHECK_DELAYS_MS.map((delay) =>
window.setTimeout(schedule, delay),
@@ -117,6 +141,8 @@ export function useKeyboardHeight({
vv.addEventListener("resize", scheduleRechecks);
vv.addEventListener("scroll", scheduleRechecks);
}
const virtualKeyboard = getVirtualKeyboard();
virtualKeyboard?.addEventListener("geometrychange", scheduleRechecks);
window.addEventListener("resize", scheduleRechecks);
window.addEventListener("orientationchange", scheduleRechecks);
window.addEventListener("focusin", scheduleRechecks);
@@ -132,6 +158,7 @@ export function useKeyboardHeight({
vv.removeEventListener("resize", scheduleRechecks);
vv.removeEventListener("scroll", scheduleRechecks);
}
virtualKeyboard?.removeEventListener("geometrychange", scheduleRechecks);
window.removeEventListener("resize", scheduleRechecks);
window.removeEventListener("orientationchange", scheduleRechecks);
window.removeEventListener("focusin", scheduleRechecks);
@@ -144,6 +171,7 @@ export function useKeyboardHeight({
interface ViewportMetrics {
hasVisualViewport: boolean;
hasVirtualKeyboard: boolean;
layoutHeight: number;
visibleHeight: number;
visibleBottom: number;
@@ -153,24 +181,36 @@ interface ViewportMetrics {
function readViewportMetrics(): ViewportMetrics {
const layoutHeight = window.innerHeight;
const vv = window.visualViewport;
const virtualKeyboard = getVirtualKeyboard();
const virtualKeyboardRect = virtualKeyboard?.boundingRect ?? null;
const virtualKeyboardHeight = virtualKeyboardRect?.height ?? 0;
if (!vv) {
const visibleBottom =
virtualKeyboardHeight > 0 ? layoutHeight - virtualKeyboardHeight : layoutHeight;
return {
hasVisualViewport: false,
hasVirtualKeyboard: virtualKeyboard != null,
layoutHeight,
visibleHeight: layoutHeight,
visibleBottom: layoutHeight,
keyboardHeight: 0,
visibleHeight: visibleBottom,
visibleBottom,
keyboardHeight: virtualKeyboardHeight,
};
}
const visibleHeight = vv.height;
const visibleBottom = vv.offsetTop + vv.height;
const visualBottom = vv.offsetTop + vv.height;
const keyboardTop =
virtualKeyboardHeight > 0
? Math.min(virtualKeyboardRect?.y ?? layoutHeight, layoutHeight - virtualKeyboardHeight)
: layoutHeight;
const visibleBottom = Math.min(visualBottom, keyboardTop);
const visibleHeight = Math.min(vv.height, visibleBottom);
return {
hasVisualViewport: true,
hasVirtualKeyboard: virtualKeyboard != null,
layoutHeight,
visibleHeight,
visibleBottom,
keyboardHeight: Math.max(0, layoutHeight - visibleBottom),
keyboardHeight: Math.max(0, layoutHeight - visibleBottom, virtualKeyboardHeight),
};
}
@@ -185,6 +225,7 @@ function computeTargetOverlap(
function computeFallbackLift(metrics: ViewportMetrics): number {
if (!shouldUseInAppKeyboardFallback()) return 0;
if (metrics.hasVirtualKeyboard && metrics.keyboardHeight <= 0) return 0;
const viewportAlreadyShrank = metrics.visibleHeight < metrics.layoutHeight * 0.82;
const keyboardAlreadyDetected = metrics.keyboardHeight > 80;
@@ -197,6 +238,26 @@ function computeFallbackLift(metrics: ViewportMetrics): number {
);
}
function shouldReleaseFallback(
metrics: ViewportMetrics,
focusedAt: number,
): boolean {
if (focusedAt <= 0) return false;
if (performance.now() - focusedAt < FALLBACK_CLOSE_GRACE_MS) return false;
return !hasDetectedKeyboard(metrics);
}
function hasDetectedKeyboard(metrics: ViewportMetrics): boolean {
return (
metrics.keyboardHeight > 80 ||
metrics.visibleHeight < metrics.layoutHeight * 0.82
);
}
function isPossibleKeyboardCloseEvent(event: Event): boolean {
return event.type === "resize" || event.type === "geometrychange";
}
function shouldUseInAppKeyboardFallback(): boolean {
if (typeof navigator === "undefined") return false;
const ua = navigator.userAgent;
@@ -216,3 +277,14 @@ function resetVars(): void {
document.documentElement.style.setProperty(VIEWPORT_VAR, "100dvh");
document.documentElement.style.setProperty(INPUT_LIFT_VAR, "0px");
}
interface VirtualKeyboardLike extends EventTarget {
readonly boundingRect: DOMRectReadOnly;
}
function getVirtualKeyboard(): VirtualKeyboardLike | null {
const nav = navigator as Navigator & {
virtualKeyboard?: VirtualKeyboardLike;
};
return nav.virtualKeyboard ?? null;
}