From c34959ed0ee0d307c1f12ba1a08b4ed253d5e812 Mon Sep 17 00:00:00 2001 From: Riyadh Date: Wed, 20 May 2026 14:30:09 +0000 Subject: [PATCH] Make dialogs appear correctly on iOS when the keyboard is open Adjust dialog positioning logic to use visual viewport dimensions for precise placement and sizing on iOS, resolving display issues caused by layout viewport discrepancies with the soft keyboard. --- artifacts/tx-os/src/components/ui/dialog.tsx | 15 +++++++++++---- artifacts/tx-os/src/hooks/use-visual-viewport.ts | 8 +++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/artifacts/tx-os/src/components/ui/dialog.tsx b/artifacts/tx-os/src/components/ui/dialog.tsx index 84295812..23e33173 100644 --- a/artifacts/tx-os/src/components/ui/dialog.tsx +++ b/artifacts/tx-os/src/components/ui/dialog.tsx @@ -32,7 +32,7 @@ const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, style, ...props }, ref) => { - const { keyboardInset, height, offsetTop } = useVisualViewport() + const { keyboardInset, width, height, offsetTop, offsetLeft } = useVisualViewport() // Only react when an on-screen keyboard is actually present. iOS Safari // and Android Chrome both report a non-trivial bottom inset (>= ~150px) // when their software keyboard opens; tiny insets from browser chrome @@ -60,10 +60,17 @@ const DialogContent = React.forwardRef< // dialog's internal `flex flex-col` (header / scrollable middle // / sticky footer) then keeps Save/Cancel pinned above the // keyboard and the rest of the form scrolls inside. + // Use the visualViewport rectangle directly. position:fixed on + // iOS Safari is measured against the LAYOUT viewport, which + // diverges from the visual viewport when the soft keyboard is + // open AND when Safari pinch/auto-zooms a focused input. So we + // must explicitly set top/left/width to the visual viewport's + // offsetTop/offsetLeft/width and zero out the centering + // translates the base className applies. top: `${offsetTop}px`, - left: "8px", - right: "8px", - width: "auto", + left: `${offsetLeft}px`, + right: "auto", + width: `${width}px`, maxWidth: "none", maxHeight: `${height - 8}px`, transform: "none", diff --git a/artifacts/tx-os/src/hooks/use-visual-viewport.ts b/artifacts/tx-os/src/hooks/use-visual-viewport.ts index 1ef094fc..a039bfd7 100644 --- a/artifacts/tx-os/src/hooks/use-visual-viewport.ts +++ b/artifacts/tx-os/src/hooks/use-visual-viewport.ts @@ -1,28 +1,34 @@ import { useEffect, useState } from "react"; export interface VisualViewportState { + width: number; height: number; keyboardInset: number; offsetTop: number; + offsetLeft: number; } function readState(): VisualViewportState { if (typeof window === "undefined") { - return { height: 0, keyboardInset: 0, offsetTop: 0 }; + return { width: 0, height: 0, keyboardInset: 0, offsetTop: 0, offsetLeft: 0 }; } const vv = window.visualViewport; if (!vv) { return { + width: window.innerWidth, height: window.innerHeight, keyboardInset: 0, offsetTop: 0, + offsetLeft: 0, }; } const inset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop); return { + width: vv.width, height: vv.height, keyboardInset: inset, offsetTop: vv.offsetTop, + offsetLeft: vv.offsetLeft, }; }