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, }; }