Task #630: keep recipients list visible above iPad keyboard in Send Note
Problem: opening Send Note on iPad, tapping the search field opened the
soft keyboard which covered the recipients list and the Send button —
the dialog stayed centered on the full layout viewport.
Fix (artifacts/tx-os/src/components/notes/send-note-dialog.tsx):
Two-tier keyboard detection:
1. Primary — `vv.keyboardInset > 0` from useVisualViewport. Works on
desktop touch laptops, iPad Safari (regular tab), Android Chrome.
When true, override DialogContent style with
top = vv.offsetTop + vv.height/2 − 48 (QuickType inset)
transform: translate(-50%, -50%)
so the dialog recenters around the visible viewport region.
2. Fallback — `searchFocused && matchMedia('(pointer: coarse)')`.
Only used when visualViewport does NOT report an inset, which is
the iPad PWA (Add to Home Screen) case explicitly called out in
components/ui/dialog.tsx comment. Top-anchors at 6vh with
maxHeight 50dvh so the dialog never crosses the screen midpoint.
3. Neither — pass NO style at all so Radix's default centered layout
is fully restored (no leftover transition or top).
Touch-only fallback gating (matchMedia pointer:coarse) prevents the
earlier desktop-regression flagged by code review — focusing the
search on a desktop never repositions the dialog.
Other changes:
- onFocus/onBlur on the search input feed the fallback signal.
- Blur is debounced 150ms so tapping a recipient row mid-blur doesn't
recenter the dialog out from under the finger.
- useEffect cleanup clears pending blur timeout on unmount.
Verification: tsc clean. Architect approved the touch-gated fallback;
follow-up review reconciled the diff with the spec's primary
visualViewport-driven path.
This commit is contained in:
@@ -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({
|
||||
<Dialog open onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent
|
||||
className="max-w-md"
|
||||
style={dialogStyle}
|
||||
{...(dialogStyle ? { style: dialogStyle } : {})}
|
||||
data-testid="send-note-dialog"
|
||||
>
|
||||
<DialogHeader className="pr-8">
|
||||
|
||||
Reference in New Issue
Block a user