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.
This commit is contained in:
Riyadh
2026-05-20 14:30:09 +00:00
parent 6e7e6091bb
commit c34959ed0e
2 changed files with 18 additions and 5 deletions
+11 -4
View File
@@ -32,7 +32,7 @@ const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ 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",
@@ -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,
};
}