Files
TX/artifacts/tx-os/src/hooks/use-visual-viewport-vars.ts
T
riyadhafraa 338d80cf0a iOS keyboard fix v3 — visualViewport + touch-gate (#624)
Root causes the prior attempts missed:

1) Safari iOS/iPadOS ignores `interactive-widget=resizes-content`,
   so `100dvh` never shrinks when the soft keyboard opens. Every
   previous override that used `dvh` to size the dialog was sized
   to the full screen and ran behind the keyboard.

2) The CSS rule from #622 (`[role="dialog"]:has(:focus)`) was
   unconditional. On desktop, focusing any input inside a Radix
   DialogContent instantly stretched it to width:100vw + top:0,
   which collided with Radix's open-state slide-animation
   transforms and rendered the dialog as a broken narrow strip
   pinned in the top-left corner — visible in Tahani's laptop
   screenshot when opening "Add Meeting".

3) `AdminFormDialog` puts `role="dialog"` on a NON-fixed inner
   card, so the rule's `top/left/right` had no effect there
   anyway; the card stayed centred in a layout-viewport-sized
   wrapper which iOS does not shrink.

Fix:
- New `useVisualViewportVars` hook mounted in
  NotificationsSocketBridge. Writes `--vv-height` and
  `--vv-offset-top` on `<html>` from `window.visualViewport`,
  updated on resize / scroll / orientationchange. Falls back
  to `innerHeight`/0 if visualViewport is absent.
- Rewrote the index.css dialog rule:
  * Gated inside `@media (any-pointer: coarse)` so desktop is
    completely unaffected.
  * Uses `var(--vv-height, 100vh)` and `var(--vv-offset-top, 0px)`
    instead of `100dvh` / `0`, so dialogs actually track the
    visible band on iOS.
- New `.dialog-vv-wrapper` helper class that applies the same
  vv-sized top/height to bespoke full-screen overlay wrappers.
- Applied `.dialog-vv-wrapper` to AdminFormDialog and the admin
  ConfirmDialog wrappers; switched their `fixed inset-0` to
  `fixed inset-x-0 top-0 h-screen` and their inner cards from
  `max-h-[90vh]`/`max-h-[85vh]` to `max-h-full` so the card
  shrinks with the wrapper.

Kept `useScrollFocusedDialogField` unchanged — works correctly
once the dialog itself is sized to the visible band.

`tsc --noEmit` clean.
2026-05-21 12:04:12 +00:00

57 lines
2.0 KiB
TypeScript

import { useEffect } from "react";
/**
* Publishes the live visual-viewport geometry as CSS custom properties
* on `<html>` so stylesheets can size dialogs/sheets correctly above
* the iOS soft keyboard.
*
* Why a JS hook (not CSS `dvh`):
* - Safari iOS/iPadOS ignores the `interactive-widget=resizes-content`
* viewport hint, so `100dvh` does NOT shrink when the soft keyboard
* opens — it stays equal to the full screen. Any layout pinned with
* `dvh` therefore sits behind the keyboard.
* - `window.visualViewport` IS updated on iOS (including standalone
* PWAs) whenever the keyboard, page-pinch zoom, or browser chrome
* resizes the visible area. It's the only reliable signal.
*
* The hook writes two vars on `document.documentElement`:
* --vv-height : current visualViewport.height in `px`
* --vv-offset-top : current visualViewport.offsetTop in `px`
* On browsers without `visualViewport` it falls back to `window.innerHeight`
* and `0`, which matches the pre-existing `100vh` behaviour.
*
* Mount once at the app root (e.g. inside `NotificationsSocketBridge`).
*/
export function useVisualViewportVars(): void {
useEffect(() => {
if (typeof window === "undefined") return;
const root = document.documentElement;
const vv = window.visualViewport ?? null;
const apply = () => {
const h = vv ? vv.height : window.innerHeight;
const top = vv ? vv.offsetTop : 0;
root.style.setProperty("--vv-height", `${Math.round(h)}px`);
root.style.setProperty("--vv-offset-top", `${Math.round(top)}px`);
};
apply();
if (vv) {
vv.addEventListener("resize", apply);
vv.addEventListener("scroll", apply);
}
window.addEventListener("resize", apply);
window.addEventListener("orientationchange", apply);
return () => {
if (vv) {
vv.removeEventListener("resize", apply);
vv.removeEventListener("scroll", apply);
}
window.removeEventListener("resize", apply);
window.removeEventListener("orientationchange", apply);
};
}, []);
}