fix(chat): scope keyboard avoidance to Facebook IAB
This commit is contained in:
@@ -38,7 +38,7 @@ describe("resolveChatKeyboardInset", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the visual viewport occlusion without an extra gap", () => {
|
it("uses the visual viewport occlusion with the target gap", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveChatKeyboardInset(
|
resolveChatKeyboardInset(
|
||||||
baseline,
|
baseline,
|
||||||
@@ -46,7 +46,7 @@ describe("resolveChatKeyboardInset", () => {
|
|||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
keyboardVisible: true,
|
keyboardVisible: true,
|
||||||
bottomInset: 300,
|
bottomInset: 308,
|
||||||
source: "visual-viewport",
|
source: "visual-viewport",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,7 @@ describe("resolveChatKeyboardInset", () => {
|
|||||||
),
|
),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
keyboardVisible: true,
|
keyboardVisible: true,
|
||||||
bottomInset: 320,
|
bottomInset: 328,
|
||||||
source: "virtual-keyboard",
|
source: "virtual-keyboard",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -83,8 +83,8 @@ describe("resolveChatKeyboardInset", () => {
|
|||||||
|
|
||||||
describe("resolveChatKeyboardFallbackInset", () => {
|
describe("resolveChatKeyboardFallbackInset", () => {
|
||||||
it("scales with viewport height and remains bounded", () => {
|
it("scales with viewport height and remains bounded", () => {
|
||||||
expect(resolveChatKeyboardFallbackInset(500)).toBe(260);
|
expect(resolveChatKeyboardFallbackInset(500)).toBe(268);
|
||||||
expect(resolveChatKeyboardFallbackInset(800)).toBe(336);
|
expect(resolveChatKeyboardFallbackInset(800)).toBe(344);
|
||||||
expect(resolveChatKeyboardFallbackInset(1_200)).toBe(380);
|
expect(resolveChatKeyboardFallbackInset(1_200)).toBe(388);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client";
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { BrowserDetector } from "@/utils/browser-detect";
|
import { BrowserDetector } from "@/utils/browser-detect";
|
||||||
|
import { PlatformDetector } from "@/utils/platform-detect";
|
||||||
|
|
||||||
import { useChatKeyboardAvoidance } from "../use-chat-keyboard-avoidance";
|
import { useChatKeyboardAvoidance } from "../use-chat-keyboard-avoidance";
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
|
||||||
.IS_REACT_ACT_ENVIRONMENT = true;
|
.IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(true);
|
vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(true);
|
||||||
|
vi.mocked(PlatformDetector.isAndroid).mockReturnValue(true);
|
||||||
innerHeight = 800;
|
innerHeight = 800;
|
||||||
visualViewport = new MockVisualViewport();
|
visualViewport = new MockVisualViewport();
|
||||||
virtualKeyboard = new MockVirtualKeyboard();
|
virtualKeyboard = new MockVirtualKeyboard();
|
||||||
@@ -96,16 +98,36 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
expect(getBottomInset(container)).toBe("");
|
expect(getBottomInset(container)).toBe("");
|
||||||
|
|
||||||
act(() => vi.advanceTimersByTime(1));
|
act(() => vi.advanceTimersByTime(1));
|
||||||
expect(getBottomInset(container)).toBe("336px");
|
expect(getBottomInset(container)).toBe("344px");
|
||||||
expect(intervalSpy).not.toHaveBeenCalled();
|
expect(intervalSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not use fallback outside Facebook's in-app browser", () => {
|
it("does not run any avoidance outside Facebook's in-app browser", () => {
|
||||||
vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(false);
|
vi.mocked(BrowserDetector.isFacebookInAppBrowser).mockReturnValue(false);
|
||||||
renderHarness(root, false);
|
renderHarness(root, false);
|
||||||
renderHarness(root, true);
|
renderHarness(root, true);
|
||||||
|
|
||||||
act(() => vi.advanceTimersByTime(250));
|
visualViewport.height = 460;
|
||||||
|
virtualKeyboard.boundingRect.height = 320;
|
||||||
|
act(() => {
|
||||||
|
visualViewport.dispatchEvent(new Event("resize"));
|
||||||
|
virtualKeyboard.dispatchEvent(new Event("geometrychange"));
|
||||||
|
vi.advanceTimersByTime(250);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getBottomInset(container)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not run avoidance for iOS Facebook", () => {
|
||||||
|
vi.mocked(PlatformDetector.isAndroid).mockReturnValue(false);
|
||||||
|
renderHarness(root, false);
|
||||||
|
renderHarness(root, true);
|
||||||
|
|
||||||
|
visualViewport.height = 460;
|
||||||
|
act(() => {
|
||||||
|
visualViewport.dispatchEvent(new Event("resize"));
|
||||||
|
vi.advanceTimersByTime(250);
|
||||||
|
});
|
||||||
|
|
||||||
expect(getBottomInset(container)).toBe("");
|
expect(getBottomInset(container)).toBe("");
|
||||||
});
|
});
|
||||||
@@ -119,7 +141,7 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
visualViewport.dispatchEvent(new Event("resize"));
|
visualViewport.dispatchEvent(new Event("resize"));
|
||||||
vi.advanceTimersByTime(0);
|
vi.advanceTimersByTime(0);
|
||||||
});
|
});
|
||||||
expect(getBottomInset(container)).toBe("300px");
|
expect(getBottomInset(container)).toBe("308px");
|
||||||
|
|
||||||
visualViewport.height = 760;
|
visualViewport.height = 760;
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -129,7 +151,7 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
expect(getBottomInset(container)).toBe("0px");
|
expect(getBottomInset(container)).toBe("0px");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses Virtual Keyboard geometry without an extra gap", () => {
|
it("uses Virtual Keyboard geometry with the target gap", () => {
|
||||||
renderHarness(root, false);
|
renderHarness(root, false);
|
||||||
renderHarness(root, true);
|
renderHarness(root, true);
|
||||||
|
|
||||||
@@ -139,7 +161,7 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
vi.advanceTimersByTime(0);
|
vi.advanceTimersByTime(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getBottomInset(container)).toBe("320px");
|
expect(getBottomInset(container)).toBe("328px");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not add fallback when the layout viewport resized", () => {
|
it("does not add fallback when the layout viewport resized", () => {
|
||||||
@@ -160,7 +182,7 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
renderHarness(root, false);
|
renderHarness(root, false);
|
||||||
renderHarness(root, true);
|
renderHarness(root, true);
|
||||||
act(() => vi.advanceTimersByTime(250));
|
act(() => vi.advanceTimersByTime(250));
|
||||||
expect(getBottomInset(container)).toBe("336px");
|
expect(getBottomInset(container)).toBe("344px");
|
||||||
|
|
||||||
visualViewport.height = 460;
|
visualViewport.height = 460;
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -168,14 +190,14 @@ describe("useChatKeyboardAvoidance", () => {
|
|||||||
vi.advanceTimersByTime(0);
|
vi.advanceTimersByTime(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getBottomInset(container)).toBe("300px");
|
expect(getBottomInset(container)).toBe("308px");
|
||||||
});
|
});
|
||||||
|
|
||||||
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(250));
|
||||||
expect(getBottomInset(container)).toBe("336px");
|
expect(getBottomInset(container)).toBe("344px");
|
||||||
|
|
||||||
renderHarness(root, false);
|
renderHarness(root, false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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 = 8;
|
||||||
export const CHAT_KEYBOARD_FALLBACK_RATIO = 0.42;
|
export const CHAT_KEYBOARD_FALLBACK_RATIO = 0.42;
|
||||||
export const CHAT_KEYBOARD_FALLBACK_MIN_PX = 260;
|
export const CHAT_KEYBOARD_FALLBACK_MIN_PX = 260;
|
||||||
export const CHAT_KEYBOARD_FALLBACK_MAX_PX = 380;
|
export const CHAT_KEYBOARD_FALLBACK_MAX_PX = 380;
|
||||||
@@ -67,7 +68,10 @@ export function resolveChatKeyboardInset(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
keyboardVisible: true,
|
keyboardVisible: true,
|
||||||
bottomInset: Math.min(maxInset, Math.round(occludedHeight)),
|
bottomInset: Math.min(
|
||||||
|
maxInset,
|
||||||
|
Math.round(occludedHeight + CHAT_KEYBOARD_TARGET_GAP_PX),
|
||||||
|
),
|
||||||
source:
|
source:
|
||||||
virtualKeyboardHeight >= visualViewportHeight
|
virtualKeyboardHeight >= visualViewportHeight
|
||||||
? "virtual-keyboard"
|
? "virtual-keyboard"
|
||||||
@@ -78,11 +82,13 @@ export function resolveChatKeyboardInset(
|
|||||||
export function resolveChatKeyboardFallbackInset(
|
export function resolveChatKeyboardFallbackInset(
|
||||||
layoutHeight: number,
|
layoutHeight: number,
|
||||||
): number {
|
): number {
|
||||||
return 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(layoutHeight * CHAT_KEYBOARD_FALLBACK_RATIO),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return estimatedKeyboardHeight + CHAT_KEYBOARD_TARGET_GAP_PX;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function useChatKeyboardAvoidance({
|
|||||||
}, [active]);
|
}, [active]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined" || !PlatformDetector.isAndroid()) {
|
if (typeof window === "undefined" || !isKeyboardAvoidanceEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,16 +72,16 @@ export function useChatKeyboardAvoidance({
|
|||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (typeof window === "undefined" || !container) return;
|
if (typeof window === "undefined" || !container) return;
|
||||||
|
|
||||||
if (!active || !PlatformDetector.isAndroid()) {
|
const enabled = isKeyboardAvoidanceEnabled();
|
||||||
|
if (!active || !enabled) {
|
||||||
clearBottomInset(container);
|
clearBottomInset(container);
|
||||||
if (!active) baselineRef.current = readViewportBaseline();
|
if (!active && enabled) baselineRef.current = readViewportBaseline();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseline = baselineRef.current ?? readViewportBaseline();
|
const baseline = baselineRef.current ?? readViewportBaseline();
|
||||||
const visualViewport = window.visualViewport;
|
const visualViewport = window.visualViewport;
|
||||||
const virtualKeyboard = getVirtualKeyboard();
|
const virtualKeyboard = getVirtualKeyboard();
|
||||||
const fallbackAllowed = BrowserDetector.isFacebookInAppBrowser();
|
|
||||||
let frameId: number | null = null;
|
let frameId: number | null = null;
|
||||||
let fallbackTimerId: number | null = null;
|
let fallbackTimerId: number | null = null;
|
||||||
let lastInset = -1;
|
let lastInset = -1;
|
||||||
@@ -194,8 +194,6 @@ export function useChatKeyboardAvoidance({
|
|||||||
applyBottomInset(resolution.bottomInset, resolution.source);
|
applyBottomInset(resolution.bottomInset, resolution.source);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!fallbackAllowed) return;
|
|
||||||
|
|
||||||
fallbackApplied = true;
|
fallbackApplied = true;
|
||||||
applyBottomInset(
|
applyBottomInset(
|
||||||
resolveChatKeyboardFallbackInset(baseline.layoutHeight),
|
resolveChatKeyboardFallbackInset(baseline.layoutHeight),
|
||||||
@@ -246,3 +244,9 @@ function getVirtualKeyboard(): VirtualKeyboardLike | null {
|
|||||||
function clearBottomInset(container: HTMLElement): void {
|
function clearBottomInset(container: HTMLElement): void {
|
||||||
container.style.removeProperty(CHAT_KEYBOARD_BOTTOM_INSET_VAR);
|
container.style.removeProperty(CHAT_KEYBOARD_BOTTOM_INSET_VAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isKeyboardAvoidanceEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
PlatformDetector.isAndroid() && BrowserDetector.isFacebookInAppBrowser()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user