Scroll focused dialog field above iOS keyboard (#622)

The CSS `[role="dialog"]:has(:focus)` rule from #621 promotes any
focused dialog to a full-width sheet sized to `100dvh`, but Safari
does not auto-scroll the focused input into view inside the
dialog's own overflow container — it only tries to scroll the
document, which our position:fixed dialog ignores. The field stays
where it was, often behind the keyboard.

Added a global `useScrollFocusedDialogField` hook (in
artifacts/tx-os/src/hooks/) that listens to `document` focusin. If
the target is an input/textarea/select/contenteditable inside a
`closest('[role="dialog"]')`, it calls
`target.scrollIntoView({block:'center', behavior:'smooth'})` twice
(300ms and 600ms) to cover the iPad keyboard animation window and
the dialog's `max-height:100dvh` recalc. Timers are cleared on blur
and on unmount.

Mounted the hook once in `NotificationsSocketBridge` inside App.tsx
so it covers every page and every dialog kind we use (Radix
DialogContent, the bespoke AdminFormDialog, ad-hoc role="dialog"
wrappers) without touching their JSX.

No CSS or dialog component changes. `tsc --noEmit` clean.
This commit is contained in:
Riyadh
2026-05-21 09:05:29 +00:00
parent 9370051ecf
commit b1589fe4e7
@@ -24,6 +24,15 @@ import { useEffect } from "react";
*/
export function useScrollFocusedDialogField(): void {
useEffect(() => {
// Gate strictly to touch devices. Desktop browsers (mouse pointer)
// never open a soft keyboard, so scrolling the focused field would
// be unwanted movement. `pointer: coarse` matches touch-primary
// devices (iPad, iPhone, Android) and is stable across orientation
// / split-view changes, unlike visualViewport heuristics.
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
const isTouch = window.matchMedia("(pointer: coarse)").matches;
if (!isTouch) return;
const isEditable = (el: Element | null): el is HTMLElement => {
if (!el || !(el instanceof HTMLElement)) return false;
const tag = el.tagName;