Improve dialog scrolling behavior when the keyboard is open
Adjust dialog content to enable scrolling and center focused input fields, addressing issues on iOS and Android where content was hidden by the on-screen keyboard. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 6290c891-00dd-4660-a47c-75d5e9999b12 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qrzw3bH Replit-Helium-Checkpoint-Created: true
This commit is contained in:
@@ -39,6 +39,76 @@ const DialogContent = React.forwardRef<
|
||||
// changes are ignored so desktop and normal mobile scrolling stay
|
||||
// untouched.
|
||||
const keyboardOpen = keyboardInset > 80
|
||||
const innerRef = React.useRef<HTMLDivElement | null>(null)
|
||||
// Merge the consumer-provided forwardRef with our own ref so we can
|
||||
// attach focus listeners without breaking Radix's portal/animation
|
||||
// wiring (which relies on receiving a working ref).
|
||||
const setRefs = React.useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
innerRef.current = node
|
||||
if (typeof ref === "function") ref(node)
|
||||
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
||||
},
|
||||
[ref],
|
||||
)
|
||||
|
||||
// iOS PWA + Android Chrome both mis-handle focus inside a position:fixed
|
||||
// dialog when the soft keyboard opens:
|
||||
// * iOS scrolls the *layout* viewport upward trying to reveal the
|
||||
// focused input, which slides our fixed-positioned dialog off the
|
||||
// top of the screen. The text the user is typing disappears.
|
||||
// * Neither browser scrolls the focused field into view inside the
|
||||
// dialog's own scroll container, so tall forms (saved notes,
|
||||
// user profile, executive meetings) lose the focused field below
|
||||
// the keyboard or above the visible band.
|
||||
// The fix is dialog-wide so every form benefits without page edits:
|
||||
// 1. Undo iOS's layout-viewport scroll (`window.scrollTo(0, 0)`).
|
||||
// 2. After the keyboard animation settles, scrollIntoView({block:
|
||||
// 'center'}) on the focused field — this scrolls the dialog's
|
||||
// own overflow container, since the dialog is the nearest
|
||||
// scrollable ancestor when keyboardOpen flips on overflow-y.
|
||||
React.useEffect(() => {
|
||||
const node = innerRef.current
|
||||
if (!node) return
|
||||
const onFocusIn = (e: FocusEvent) => {
|
||||
const target = e.target as HTMLElement | null
|
||||
if (!target) return
|
||||
const tag = target.tagName
|
||||
if (
|
||||
tag !== "INPUT" &&
|
||||
tag !== "TEXTAREA" &&
|
||||
tag !== "SELECT" &&
|
||||
!target.isContentEditable
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Undo iOS layout-viewport push-up immediately, then again after
|
||||
// the keyboard finishes animating (~250ms on iPad).
|
||||
const reset = () => {
|
||||
try {
|
||||
window.scrollTo(0, 0)
|
||||
} catch {}
|
||||
try {
|
||||
target.scrollIntoView({ block: "center", behavior: "smooth" })
|
||||
} catch {
|
||||
target.scrollIntoView()
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(reset)
|
||||
const t = window.setTimeout(reset, 300)
|
||||
const t2 = window.setTimeout(reset, 600)
|
||||
target.addEventListener(
|
||||
"blur",
|
||||
() => {
|
||||
clearTimeout(t)
|
||||
clearTimeout(t2)
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
}
|
||||
node.addEventListener("focusin", onFocusIn)
|
||||
return () => node.removeEventListener("focusin", onFocusIn)
|
||||
}, [])
|
||||
const dynamicStyle: React.CSSProperties = keyboardOpen
|
||||
? {
|
||||
// Mobile-sheet behaviour when an on-screen keyboard is open.
|
||||
@@ -81,6 +151,13 @@ const DialogContent = React.forwardRef<
|
||||
maxWidth: "none",
|
||||
maxHeight: `${height - 8}px`,
|
||||
transform: "none",
|
||||
// Make the dialog itself the scrollable region above the
|
||||
// keyboard so scrollIntoView() on the focused field actually
|
||||
// moves it into view (its nearest scrollable ancestor is now
|
||||
// the dialog, not the body which can't scroll while a
|
||||
// position:fixed parent sits on top).
|
||||
overflowY: "auto",
|
||||
WebkitOverflowScrolling: "touch",
|
||||
...style,
|
||||
}
|
||||
: (style ?? {})
|
||||
@@ -88,7 +165,7 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
ref={setRefs}
|
||||
style={dynamicStyle}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
|
||||
Reference in New Issue
Block a user