56 lines
2.3 KiB
TypeScript
56 lines
2.3 KiB
TypeScript
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<VisualViewportInsets>(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;
|
||
|
|
}
|