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