fix(chat): simplify Xiaomi Facebook keyboard lift
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
/* ChatInputBar 输入栏容器样式(与 Dart chat_input_bar.dart 对齐) */
|
||||
|
||||
/* 键盘适配:
|
||||
* --app-viewport-height 会让聊天容器优先贴合 visualViewport。
|
||||
* --chat-input-lift 是 Android 应用内浏览器的兜底抬升值。
|
||||
* --chat-input-lift 是小米 Android + Facebook IAB 的专用抬升值。
|
||||
* env(safe-area-inset-bottom) 处理小米全面屏手势条 / iPhone home indicator。
|
||||
* 正常浏览器中 --chat-input-lift 为 0,因此无视觉副作用。 */
|
||||
.bar {
|
||||
|
||||
@@ -22,7 +22,6 @@ function makeEnvironment(
|
||||
isXiaomiFamilyDevice: true,
|
||||
isFacebookInAppBrowser: true,
|
||||
hasVisualViewport: true,
|
||||
hasVirtualKeyboard: true,
|
||||
usesXiaomiFacebookFallback: true,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
export const KEYBOARD_VAR = "--keyboard-height";
|
||||
export const VIEWPORT_VAR = "--app-viewport-height";
|
||||
export const INPUT_LIFT_VAR = "--chat-input-lift";
|
||||
|
||||
export const FOCUS_GAP_PX = 14;
|
||||
export const FALLBACK_KEYBOARD_RATIO = 0.42;
|
||||
export const FALLBACK_KEYBOARD_MIN = 260;
|
||||
export const FALLBACK_KEYBOARD_MAX = 380;
|
||||
export const FALLBACK_CLOSE_GRACE_MS = 900;
|
||||
export const RECHECK_DELAYS_MS = [0, 50, 120, 240, 420, 700];
|
||||
export const KEYBOARD_RELEASE_EVENT_TYPES = new Set([
|
||||
"resize",
|
||||
"scroll",
|
||||
"geometrychange",
|
||||
"focusout",
|
||||
"blur",
|
||||
"visibilitychange",
|
||||
]);
|
||||
export const RECHECK_EVENT = "cozsweet:keyboard-recheck";
|
||||
export const FALLBACK_INPUT_LIFT_PX = 320;
|
||||
export const KEYBOARD_VISIBLE_THRESHOLD_PX = 80;
|
||||
export const KEYBOARD_VISIBLE_RATIO = 0.82;
|
||||
export const KEYBOARD_RELEASE_GRACE_MS = 900;
|
||||
export const KEYBOARD_MEASURE_DELAYS_MS = [0, 100, 250, 500];
|
||||
export const KEYBOARD_POLL_INTERVAL_MS = 300;
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { INPUT_LIFT_VAR, KEYBOARD_VAR, VIEWPORT_VAR } from "./constants";
|
||||
|
||||
export function setKeyboardHeightVar(height: number): void {
|
||||
document.documentElement.style.setProperty(KEYBOARD_VAR, `${height}px`);
|
||||
}
|
||||
|
||||
export function setViewportHeightVar(height: number): void {
|
||||
document.documentElement.style.setProperty(VIEWPORT_VAR, `${height}px`);
|
||||
}
|
||||
import { INPUT_LIFT_VAR } from "./constants";
|
||||
|
||||
export function setInputLiftVar(lift: number): void {
|
||||
document.documentElement.style.setProperty(INPUT_LIFT_VAR, `${lift}px`);
|
||||
}
|
||||
|
||||
export function resetKeyboardVars(): void {
|
||||
document.documentElement.style.setProperty(KEYBOARD_VAR, "0px");
|
||||
document.documentElement.style.setProperty(VIEWPORT_VAR, "100dvh");
|
||||
export function resetInputLiftVar(): void {
|
||||
document.documentElement.style.setProperty(INPUT_LIFT_VAR, "0px");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BrowserDetector, PlatformDetector } from "@/utils";
|
||||
|
||||
import type { KeyboardAdaptEnvironment } from "./types";
|
||||
import { getVirtualKeyboard } from "./virtual-keyboard";
|
||||
|
||||
export function getKeyboardAdaptEnvironment(
|
||||
ua: string | null = null,
|
||||
@@ -28,7 +27,6 @@ export function getKeyboardAdaptEnvironment(
|
||||
isFacebookInAppBrowser,
|
||||
hasVisualViewport:
|
||||
typeof window !== "undefined" && window.visualViewport != null,
|
||||
hasVirtualKeyboard: getVirtualKeyboard() != null,
|
||||
usesXiaomiFacebookFallback:
|
||||
platform.isAndroid && isXiaomiFamilyDevice && isFacebookInAppBrowser,
|
||||
};
|
||||
@@ -39,9 +37,3 @@ export function shouldEnableKeyboardAdaptation(
|
||||
): boolean {
|
||||
return environment.usesXiaomiFacebookFallback;
|
||||
}
|
||||
|
||||
export function shouldUseInAppKeyboardFallback(): boolean {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
return getKeyboardAdaptEnvironment(navigator.userAgent)
|
||||
.usesXiaomiFacebookFallback;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
"use client";
|
||||
/**
|
||||
* 软键盘避让 hook。
|
||||
* 小米 Android + Facebook IAB 专用输入框避让 hook。
|
||||
*
|
||||
* 目标环境:小米系 Android 设备上的 Facebook IAB。
|
||||
* 这不是通用软键盘方案。它只解决一个已知 WebView 问题:
|
||||
* 输入框 focus 后键盘弹出,但 Facebook IAB 没有稳定调整页面 viewport,
|
||||
* 导致底部输入框被键盘遮挡。
|
||||
*
|
||||
* 策略:
|
||||
* 1. 持续写入 --app-viewport-height,聊天页高度不再只依赖 100dvh。
|
||||
* 2. 优先使用 visualViewport 计算键盘高度。
|
||||
* 3. 输入框聚焦后测量目标元素是否越过可见底部,计算额外抬升。
|
||||
* 4. 小米系 Android + Facebook IAB 若 viewport 未可靠变化,启用保守 fallback 抬升;
|
||||
* 键盘收起后若焦点仍停留在 textarea,也要主动释放 fallback。
|
||||
* - 非小米 Android Facebook IAB:完全不生效。
|
||||
* - focus 后短期多次测量输入框是否被 visibleViewport 遮挡。
|
||||
* - 若 viewport 没有可靠变化,则使用固定 fallback 抬升。
|
||||
* - 聚焦期间轻量轮询;一旦检测到 viewport 恢复,主动 blur 并清除抬升。
|
||||
*/
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
import {
|
||||
RECHECK_DELAYS_MS,
|
||||
RECHECK_EVENT,
|
||||
FALLBACK_INPUT_LIFT_PX,
|
||||
FOCUS_GAP_PX,
|
||||
KEYBOARD_MEASURE_DELAYS_MS,
|
||||
KEYBOARD_POLL_INTERVAL_MS,
|
||||
KEYBOARD_RELEASE_GRACE_MS,
|
||||
KEYBOARD_VISIBLE_RATIO,
|
||||
KEYBOARD_VISIBLE_THRESHOLD_PX,
|
||||
} from "./constants";
|
||||
import {
|
||||
resetKeyboardVars,
|
||||
resetInputLiftVar,
|
||||
setInputLiftVar,
|
||||
setKeyboardHeightVar,
|
||||
setViewportHeightVar,
|
||||
} from "./css-vars";
|
||||
import {
|
||||
getKeyboardAdaptEnvironment,
|
||||
@@ -33,59 +37,35 @@ import {
|
||||
blurKeyboardTarget,
|
||||
isKeyboardTargetActive,
|
||||
} from "./keyboard-target";
|
||||
import { addKeyboardRecheckListeners } from "./keyboard-event-listeners";
|
||||
import { isPossibleKeyboardCloseEvent } from "./release-events";
|
||||
import type { UseKeyboardHeightOptions, ViewportMetrics } from "./types";
|
||||
import {
|
||||
computeFallbackLift,
|
||||
computeTargetOverlap,
|
||||
formatMetrics,
|
||||
getFocusedDurationMs,
|
||||
getLiftSource,
|
||||
getReleaseSkipReason,
|
||||
readViewportMetrics,
|
||||
shouldReleaseKeyboardState,
|
||||
} from "./viewport-metrics";
|
||||
import type { UseKeyboardHeightOptions } from "./types";
|
||||
|
||||
const log = new Logger("HooksUseKeyboardHeight");
|
||||
|
||||
interface KeyboardSnapshot {
|
||||
layoutHeight: number;
|
||||
visibleHeight: number;
|
||||
visibleBottom: number;
|
||||
keyboardHeight: number;
|
||||
keyboardVisible: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
focusedAtRef.current = active ? performance.now() : 0;
|
||||
window.dispatchEvent(new Event(RECHECK_EVENT));
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
let rafId: number | null = null;
|
||||
let timeoutIds: number[] = [];
|
||||
let lastKeyboardHeight = -1;
|
||||
let lastViewportHeight = -1;
|
||||
let lastInputLift = -1;
|
||||
let lastMayBeKeyboardClose = false;
|
||||
let fallbackWasApplied = false;
|
||||
let lastEventType = "initial";
|
||||
const environment = getKeyboardAdaptEnvironment();
|
||||
|
||||
log.debug("[keyboard] environment", environment);
|
||||
if (!shouldEnableKeyboardAdaptation(environment)) {
|
||||
resetKeyboardVars();
|
||||
resetInputLiftVar();
|
||||
log.debug("[keyboard] disabled", {
|
||||
reason: "not-xiaomi-facebook-iab",
|
||||
environment,
|
||||
@@ -93,159 +73,187 @@ export function useKeyboardHeight({
|
||||
return;
|
||||
}
|
||||
|
||||
const apply = () => {
|
||||
rafId = null;
|
||||
let rafId: number | null = null;
|
||||
let pollId: number | null = null;
|
||||
let timeoutIds: number[] = [];
|
||||
let released = false;
|
||||
let keyboardWasDetected = false;
|
||||
let lastInputLift = -1;
|
||||
const focusedAt = active ? performance.now() : 0;
|
||||
|
||||
const metrics = readViewportMetrics();
|
||||
let activeNow = isKeyboardTargetActive(
|
||||
targetRef?.current,
|
||||
activeRef.current,
|
||||
);
|
||||
const shouldTryRelease =
|
||||
activeNow &&
|
||||
lastMayBeKeyboardClose &&
|
||||
shouldReleaseKeyboardState(metrics, focusedAtRef.current);
|
||||
log.debug("[keyboard] environment", environment);
|
||||
|
||||
if (activeNow && lastMayBeKeyboardClose && !shouldTryRelease) {
|
||||
log.debug("[keyboard] release skipped", {
|
||||
reason: getReleaseSkipReason(metrics, focusedAtRef.current),
|
||||
lastEventType,
|
||||
environment,
|
||||
active: activeNow,
|
||||
reactActive: activeRef.current,
|
||||
fallbackAllowed: fallbackAllowedRef.current,
|
||||
lastInputLift,
|
||||
fallbackWasApplied,
|
||||
focusedForMs: getFocusedDurationMs(focusedAtRef.current),
|
||||
metrics: formatMetrics(metrics),
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldTryRelease) {
|
||||
log.debug("[keyboard] release confirmed", {
|
||||
lastEventType,
|
||||
environment,
|
||||
active: activeNow,
|
||||
reactActive: activeRef.current,
|
||||
fallbackAllowed: fallbackAllowedRef.current,
|
||||
lastInputLift,
|
||||
fallbackWasApplied,
|
||||
focusedForMs: getFocusedDurationMs(focusedAtRef.current),
|
||||
metrics: formatMetrics(metrics),
|
||||
});
|
||||
releaseKeyboardState("keyboard-close-detected", metrics);
|
||||
activeNow = false;
|
||||
}
|
||||
lastMayBeKeyboardClose = false;
|
||||
|
||||
const keyboardHeight = Math.round(metrics.keyboardHeight);
|
||||
const viewportHeight = Math.round(metrics.visibleHeight);
|
||||
const fallbackLift = fallbackAllowedRef.current
|
||||
? computeFallbackLift(metrics)
|
||||
: 0;
|
||||
const inputLift = Math.round(
|
||||
activeNow
|
||||
? Math.max(
|
||||
computeTargetOverlap(targetRef?.current, metrics.visibleBottom),
|
||||
fallbackLift,
|
||||
)
|
||||
: 0,
|
||||
);
|
||||
fallbackWasApplied =
|
||||
fallbackLift > 0 &&
|
||||
inputLift >= fallbackLift &&
|
||||
fallbackAllowedRef.current;
|
||||
|
||||
if (keyboardHeight !== lastKeyboardHeight) {
|
||||
lastKeyboardHeight = keyboardHeight;
|
||||
setKeyboardHeightVar(keyboardHeight);
|
||||
}
|
||||
|
||||
if (viewportHeight !== lastViewportHeight) {
|
||||
lastViewportHeight = viewportHeight;
|
||||
setViewportHeightVar(viewportHeight);
|
||||
}
|
||||
|
||||
if (inputLift !== lastInputLift) {
|
||||
lastInputLift = inputLift;
|
||||
setInputLiftVar(inputLift);
|
||||
log.debug("[keyboard] apply", {
|
||||
environment,
|
||||
active: activeNow,
|
||||
reactActive: activeRef.current,
|
||||
inputLift,
|
||||
fallbackLift,
|
||||
fallbackAllowed: fallbackAllowedRef.current,
|
||||
source: getLiftSource(inputLift, fallbackLift),
|
||||
metrics: formatMetrics(metrics),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
if (rafId != null) return;
|
||||
rafId = window.requestAnimationFrame(apply);
|
||||
};
|
||||
|
||||
const scheduleRechecks = (event?: Event) => {
|
||||
lastEventType = event?.type ?? "manual";
|
||||
const hadInputLift = fallbackWasApplied || lastInputLift > 0;
|
||||
const possibleKeyboardClose =
|
||||
event != null && isPossibleKeyboardCloseEvent(event, hadInputLift);
|
||||
log.debug("[keyboard] schedule rechecks", {
|
||||
eventType: lastEventType,
|
||||
possibleKeyboardClose,
|
||||
hadInputLift,
|
||||
fallbackWasApplied,
|
||||
lastInputLift,
|
||||
active: activeRef.current,
|
||||
fallbackAllowed: fallbackAllowedRef.current,
|
||||
focusedForMs: getFocusedDurationMs(focusedAtRef.current),
|
||||
});
|
||||
if (possibleKeyboardClose) {
|
||||
lastMayBeKeyboardClose = true;
|
||||
log.debug("[keyboard] possible close detected", {
|
||||
eventType: lastEventType,
|
||||
hadInputLift,
|
||||
fallbackWasApplied,
|
||||
lastInputLift,
|
||||
focusedForMs: getFocusedDurationMs(focusedAtRef.current),
|
||||
});
|
||||
}
|
||||
const clearTimers = () => {
|
||||
timeoutIds.forEach((id) => window.clearTimeout(id));
|
||||
timeoutIds = RECHECK_DELAYS_MS.map((delay) =>
|
||||
window.setTimeout(schedule, delay),
|
||||
);
|
||||
timeoutIds = [];
|
||||
if (pollId != null) {
|
||||
window.clearInterval(pollId);
|
||||
pollId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const releaseKeyboardState = (
|
||||
reason: string,
|
||||
metrics: ViewportMetrics,
|
||||
): void => {
|
||||
activeRef.current = false;
|
||||
fallbackAllowedRef.current = false;
|
||||
focusedAtRef.current = 0;
|
||||
const applyLift = (lift: number, reason: string) => {
|
||||
const nextLift = Math.max(0, Math.round(lift));
|
||||
if (nextLift === lastInputLift) return;
|
||||
|
||||
lastInputLift = nextLift;
|
||||
setInputLiftVar(nextLift);
|
||||
log.debug("[keyboard] apply", {
|
||||
reason,
|
||||
inputLift: nextLift,
|
||||
fallback: nextLift === FALLBACK_INPUT_LIFT_PX,
|
||||
snapshot: formatSnapshot(readKeyboardSnapshot()),
|
||||
});
|
||||
};
|
||||
|
||||
const release = (reason: string) => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
clearTimers();
|
||||
applyLift(0, reason);
|
||||
blurKeyboardTarget(targetRef?.current);
|
||||
onKeyboardDismissRef.current?.();
|
||||
log.debug("[keyboard] released", {
|
||||
reason,
|
||||
environment,
|
||||
metrics: formatMetrics(metrics),
|
||||
snapshot: formatSnapshot(readKeyboardSnapshot()),
|
||||
});
|
||||
};
|
||||
|
||||
const removeKeyboardRecheckListeners =
|
||||
addKeyboardRecheckListeners(scheduleRechecks);
|
||||
const measure = (reason: string) => {
|
||||
rafId = null;
|
||||
|
||||
scheduleRechecks();
|
||||
const target = targetRef?.current;
|
||||
const activeNow = isKeyboardTargetActive(target, active);
|
||||
const snapshot = readKeyboardSnapshot();
|
||||
if (snapshot.keyboardVisible) keyboardWasDetected = true;
|
||||
|
||||
if (!activeNow) {
|
||||
release("target-inactive");
|
||||
return;
|
||||
}
|
||||
|
||||
const focusedFor = focusedAt > 0 ? performance.now() - focusedAt : 0;
|
||||
if (
|
||||
keyboardWasDetected &&
|
||||
!snapshot.keyboardVisible &&
|
||||
focusedFor >= KEYBOARD_RELEASE_GRACE_MS
|
||||
) {
|
||||
release("viewport-restored");
|
||||
return;
|
||||
}
|
||||
|
||||
const overlapLift = computeTargetOverlap(target, snapshot.visibleBottom);
|
||||
const fallbackLift = snapshot.keyboardVisible ? 0 : FALLBACK_INPUT_LIFT_PX;
|
||||
applyLift(Math.max(overlapLift, fallbackLift), reason);
|
||||
};
|
||||
|
||||
const scheduleMeasure = (reason: string) => {
|
||||
if (rafId != null) return;
|
||||
rafId = window.requestAnimationFrame(() => measure(reason));
|
||||
};
|
||||
|
||||
const scheduleInitialMeasures = () => {
|
||||
timeoutIds = KEYBOARD_MEASURE_DELAYS_MS.map((delay) =>
|
||||
window.setTimeout(() => scheduleMeasure(`focus-${delay}`), delay),
|
||||
);
|
||||
};
|
||||
|
||||
const handleViewportSignal = () => {
|
||||
scheduleMeasure("viewport-signal");
|
||||
};
|
||||
|
||||
const handleFocusOut = () => {
|
||||
release("focusout");
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: PointerEvent | TouchEvent) => {
|
||||
const target = targetRef?.current;
|
||||
if (!target || event.target === target) return;
|
||||
if (event.target instanceof Node && target.contains(event.target)) return;
|
||||
release("outside-interaction");
|
||||
};
|
||||
|
||||
if (!active) {
|
||||
release("react-inactive");
|
||||
return;
|
||||
}
|
||||
|
||||
const visualViewport = window.visualViewport;
|
||||
visualViewport?.addEventListener("resize", handleViewportSignal);
|
||||
visualViewport?.addEventListener("scroll", handleViewportSignal);
|
||||
window.addEventListener("resize", handleViewportSignal);
|
||||
window.addEventListener("orientationchange", handleViewportSignal);
|
||||
window.addEventListener("focusout", handleFocusOut);
|
||||
window.addEventListener("blur", handleFocusOut);
|
||||
document.addEventListener("visibilitychange", handleViewportSignal);
|
||||
document.addEventListener("pointerdown", handlePointerDown, true);
|
||||
document.addEventListener("touchstart", handlePointerDown, true);
|
||||
|
||||
scheduleInitialMeasures();
|
||||
pollId = window.setInterval(
|
||||
() => scheduleMeasure("poll"),
|
||||
KEYBOARD_POLL_INTERVAL_MS,
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (rafId != null) window.cancelAnimationFrame(rafId);
|
||||
timeoutIds.forEach((id) => window.clearTimeout(id));
|
||||
removeKeyboardRecheckListeners();
|
||||
resetKeyboardVars();
|
||||
clearTimers();
|
||||
visualViewport?.removeEventListener("resize", handleViewportSignal);
|
||||
visualViewport?.removeEventListener("scroll", handleViewportSignal);
|
||||
window.removeEventListener("resize", handleViewportSignal);
|
||||
window.removeEventListener("orientationchange", handleViewportSignal);
|
||||
window.removeEventListener("focusout", handleFocusOut);
|
||||
window.removeEventListener("blur", handleFocusOut);
|
||||
document.removeEventListener("visibilitychange", handleViewportSignal);
|
||||
document.removeEventListener("pointerdown", handlePointerDown, true);
|
||||
document.removeEventListener("touchstart", handlePointerDown, true);
|
||||
resetInputLiftVar();
|
||||
};
|
||||
}, [
|
||||
active,
|
||||
targetRef,
|
||||
]);
|
||||
}
|
||||
|
||||
function readKeyboardSnapshot(): KeyboardSnapshot {
|
||||
const layoutHeight = window.innerHeight;
|
||||
const visualViewport = window.visualViewport;
|
||||
const visibleHeight = visualViewport?.height ?? layoutHeight;
|
||||
const visibleBottom = visualViewport
|
||||
? visualViewport.offsetTop + visualViewport.height
|
||||
: layoutHeight;
|
||||
const keyboardHeight = Math.max(0, layoutHeight - visibleBottom);
|
||||
const keyboardVisible =
|
||||
keyboardHeight > KEYBOARD_VISIBLE_THRESHOLD_PX ||
|
||||
visibleHeight < layoutHeight * KEYBOARD_VISIBLE_RATIO;
|
||||
|
||||
return {
|
||||
layoutHeight,
|
||||
visibleHeight,
|
||||
visibleBottom,
|
||||
keyboardHeight,
|
||||
keyboardVisible,
|
||||
};
|
||||
}
|
||||
|
||||
function computeTargetOverlap(
|
||||
target: HTMLElement | null | undefined,
|
||||
visibleBottom: number,
|
||||
): number {
|
||||
if (!target) return 0;
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
return Math.max(0, rect.bottom + FOCUS_GAP_PX - visibleBottom);
|
||||
}
|
||||
|
||||
function formatSnapshot(snapshot: KeyboardSnapshot) {
|
||||
return {
|
||||
layoutHeight: Math.round(snapshot.layoutHeight),
|
||||
visibleHeight: Math.round(snapshot.visibleHeight),
|
||||
visibleBottom: Math.round(snapshot.visibleBottom),
|
||||
keyboardHeight: Math.round(snapshot.keyboardHeight),
|
||||
keyboardVisible: snapshot.keyboardVisible,
|
||||
};
|
||||
}, [targetRef]);
|
||||
}
|
||||
|
||||
export type { UseKeyboardHeightOptions };
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { RECHECK_EVENT } from "./constants";
|
||||
import { getVirtualKeyboard } from "./virtual-keyboard";
|
||||
|
||||
export function addKeyboardRecheckListeners(handler: (event?: Event) => void) {
|
||||
const vv = window.visualViewport;
|
||||
if (vv) {
|
||||
vv.addEventListener("resize", handler);
|
||||
vv.addEventListener("scroll", handler);
|
||||
}
|
||||
|
||||
const virtualKeyboard = getVirtualKeyboard();
|
||||
virtualKeyboard?.addEventListener("geometrychange", handler);
|
||||
window.addEventListener("resize", handler);
|
||||
window.addEventListener("orientationchange", handler);
|
||||
window.addEventListener("focusin", handler);
|
||||
window.addEventListener("focusout", handler);
|
||||
window.addEventListener("blur", handler);
|
||||
document.addEventListener("visibilitychange", handler);
|
||||
window.addEventListener(RECHECK_EVENT, handler);
|
||||
|
||||
return () => {
|
||||
if (vv) {
|
||||
vv.removeEventListener("resize", handler);
|
||||
vv.removeEventListener("scroll", handler);
|
||||
}
|
||||
virtualKeyboard?.removeEventListener("geometrychange", handler);
|
||||
window.removeEventListener("resize", handler);
|
||||
window.removeEventListener("orientationchange", handler);
|
||||
window.removeEventListener("focusin", handler);
|
||||
window.removeEventListener("focusout", handler);
|
||||
window.removeEventListener("blur", handler);
|
||||
document.removeEventListener("visibilitychange", handler);
|
||||
window.removeEventListener(RECHECK_EVENT, handler);
|
||||
};
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { KEYBOARD_RELEASE_EVENT_TYPES } from "./constants";
|
||||
|
||||
export function isPossibleKeyboardCloseEvent(
|
||||
event: Event,
|
||||
hadInputLift: boolean,
|
||||
): boolean {
|
||||
if (!hadInputLift) {
|
||||
return event.type === "resize" || event.type === "geometrychange";
|
||||
}
|
||||
return KEYBOARD_RELEASE_EVENT_TYPES.has(event.type);
|
||||
}
|
||||
@@ -6,15 +6,6 @@ export interface UseKeyboardHeightOptions {
|
||||
onKeyboardDismiss?: () => void;
|
||||
}
|
||||
|
||||
export interface ViewportMetrics {
|
||||
hasVisualViewport: boolean;
|
||||
hasVirtualKeyboard: boolean;
|
||||
layoutHeight: number;
|
||||
visibleHeight: number;
|
||||
visibleBottom: number;
|
||||
keyboardHeight: number;
|
||||
}
|
||||
|
||||
export interface KeyboardAdaptEnvironment {
|
||||
platform: string;
|
||||
osName: string;
|
||||
@@ -31,6 +22,5 @@ export interface KeyboardAdaptEnvironment {
|
||||
isXiaomiFamilyDevice: boolean;
|
||||
isFacebookInAppBrowser: boolean;
|
||||
hasVisualViewport: boolean;
|
||||
hasVirtualKeyboard: boolean;
|
||||
usesXiaomiFacebookFallback: boolean;
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import {
|
||||
FALLBACK_CLOSE_GRACE_MS,
|
||||
FALLBACK_KEYBOARD_MAX,
|
||||
FALLBACK_KEYBOARD_MIN,
|
||||
FALLBACK_KEYBOARD_RATIO,
|
||||
FOCUS_GAP_PX,
|
||||
} from "./constants";
|
||||
import { shouldUseInAppKeyboardFallback } from "./environment";
|
||||
import type { ViewportMetrics } from "./types";
|
||||
import { getVirtualKeyboard } from "./virtual-keyboard";
|
||||
|
||||
export 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: visibleBottom,
|
||||
visibleBottom,
|
||||
keyboardHeight: virtualKeyboardHeight,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
virtualKeyboardHeight,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function computeTargetOverlap(
|
||||
target: HTMLElement | null | undefined,
|
||||
visibleBottom: number,
|
||||
): number {
|
||||
if (!target) return 0;
|
||||
const rect = target.getBoundingClientRect();
|
||||
return Math.max(0, rect.bottom + FOCUS_GAP_PX - visibleBottom);
|
||||
}
|
||||
|
||||
export function computeFallbackLift(metrics: ViewportMetrics): number {
|
||||
if (!shouldUseInAppKeyboardFallback()) return 0;
|
||||
|
||||
const viewportAlreadyShrank = metrics.visibleHeight < metrics.layoutHeight * 0.82;
|
||||
const keyboardAlreadyDetected = metrics.keyboardHeight > 80;
|
||||
if (viewportAlreadyShrank || keyboardAlreadyDetected) return 0;
|
||||
|
||||
return clamp(
|
||||
Math.round(metrics.layoutHeight * FALLBACK_KEYBOARD_RATIO),
|
||||
FALLBACK_KEYBOARD_MIN,
|
||||
FALLBACK_KEYBOARD_MAX,
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldReleaseKeyboardState(
|
||||
metrics: ViewportMetrics,
|
||||
focusedAt: number,
|
||||
): boolean {
|
||||
if (focusedAt <= 0) return false;
|
||||
if (performance.now() - focusedAt < FALLBACK_CLOSE_GRACE_MS) return false;
|
||||
return !hasDetectedKeyboard(metrics);
|
||||
}
|
||||
|
||||
export function getFocusedDurationMs(focusedAt: number): number | null {
|
||||
if (focusedAt <= 0) return null;
|
||||
return Math.round(performance.now() - focusedAt);
|
||||
}
|
||||
|
||||
export function getReleaseSkipReason(
|
||||
metrics: ViewportMetrics,
|
||||
focusedAt: number,
|
||||
): string {
|
||||
if (focusedAt <= 0) return "not-focused-by-hook";
|
||||
if (performance.now() - focusedAt < FALLBACK_CLOSE_GRACE_MS) {
|
||||
return "within-close-grace-window";
|
||||
}
|
||||
if (hasDetectedKeyboard(metrics)) return "keyboard-still-detected";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export function didViewportMetricsChange(
|
||||
previous: ViewportMetrics | null,
|
||||
current: ViewportMetrics,
|
||||
): boolean {
|
||||
if (!previous) return true;
|
||||
return (
|
||||
Math.round(previous.layoutHeight) !== Math.round(current.layoutHeight) ||
|
||||
Math.round(previous.visibleHeight) !== Math.round(current.visibleHeight) ||
|
||||
Math.round(previous.visibleBottom) !== Math.round(current.visibleBottom) ||
|
||||
Math.round(previous.keyboardHeight) !== Math.round(current.keyboardHeight)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasDetectedKeyboard(metrics: ViewportMetrics): boolean {
|
||||
return (
|
||||
metrics.keyboardHeight > 80 ||
|
||||
metrics.visibleHeight < metrics.layoutHeight * 0.82
|
||||
);
|
||||
}
|
||||
|
||||
export function formatMetrics(metrics: ViewportMetrics) {
|
||||
return {
|
||||
hasVisualViewport: metrics.hasVisualViewport,
|
||||
hasVirtualKeyboard: metrics.hasVirtualKeyboard,
|
||||
layoutHeight: Math.round(metrics.layoutHeight),
|
||||
visibleHeight: Math.round(metrics.visibleHeight),
|
||||
visibleBottom: Math.round(metrics.visibleBottom),
|
||||
keyboardHeight: Math.round(metrics.keyboardHeight),
|
||||
};
|
||||
}
|
||||
|
||||
export function getLiftSource(inputLift: number, fallbackLift: number): string {
|
||||
if (inputLift <= 0) return "none";
|
||||
if (fallbackLift > 0 && inputLift === fallbackLift) {
|
||||
return "xiaomi-facebook-fallback";
|
||||
}
|
||||
if (fallbackLift > 0) return "overlap-and-fallback";
|
||||
return "target-overlap";
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
interface VirtualKeyboardLike extends EventTarget {
|
||||
readonly boundingRect: DOMRectReadOnly;
|
||||
}
|
||||
|
||||
export function getVirtualKeyboard(): VirtualKeyboardLike | null {
|
||||
if (typeof navigator === "undefined") return null;
|
||||
const nav = navigator as Navigator & {
|
||||
virtualKeyboard?: VirtualKeyboardLike;
|
||||
};
|
||||
return nav.virtualKeyboard ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user