fix(chat): stabilize Facebook keyboard fallback
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
|||||||
|
|
||||||
const baseline: ChatKeyboardViewportBaseline = {
|
const baseline: ChatKeyboardViewportBaseline = {
|
||||||
layoutHeight: 800,
|
layoutHeight: 800,
|
||||||
|
layoutWidth: 390,
|
||||||
visualHeight: 760,
|
visualHeight: 760,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,9 +83,42 @@ describe("resolveChatKeyboardInset", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveChatKeyboardFallbackInset", () => {
|
describe("resolveChatKeyboardFallbackInset", () => {
|
||||||
it("scales with viewport height and remains bounded", () => {
|
it.each([
|
||||||
expect(resolveChatKeyboardFallbackInset(500)).toBe(276);
|
[{ layoutWidth: 320, layoutHeight: 568 }, 256],
|
||||||
expect(resolveChatKeyboardFallbackInset(800)).toBe(352);
|
[{ layoutWidth: 360, layoutHeight: 640 }, 285],
|
||||||
expect(resolveChatKeyboardFallbackInset(1_200)).toBe(396);
|
[{ 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 visualViewport: MockVisualViewport;
|
||||||
let virtualKeyboard: MockVirtualKeyboard;
|
let virtualKeyboard: MockVirtualKeyboard;
|
||||||
let innerHeight: number;
|
let innerHeight: number;
|
||||||
|
let innerWidth: number;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
@@ -52,12 +53,17 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(true);
|
vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(true);
|
||||||
vi.mocked(PlatformDetector.isAndroid).mockReturnValue(true);
|
vi.mocked(PlatformDetector.isAndroid).mockReturnValue(true);
|
||||||
innerHeight = 800;
|
innerHeight = 800;
|
||||||
|
innerWidth = 390;
|
||||||
visualViewport = new MockVisualViewport();
|
visualViewport = new MockVisualViewport();
|
||||||
virtualKeyboard = new MockVirtualKeyboard();
|
virtualKeyboard = new MockVirtualKeyboard();
|
||||||
Object.defineProperty(window, "innerHeight", {
|
Object.defineProperty(window, "innerHeight", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get: () => innerHeight,
|
get: () => innerHeight,
|
||||||
});
|
});
|
||||||
|
Object.defineProperty(window, "innerWidth", {
|
||||||
|
configurable: true,
|
||||||
|
get: () => innerWidth,
|
||||||
|
});
|
||||||
Object.defineProperty(window, "visualViewport", {
|
Object.defineProperty(window, "visualViewport", {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: visualViewport,
|
value: visualViewport,
|
||||||
@@ -89,16 +95,16 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
vi.unstubAllGlobals();
|
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");
|
const intervalSpy = vi.spyOn(window, "setInterval");
|
||||||
renderHarness(root, false);
|
renderHarness(root, false);
|
||||||
renderHarness(root, true);
|
renderHarness(root, true);
|
||||||
|
|
||||||
act(() => vi.advanceTimersByTime(249));
|
act(() => vi.advanceTimersByTime(149));
|
||||||
expect(getBottomInset(container)).toBe("");
|
expect(getBottomInset(container)).toBe("");
|
||||||
|
|
||||||
act(() => vi.advanceTimersByTime(1));
|
act(() => vi.advanceTimersByTime(1));
|
||||||
expect(getBottomInset(container)).toBe("352px");
|
expect(getBottomInset(container)).toBe("316px");
|
||||||
expect(intervalSpy).not.toHaveBeenCalled();
|
expect(intervalSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -112,7 +118,7 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
act(() => {
|
act(() => {
|
||||||
visualViewport.dispatchEvent(new Event("resize"));
|
visualViewport.dispatchEvent(new Event("resize"));
|
||||||
virtualKeyboard.dispatchEvent(new Event("geometrychange"));
|
virtualKeyboard.dispatchEvent(new Event("geometrychange"));
|
||||||
vi.advanceTimersByTime(250);
|
vi.advanceTimersByTime(150);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getBottomInset(container)).toBe("");
|
expect(getBottomInset(container)).toBe("");
|
||||||
@@ -126,7 +132,7 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
visualViewport.height = 460;
|
visualViewport.height = 460;
|
||||||
act(() => {
|
act(() => {
|
||||||
visualViewport.dispatchEvent(new Event("resize"));
|
visualViewport.dispatchEvent(new Event("resize"));
|
||||||
vi.advanceTimersByTime(250);
|
vi.advanceTimersByTime(150);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getBottomInset(container)).toBe("");
|
expect(getBottomInset(container)).toBe("");
|
||||||
@@ -172,7 +178,7 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
visualViewport.height = 500;
|
visualViewport.height = 500;
|
||||||
act(() => {
|
act(() => {
|
||||||
window.dispatchEvent(new Event("resize"));
|
window.dispatchEvent(new Event("resize"));
|
||||||
vi.advanceTimersByTime(250);
|
vi.advanceTimersByTime(150);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getBottomInset(container)).toBe("0px");
|
expect(getBottomInset(container)).toBe("0px");
|
||||||
@@ -181,23 +187,23 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
it("allows a delayed real viewport signal to replace the fallback", () => {
|
it("allows a delayed real viewport signal to replace the fallback", () => {
|
||||||
renderHarness(root, false);
|
renderHarness(root, false);
|
||||||
renderHarness(root, true);
|
renderHarness(root, true);
|
||||||
act(() => vi.advanceTimersByTime(250));
|
act(() => vi.advanceTimersByTime(150));
|
||||||
expect(getBottomInset(container)).toBe("352px");
|
expect(getBottomInset(container)).toBe("316px");
|
||||||
|
|
||||||
visualViewport.height = 460;
|
visualViewport.height = 500;
|
||||||
act(() => {
|
act(() => {
|
||||||
visualViewport.dispatchEvent(new Event("resize"));
|
visualViewport.dispatchEvent(new Event("resize"));
|
||||||
vi.advanceTimersByTime(0);
|
vi.advanceTimersByTime(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getBottomInset(container)).toBe("316px");
|
expect(getBottomInset(container)).toBe("276px");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes the local inset when focus leaves", () => {
|
it("removes the local inset when focus leaves", () => {
|
||||||
renderHarness(root, false);
|
renderHarness(root, false);
|
||||||
renderHarness(root, true);
|
renderHarness(root, true);
|
||||||
act(() => vi.advanceTimersByTime(250));
|
act(() => vi.advanceTimersByTime(150));
|
||||||
expect(getBottomInset(container)).toBe("352px");
|
expect(getBottomInset(container)).toBe("316px");
|
||||||
|
|
||||||
renderHarness(root, false);
|
renderHarness(root, false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
export const CHAT_KEYBOARD_VISIBLE_THRESHOLD_PX = 80;
|
export const CHAT_KEYBOARD_VISIBLE_THRESHOLD_PX = 80;
|
||||||
export const CHAT_KEYBOARD_ZOOM_THRESHOLD = 1.05;
|
export const CHAT_KEYBOARD_ZOOM_THRESHOLD = 1.05;
|
||||||
export const CHAT_KEYBOARD_TARGET_GAP_PX = 16;
|
export const CHAT_KEYBOARD_TARGET_GAP_PX = 16;
|
||||||
export const CHAT_KEYBOARD_FALLBACK_RATIO = 0.42;
|
export const CHAT_KEYBOARD_FALLBACK_WIDTH_RATIO = 0.77;
|
||||||
export const CHAT_KEYBOARD_FALLBACK_MIN_PX = 260;
|
export const CHAT_KEYBOARD_FALLBACK_HEIGHT_RATIO = 0.42;
|
||||||
export const CHAT_KEYBOARD_FALLBACK_MAX_PX = 380;
|
export const CHAT_KEYBOARD_FALLBACK_MIN_PX = 240;
|
||||||
|
export const CHAT_KEYBOARD_FALLBACK_MAX_PX = 340;
|
||||||
|
|
||||||
const CHAT_KEYBOARD_MAX_INSET_RATIO = 0.55;
|
const CHAT_KEYBOARD_MAX_INSET_RATIO = 0.55;
|
||||||
|
|
||||||
export interface ChatKeyboardViewportBaseline {
|
export interface ChatKeyboardViewportBaseline {
|
||||||
layoutHeight: number;
|
layoutHeight: number;
|
||||||
|
layoutWidth: number;
|
||||||
visualHeight: number;
|
visualHeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChatKeyboardFallbackViewport = Pick<
|
||||||
|
ChatKeyboardViewportBaseline,
|
||||||
|
"layoutHeight" | "layoutWidth"
|
||||||
|
>;
|
||||||
|
|
||||||
export interface ChatKeyboardViewportSnapshot {
|
export interface ChatKeyboardViewportSnapshot {
|
||||||
layoutHeight: number;
|
layoutHeight: number;
|
||||||
visualHeight: number;
|
visualHeight: number;
|
||||||
@@ -80,13 +87,17 @@ export function resolveChatKeyboardInset(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveChatKeyboardFallbackInset(
|
export function resolveChatKeyboardFallbackInset(
|
||||||
layoutHeight: number,
|
viewport: ChatKeyboardFallbackViewport,
|
||||||
): number {
|
): number {
|
||||||
|
const widthEstimate =
|
||||||
|
viewport.layoutWidth * CHAT_KEYBOARD_FALLBACK_WIDTH_RATIO;
|
||||||
|
const heightLimit =
|
||||||
|
viewport.layoutHeight * CHAT_KEYBOARD_FALLBACK_HEIGHT_RATIO;
|
||||||
const estimatedKeyboardHeight = Math.min(
|
const estimatedKeyboardHeight = Math.min(
|
||||||
CHAT_KEYBOARD_FALLBACK_MAX_PX,
|
CHAT_KEYBOARD_FALLBACK_MAX_PX,
|
||||||
Math.max(
|
Math.max(
|
||||||
CHAT_KEYBOARD_FALLBACK_MIN_PX,
|
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;
|
fallbackApplied = true;
|
||||||
applyBottomInset(
|
applyBottomInset(
|
||||||
resolveChatKeyboardFallbackInset(baseline.layoutHeight),
|
resolveChatKeyboardFallbackInset(baseline),
|
||||||
"fallback",
|
"fallback",
|
||||||
);
|
);
|
||||||
}, CHAT_KEYBOARD_FALLBACK_DELAY_MS);
|
}, CHAT_KEYBOARD_FALLBACK_DELAY_MS);
|
||||||
@@ -221,6 +221,7 @@ export function useChatKeyboardAvoidance({
|
|||||||
function readViewportBaseline(): ChatKeyboardViewportBaseline {
|
function readViewportBaseline(): ChatKeyboardViewportBaseline {
|
||||||
return {
|
return {
|
||||||
layoutHeight: window.innerHeight,
|
layoutHeight: window.innerHeight,
|
||||||
|
layoutWidth: window.innerWidth,
|
||||||
visualHeight: window.visualViewport?.height ?? window.innerHeight,
|
visualHeight: window.visualViewport?.height ?? window.innerHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user