Files
spsays-training/lib/use-visual-viewport.ts
T
2026-08-29 04:12:30 +05:30

64 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* Tracks the *visual* viewport — the part of the page not covered by the
* software keyboard.
*
* Why this exists: on iOS Safari `100dvh` does not shrink when the keyboard
* opens. The dynamic viewport units respond to browser chrome collapsing, not
* to the keyboard, so a composer pinned to the bottom of a `100dvh` column
* ends up underneath it. `window.visualViewport` is the only thing iOS updates
* reliably, so the thread is sized from that instead while the keyboard is up.
*
* `offsetTop` matters as much as `height`: iOS scrolls the layout viewport to
* reveal the focused field rather than resizing it, so a panel pinned to the
* visible area has to follow that offset or it drifts off-screen.
*/
import { useEffect, useState } from "react";
export type VisualViewport = {
/** Visible height in px, excluding the keyboard. */
height: number;
/** How far the layout viewport has scrolled to reveal the focused field. */
offsetTop: number;
/** True once the visible area is materially shorter than the window. */
keyboardOpen: boolean;
};
/**
* A URL bar collapsing costs ~50–60px; a keyboard costs several hundred. The
* threshold keeps ordinary chrome changes from being read as a keyboard.
*/
const KEYBOARD_THRESHOLD_PX = 120;
export function useVisualViewport(): VisualViewport | null {
const [state, setState] = useState<VisualViewport | null>(null);
useEffect(() => {
const vv = window.visualViewport;
// Absent on older browsers — callers fall back to their CSS height.
if (!vv) return;
const apply = () => {
setState({
height: vv.height,
offsetTop: vv.offsetTop,
keyboardOpen: window.innerHeight - vv.height > KEYBOARD_THRESHOLD_PX,
});
};
apply();
vv.addEventListener("resize", apply);
// iOS fires scroll (not resize) as the layout viewport shifts under the
// keyboard, so both are needed to stay pinned.
vv.addEventListener("scroll", apply);
return () => {
vv.removeEventListener("resize", apply);
vv.removeEventListener("scroll", apply);
};
}, []);
return state;
}