78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import { useEffect, useState, type CSSProperties } from "react";
|
|
|
|
export type VisualViewportOffset = {
|
|
/** Pixels between the bottom of `visualViewport` and the bottom of the layout viewport. */
|
|
bottomOffset: number;
|
|
/** True when the visual viewport is noticeably shorter than the layout viewport (virtual keyboard open). */
|
|
keyboardOpen: boolean;
|
|
/** Y-coordinate of the visual viewport's bottom edge in the layout viewport — `vv.offsetTop + vv.height`. Use with `position: fixed; top:` to pin an element above the keyboard reliably on iOS even during scrolls. */
|
|
viewportBottom: number;
|
|
};
|
|
|
|
const KEYBOARD_THRESHOLD_PX = 80;
|
|
|
|
export function resolveVisualViewportPosition({
|
|
keyboardOpen,
|
|
viewportBottom,
|
|
barHeight,
|
|
baseBottomOffset,
|
|
}: {
|
|
keyboardOpen: boolean;
|
|
viewportBottom: number;
|
|
barHeight: number;
|
|
baseBottomOffset?: string;
|
|
}): Pick<CSSProperties, "top" | "bottom"> {
|
|
if (keyboardOpen) {
|
|
return { top: `${viewportBottom - barHeight}px`, bottom: "auto" };
|
|
}
|
|
|
|
const base = baseBottomOffset
|
|
? `calc(${baseBottomOffset} + env(safe-area-inset-bottom, 0px))`
|
|
: "env(safe-area-inset-bottom, 0px)";
|
|
return { bottom: base };
|
|
}
|
|
|
|
function readOffset(): VisualViewportOffset {
|
|
if (typeof window === "undefined" || !window.visualViewport) {
|
|
return { bottomOffset: 0, keyboardOpen: false, viewportBottom: 0 };
|
|
}
|
|
const vv = window.visualViewport;
|
|
const bottomOffset = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
return {
|
|
bottomOffset,
|
|
keyboardOpen: bottomOffset > KEYBOARD_THRESHOLD_PX,
|
|
viewportBottom: vv.offsetTop + vv.height,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Tracks the visual viewport's bottom offset relative to the layout viewport so
|
|
* floating mobile UI (e.g., terminal key-bar) can dock above the on-screen
|
|
* keyboard. Returns zeros on the server and on browsers without
|
|
* `window.visualViewport`.
|
|
*/
|
|
export function useVisualViewportOffset(): VisualViewportOffset {
|
|
const [offset, setOffset] = useState<VisualViewportOffset>(() => ({
|
|
bottomOffset: 0,
|
|
keyboardOpen: false,
|
|
viewportBottom: 0,
|
|
}));
|
|
|
|
useEffect(() => {
|
|
if (typeof window === "undefined" || !window.visualViewport) return;
|
|
const vv = window.visualViewport;
|
|
const update = () => setOffset(readOffset());
|
|
update();
|
|
vv.addEventListener("resize", update);
|
|
vv.addEventListener("scroll", update);
|
|
window.addEventListener("resize", update);
|
|
return () => {
|
|
vv.removeEventListener("resize", update);
|
|
vv.removeEventListener("scroll", update);
|
|
window.removeEventListener("resize", update);
|
|
};
|
|
}, []);
|
|
|
|
return offset;
|
|
}
|