fix(chat): stabilize Facebook keyboard fallback

This commit is contained in:
2026-07-16 17:17:34 +08:00
parent f8934b1412
commit ffd988bf95
4 changed files with 74 additions and 22 deletions
@@ -9,6 +9,7 @@ import {
const baseline: ChatKeyboardViewportBaseline = {
layoutHeight: 800,
layoutWidth: 390,
visualHeight: 760,
};
@@ -82,9 +83,42 @@ describe("resolveChatKeyboardInset", () => {
});
describe("resolveChatKeyboardFallbackInset", () => {
it("scales with viewport height and remains bounded", () => {
expect(resolveChatKeyboardFallbackInset(500)).toBe(276);
expect(resolveChatKeyboardFallbackInset(800)).toBe(352);
expect(resolveChatKeyboardFallbackInset(1_200)).toBe(396);
it.each([
[{ layoutWidth: 320, layoutHeight: 568 }, 256],
[{ layoutWidth: 360, layoutHeight: 640 }, 285],
[{ layoutWidth: 390, layoutHeight: 844 }, 316],
[{ layoutWidth: 430, layoutHeight: 932 }, 347],
])("fits common viewport %o", (viewport, expectedInset) => {
expect(resolveChatKeyboardFallbackInset(viewport)).toBe(expectedInset);
});
it("stays stable across tall viewports with the same width", () => {
expect(
resolveChatKeyboardFallbackInset({
layoutWidth: 390,
layoutHeight: 760,
}),
).toBe(316);
expect(
resolveChatKeyboardFallbackInset({
layoutWidth: 390,
layoutHeight: 900,
}),
).toBe(316);
});
it("keeps the keyboard estimate within its bounds", () => {
expect(
resolveChatKeyboardFallbackInset({
layoutWidth: 300,
layoutHeight: 400,
}),
).toBe(256);
expect(
resolveChatKeyboardFallbackInset({
layoutWidth: 600,
layoutHeight: 1_000,
}),
).toBe(356);
});
});
@@ -44,6 +44,7 @@ describe("useChatKeyboardAvoidance", () => {
let visualViewport: MockVisualViewport;
let virtualKeyboard: MockVirtualKeyboard;
let innerHeight: number;
let innerWidth: number;
beforeEach(() => {
vi.useFakeTimers();
@@ -52,12 +53,17 @@ describe("useChatKeyboardAvoidance", () => {
vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(true);
vi.mocked(PlatformDetector.isAndroid).mockReturnValue(true);
innerHeight = 800;
innerWidth = 390;
visualViewport = new MockVisualViewport();
virtualKeyboard = new MockVirtualKeyboard();
Object.defineProperty(window, "innerHeight", {
configurable: true,
get: () => innerHeight,
});
Object.defineProperty(window, "innerWidth", {
configurable: true,
get: () => innerWidth,
});
Object.defineProperty(window, "visualViewport", {
configurable: true,
value: visualViewport,
@@ -89,16 +95,16 @@ describe("useChatKeyboardAvoidance", () => {
vi.unstubAllGlobals();
});
it("uses a one-shot Facebook fallback after 250ms without polling", () => {
it("uses a one-shot Facebook fallback after 150ms without polling", () => {
const intervalSpy = vi.spyOn(window, "setInterval");
renderHarness(root, false);
renderHarness(root, true);
act(() => vi.advanceTimersByTime(249));
act(() => vi.advanceTimersByTime(149));
expect(getBottomInset(container)).toBe("");
act(() => vi.advanceTimersByTime(1));
expect(getBottomInset(container)).toBe("352px");
expect(getBottomInset(container)).toBe("316px");
expect(intervalSpy).not.toHaveBeenCalled();
});
@@ -112,7 +118,7 @@ describe("useChatKeyboardAvoidance", () => {
act(() => {
visualViewport.dispatchEvent(new Event("resize"));
virtualKeyboard.dispatchEvent(new Event("geometrychange"));
vi.advanceTimersByTime(250);
vi.advanceTimersByTime(150);
});
expect(getBottomInset(container)).toBe("");
@@ -126,7 +132,7 @@ describe("useChatKeyboardAvoidance", () => {
visualViewport.height = 460;
act(() => {
visualViewport.dispatchEvent(new Event("resize"));
vi.advanceTimersByTime(250);
vi.advanceTimersByTime(150);
});
expect(getBottomInset(container)).toBe("");
@@ -172,7 +178,7 @@ describe("useChatKeyboardAvoidance", () => {
visualViewport.height = 500;
act(() => {
window.dispatchEvent(new Event("resize"));
vi.advanceTimersByTime(250);
vi.advanceTimersByTime(150);
});
expect(getBottomInset(container)).toBe("0px");
@@ -181,23 +187,23 @@ describe("useChatKeyboardAvoidance", () => {
it("allows a delayed real viewport signal to replace the fallback", () => {
renderHarness(root, false);
renderHarness(root, true);
act(() => vi.advanceTimersByTime(250));
expect(getBottomInset(container)).toBe("352px");
act(() => vi.advanceTimersByTime(150));
expect(getBottomInset(container)).toBe("316px");
visualViewport.height = 460;
visualViewport.height = 500;
act(() => {
visualViewport.dispatchEvent(new Event("resize"));
vi.advanceTimersByTime(0);
});
expect(getBottomInset(container)).toBe("316px");
expect(getBottomInset(container)).toBe("276px");
});
it("removes the local inset when focus leaves", () => {
renderHarness(root, false);
renderHarness(root, true);
act(() => vi.advanceTimersByTime(250));
expect(getBottomInset(container)).toBe("352px");
act(() => vi.advanceTimersByTime(150));
expect(getBottomInset(container)).toBe("316px");
renderHarness(root, false);
+16 -5
View File
@@ -1,17 +1,24 @@
export const CHAT_KEYBOARD_VISIBLE_THRESHOLD_PX = 80;
export const CHAT_KEYBOARD_ZOOM_THRESHOLD = 1.05;
export const CHAT_KEYBOARD_TARGET_GAP_PX = 16;
export const CHAT_KEYBOARD_FALLBACK_RATIO = 0.42;
export const CHAT_KEYBOARD_FALLBACK_MIN_PX = 260;
export const CHAT_KEYBOARD_FALLBACK_MAX_PX = 380;
export const CHAT_KEYBOARD_FALLBACK_WIDTH_RATIO = 0.77;
export const CHAT_KEYBOARD_FALLBACK_HEIGHT_RATIO = 0.42;
export const CHAT_KEYBOARD_FALLBACK_MIN_PX = 240;
export const CHAT_KEYBOARD_FALLBACK_MAX_PX = 340;
const CHAT_KEYBOARD_MAX_INSET_RATIO = 0.55;
export interface ChatKeyboardViewportBaseline {
layoutHeight: number;
layoutWidth: number;
visualHeight: number;
}
export type ChatKeyboardFallbackViewport = Pick<
ChatKeyboardViewportBaseline,
"layoutHeight" | "layoutWidth"
>;
export interface ChatKeyboardViewportSnapshot {
layoutHeight: number;
visualHeight: number;
@@ -80,13 +87,17 @@ export function resolveChatKeyboardInset(
}
export function resolveChatKeyboardFallbackInset(
layoutHeight: number,
viewport: ChatKeyboardFallbackViewport,
): number {
const widthEstimate =
viewport.layoutWidth * CHAT_KEYBOARD_FALLBACK_WIDTH_RATIO;
const heightLimit =
viewport.layoutHeight * CHAT_KEYBOARD_FALLBACK_HEIGHT_RATIO;
const estimatedKeyboardHeight = Math.min(
CHAT_KEYBOARD_FALLBACK_MAX_PX,
Math.max(
CHAT_KEYBOARD_FALLBACK_MIN_PX,
Math.round(layoutHeight * CHAT_KEYBOARD_FALLBACK_RATIO),
Math.round(Math.min(widthEstimate, heightLimit)),
),
);
@@ -196,7 +196,7 @@ export function useChatKeyboardAvoidance({
}
fallbackApplied = true;
applyBottomInset(
resolveChatKeyboardFallbackInset(baseline.layoutHeight),
resolveChatKeyboardFallbackInset(baseline),
"fallback",
);
}, CHAT_KEYBOARD_FALLBACK_DELAY_MS);
@@ -221,6 +221,7 @@ export function useChatKeyboardAvoidance({
function readViewportBaseline(): ChatKeyboardViewportBaseline {
return {
layoutHeight: window.innerHeight,
layoutWidth: window.innerWidth,
visualHeight: window.visualViewport?.height ?? window.innerHeight,
};
}