import { useEffect, useState } from 'react'; export interface VisualViewportInsets { /** The visual viewport's current height (shrinks when the on-screen * keyboard opens). Falls back to `window.innerHeight` when * `visualViewport` isn't supported. */ height: number; /** Extra inset a `position: fixed`, bottom-anchored element should add to * its own `bottom` offset to stay clear of the on-screen keyboard -- * `window.innerHeight` minus the visual viewport's bottom edge (its * height + offsetTop). Zero whenever no keyboard is open, or * `visualViewport` isn't supported (a safe no-op fallback). */ keyboardInset: number; } const ZERO_INSETS: VisualViewportInsets = { height: 0, keyboardInset: 0 }; function computeInsets(): VisualViewportInsets { if (typeof window === 'undefined') return ZERO_INSETS; const vv = window.visualViewport; if (!vv) return { height: window.innerHeight, keyboardInset: 0 }; const keyboardInset = Math.max(0, window.innerHeight - (vv.height + vv.offsetTop)); return { height: vv.height, keyboardInset }; } /** * Tracks `window.visualViewport`'s height/offset (item 5, Phase B) so * `BottomSheet` can stay clear of the on-screen keyboard. `position: fixed` * elements are positioned against the LAYOUT viewport, which does NOT shrink * when a mobile keyboard opens -- only the visual viewport does -- so a * bottom-anchored sheet's inputs can otherwise end up hidden underneath the * keyboard with no visual indication anything is wrong. * * Guards for browsers without `visualViewport` (older WebViews): falls back * to `{ height: window.innerHeight, keyboardInset: 0 }`, i.e. a no-op, so * the sheet just keeps its existing (keyboard-unaware) sizing there. */ export function useVisualViewportInsets(): VisualViewportInsets { const [insets, setInsets] = useState(computeInsets); useEffect(() => { if (typeof window === 'undefined' || !window.visualViewport) return; const vv = window.visualViewport; const handleChange = () => setInsets(computeInsets()); handleChange(); vv.addEventListener('resize', handleChange); vv.addEventListener('scroll', handleChange); return () => { vv.removeEventListener('resize', handleChange); vv.removeEventListener('scroll', handleChange); }; }, []); return insets; }