fix(chat): release keyboard fallback on close
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user