From e852fb62e9438e8cdc9c4d37f2ff844c8d6eac24 Mon Sep 17 00:00:00 2001 From: chenhang Date: Fri, 3 Jul 2026 11:58:46 +0800 Subject: [PATCH] fix(viewport): suppress ios facebook keyboard resize --- src/providers/viewport-css-vars-provider.tsx | 61 ++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/providers/viewport-css-vars-provider.tsx b/src/providers/viewport-css-vars-provider.tsx index 375a8dbb..8d448164 100644 --- a/src/providers/viewport-css-vars-provider.tsx +++ b/src/providers/viewport-css-vars-provider.tsx @@ -2,6 +2,8 @@ import { useEffect, type ReactNode } from "react"; +import { BrowserDetector, Logger, PlatformDetector } from "@/utils"; + const CSS_VAR_VIEWPORT_HEIGHT = "--app-viewport-height"; const CSS_VAR_VISIBLE_HEIGHT = "--app-visible-height"; const CSS_VAR_VISIBLE_OFFSET_TOP = "--app-visible-offset-top"; @@ -9,6 +11,9 @@ const CSS_VAR_SAFE_TOP = "--app-safe-top"; const CSS_VAR_SAFE_RIGHT = "--app-safe-right"; const CSS_VAR_SAFE_BOTTOM = "--app-safe-bottom"; const CSS_VAR_SAFE_LEFT = "--app-safe-left"; +const IOS_KEYBOARD_VIEWPORT_DELTA_PX = 120; + +const log = new Logger("ProvidersViewportCssVarsProvider"); export interface ViewportCssVarsProviderProps { children: ReactNode; @@ -28,6 +33,12 @@ export function ViewportCssVarsProvider({ if (typeof window === "undefined") return; let frameId: number | null = null; + let stableLayoutHeight = window.innerHeight; + let stableVisibleHeight = window.visualViewport?.height ?? window.innerHeight; + let stableVisibleOffsetTop = window.visualViewport?.offsetTop ?? 0; + let iosFacebookKeyboardSuppressed = false; + const isIOSFacebookInAppBrowser = + PlatformDetector.isIOS() && BrowserDetector.isFacebookInAppBrowser(); const applyVars = () => { frameId = null; @@ -38,12 +49,42 @@ export function ViewportCssVarsProvider({ const visibleHeight = viewport?.height ?? layoutHeight; const visibleWidth = viewport?.width ?? layoutWidth; const visibleOffsetTop = viewport?.offsetTop ?? 0; + const shouldSuppressIOSFacebookKeyboardViewport = + isIOSFacebookInAppBrowser && + isEditableElementFocused() && + stableLayoutHeight - visibleHeight > IOS_KEYBOARD_VIEWPORT_DELTA_PX; + const nextLayoutHeight = shouldSuppressIOSFacebookKeyboardViewport + ? stableLayoutHeight + : layoutHeight; + const nextVisibleHeight = shouldSuppressIOSFacebookKeyboardViewport + ? stableVisibleHeight + : visibleHeight; + const nextVisibleOffsetTop = shouldSuppressIOSFacebookKeyboardViewport + ? stableVisibleOffsetTop + : visibleOffsetTop; - root.style.setProperty(CSS_VAR_VIEWPORT_HEIGHT, `${layoutHeight}px`); - root.style.setProperty(CSS_VAR_VISIBLE_HEIGHT, `${visibleHeight}px`); + if (!shouldSuppressIOSFacebookKeyboardViewport) { + stableLayoutHeight = layoutHeight; + stableVisibleHeight = visibleHeight; + stableVisibleOffsetTop = visibleOffsetTop; + iosFacebookKeyboardSuppressed = false; + } else if (!iosFacebookKeyboardSuppressed) { + iosFacebookKeyboardSuppressed = true; + log.debug("[viewport] suppress ios facebook keyboard viewport", { + layoutHeight: Math.round(layoutHeight), + visibleHeight: Math.round(visibleHeight), + visibleOffsetTop: Math.round(visibleOffsetTop), + stableLayoutHeight: Math.round(stableLayoutHeight), + stableVisibleHeight: Math.round(stableVisibleHeight), + stableVisibleOffsetTop: Math.round(stableVisibleOffsetTop), + }); + } + + root.style.setProperty(CSS_VAR_VIEWPORT_HEIGHT, `${nextLayoutHeight}px`); + root.style.setProperty(CSS_VAR_VISIBLE_HEIGHT, `${nextVisibleHeight}px`); root.style.setProperty( CSS_VAR_VISIBLE_OFFSET_TOP, - `${visibleOffsetTop}px`, + `${nextVisibleOffsetTop}px`, ); root.style.setProperty(CSS_VAR_SAFE_TOP, "env(safe-area-inset-top, 0px)"); root.style.setProperty( @@ -59,7 +100,7 @@ export function ViewportCssVarsProvider({ "env(safe-area-inset-left, 0px)", ); root.dataset.viewportWidth = String(Math.round(visibleWidth)); - root.dataset.viewportHeight = String(Math.round(visibleHeight)); + root.dataset.viewportHeight = String(Math.round(nextVisibleHeight)); }; const scheduleApply = () => { @@ -85,3 +126,15 @@ export function ViewportCssVarsProvider({ return children; } + +function isEditableElementFocused(): boolean { + const activeElement = document.activeElement; + if (!(activeElement instanceof HTMLElement)) return false; + + const tagName = activeElement.tagName.toLowerCase(); + return ( + tagName === "input" || + tagName === "textarea" || + activeElement.isContentEditable + ); +}