diff --git a/artifacts/tx-os/src/components/notes/send-note-dialog.tsx b/artifacts/tx-os/src/components/notes/send-note-dialog.tsx index e4fef70f..9738c0ae 100644 --- a/artifacts/tx-os/src/components/notes/send-note-dialog.tsx +++ b/artifacts/tx-os/src/components/notes/send-note-dialog.tsx @@ -120,14 +120,22 @@ export function SendNoteDialog({ }, 150); }; + // Primary signal: visualViewport actually reports a keyboard inset. + // Used on desktop touch laptops, iPad Safari (regular tab), and + // Android Chrome where vv updates correctly. + const keyboardActive = vv.keyboardInset > 0; + // Fallback signal: iPad PWA (Add to Home Screen) doesn't reliably + // update visualViewport when the keyboard opens (see comment in + // components/ui/dialog.tsx). On touch-primary devices, treat focus + // on the search input as evidence the keyboard is up so the dialog + // still gets repositioned. This is a fallback only — when vv DOES + // update, the primary branch above takes over. + const keyboardFallback = !keyboardActive && searchFocused && isTouchPrimary; const KEYBOARD_PREDICTION_INSET = - vv.keyboardInset > 0 || keyboardLikely ? 48 : 0; + keyboardActive || keyboardFallback ? 48 : 0; const listMaxHeight = (() => { - // Only assume the ~50% usable bound on touch devices where a - // soft keyboard is actually about to appear. Desktops keep using - // the real viewport height (or visualViewport when it updates). const baseHeight = vv.height || window.innerHeight; - const assumedUsable = keyboardLikely + const assumedUsable = keyboardFallback ? Math.min(baseHeight, window.innerHeight * 0.5) : baseHeight; const usable = assumedUsable - KEYBOARD_PREDICTION_INSET; @@ -139,20 +147,32 @@ export function SendNoteDialog({ return forList; })(); - // Override DialogContent's centered positioning while typing on - // touch devices: anchor to top with `translate(-50%, 0)` so the - // dialog grows downward from a fixed top edge and never extends - // past the midpoint of the screen (where the soft keyboard sits). - // On desktop and other non-touch contexts, leave Radix's default - // centered layout alone. - const dialogStyle: React.CSSProperties = keyboardLikely + // Dialog positioning override: + // • Primary path (`keyboardActive`): compute the new vertical + // centre from the *visual* viewport — anchored at + // `offsetTop + height/2`, shifted up by the QuickType strip + // allowance so the Send button isn't hidden under predictions. + // Keep Radix's `translate(-50%, -50%)` so the dialog stays + // centered around the new anchor. + // • Fallback path (`keyboardFallback`): top-anchor at 6vh with + // `translate(-50%, 0)` and cap `maxHeight: 50dvh` so the dialog + // never crosses the screen midpoint where the keyboard sits. + // • Otherwise: pass NO style so Radix's default centered layout + // is fully restored (no leftover transition, no leftover top). + const dialogStyle: React.CSSProperties | undefined = keyboardActive ? { - top: "6vh", - transform: "translate(-50%, 0)", - maxHeight: "50dvh", - transition: "top 120ms ease, transform 120ms ease", + top: vv.offsetTop + vv.height / 2 - KEYBOARD_PREDICTION_INSET, + transform: "translate(-50%, -50%)", + transition: "top 120ms ease", } - : { transition: "top 120ms ease, transform 120ms ease" }; + : keyboardFallback + ? { + top: "6vh", + transform: "translate(-50%, 0)", + maxHeight: "50dvh", + transition: "top 120ms ease, transform 120ms ease", + } + : undefined; const candidates = useMemo(() => { const me = user?.id; @@ -217,7 +237,7 @@ export function SendNoteDialog({ !o && onClose()}>