Improve dialog behavior and appearance when the keyboard is open
Refactor dialog styling to use global CSS with !important rules for keyboard open state, overriding inline styles and Tailwind classes.
This commit is contained in:
@@ -39,6 +39,19 @@ const DialogContent = React.forwardRef<
|
||||
// changes are ignored so desktop and normal mobile scrolling stay
|
||||
// untouched.
|
||||
const keyboardOpen = keyboardInset > 80
|
||||
// CSS vars consumed by the global `[data-dialog-keyboard-open="true"]`
|
||||
// rule in index.css. We pass position/size through CSS variables and
|
||||
// let `!important` rules win the cascade against arbitrary consumer
|
||||
// Tailwind classes (`w-[calc(100vw-1rem)]`, `max-w-3xl`, `max-h-[92vh]`,
|
||||
// entrance-animation transforms, etc.) which previously beat inline
|
||||
// `style` on iPad PWA and left the sheet pinned as a narrow strip in
|
||||
// the top-left corner.
|
||||
const kbVars = keyboardOpen
|
||||
? ({
|
||||
["--dialog-top" as never]: `${offsetTop}px`,
|
||||
["--dialog-max-h" as never]: `${height - 8}px`,
|
||||
} as React.CSSProperties)
|
||||
: undefined
|
||||
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
|
||||
@@ -109,64 +122,14 @@ const DialogContent = React.forwardRef<
|
||||
node.addEventListener("focusin", onFocusIn)
|
||||
return () => node.removeEventListener("focusin", onFocusIn)
|
||||
}, [])
|
||||
const dynamicStyle: React.CSSProperties = keyboardOpen
|
||||
? {
|
||||
// Mobile-sheet behaviour when an on-screen keyboard is open.
|
||||
//
|
||||
// The base className puts the dialog at left:50%+translateX(-50%)
|
||||
// (horizontal centering) and top:50%+translateY(-50%) (vertical
|
||||
// centering). On iOS Safari with the soft keyboard open the
|
||||
// layout viewport and the visual viewport diverge in BOTH axes
|
||||
// (visualViewport.offsetLeft, .pageLeft and zoom factor can all
|
||||
// shift), which made our previous "top-anchor + translateX(-50%)"
|
||||
// override render as a narrow column shoved against one edge of
|
||||
// the screen.
|
||||
//
|
||||
// To sidestep all of that we drop the centering entirely while
|
||||
// the keyboard is open and pin the dialog as a top-anchored
|
||||
// full-width sheet: explicit left/right insets, no transform,
|
||||
// width:auto so the consumer's `w-[calc(100vw-1rem)]` or
|
||||
// `max-w-3xl` no longer drive horizontal position. The
|
||||
// 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.
|
||||
// `position: fixed` on iOS is anchored to the LAYOUT viewport,
|
||||
// which stays at `window.innerWidth` even while the visual
|
||||
// viewport shrinks (keyboard, pinch-zoom, etc). If we set
|
||||
// width/left from `visualViewport.width/offsetLeft` the dialog
|
||||
// ends up rendered at a fraction of the screen — a narrow
|
||||
// strip pinned to one corner — because those values describe
|
||||
// the *visible* rectangle in layout pixels, not the dialog's
|
||||
// own container.
|
||||
//
|
||||
// Pin horizontally to the layout viewport (0 / 100vw) so the
|
||||
// dialog always spans the full width the iframe sees, and pin
|
||||
// vertically to the visual viewport so it sits just above the
|
||||
// keyboard. `maxHeight` uses `vv.height` so the dialog's
|
||||
// internal scroll keeps the footer above the keyboard.
|
||||
top: `${offsetTop}px`,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: "100vw",
|
||||
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 ?? {})
|
||||
const mergedStyle: React.CSSProperties = { ...(kbVars ?? {}), ...(style ?? {}) }
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={setRefs}
|
||||
style={dynamicStyle}
|
||||
data-dialog-keyboard-open={keyboardOpen ? "true" : undefined}
|
||||
style={mergedStyle}
|
||||
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",
|
||||
className
|
||||
|
||||
@@ -469,4 +469,30 @@ html[dir="rtl"] .truncate {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Dialog keyboard-aware sheet override.
|
||||
*
|
||||
* On iPad/iPhone PWAs with the soft keyboard open, consumer
|
||||
* DialogContent classes like `w-[calc(100vw-1rem)] sm:w-full
|
||||
* max-w-3xl max-h-[92vh]` plus the Radix entrance-animation transforms
|
||||
* (slide-in-from-left-1/2, zoom-in-95) were beating inline `style` and
|
||||
* pinning the dialog as a narrow strip in the top-left corner. CSS
|
||||
* variables (`--dialog-top`, `--dialog-max-h`) are set inline by
|
||||
* `DialogContent` from `useVisualViewport`; the `!important` rules
|
||||
* here guarantee they win the cascade no matter what classes the
|
||||
* consumer passes in.
|
||||
*/
|
||||
[data-dialog-keyboard-open="true"] {
|
||||
top: var(--dialog-top, 0px) !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: auto !important;
|
||||
width: 100vw !important;
|
||||
max-width: none !important;
|
||||
max-height: var(--dialog-max-h, 100vh) !important;
|
||||
transform: none !important;
|
||||
overflow-y: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user