Files
openchamber/packages/ui/src/hooks/useVisualViewport.ts
T
Junlang HuangandBohdan Triapitsyn 48af84222a fix: prevent mobile keyboard from occluding terminal viewport (#1303)
* fix: prevent mobile keyboard from occluding terminal viewport

On mobile, 100dvh does not shrink when the soft keyboard opens, causing
the terminal and other absolute-inset-0 views to extend behind the keyboard.

Add useVisualViewport hook that tracks window.visualViewport.resize/scroll
via RAF. On mobile, use the visual viewport height (actual visible area)
for the root container instead of 100dvh, so content resizes above the
keyboard when it opens.

* fix: skip unchanged visual viewport updates

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-18 18:05:13 +03:00

53 lines
1.6 KiB
TypeScript

import React from 'react';
export interface VisualViewportState {
height: number;
keyboardHeight: number;
}
const getInitialHeight = (): number => {
if (typeof window === 'undefined') return 0;
return window.visualViewport?.height ?? window.innerHeight;
};
export const useVisualViewport = (): VisualViewportState => {
const [state, setState] = React.useState<VisualViewportState>(() => ({
height: getInitialHeight(),
keyboardHeight: 0,
}));
const rafIdRef = React.useRef<number | null>(null);
React.useEffect(() => {
if (typeof window === 'undefined' || !window.visualViewport) return;
const handleChange = () => {
if (rafIdRef.current !== null) return;
rafIdRef.current = requestAnimationFrame(() => {
rafIdRef.current = null;
const vv = window.visualViewport!;
const height = vv.height;
const keyboardHeight = Math.max(0, window.innerHeight - height);
setState((prev) => {
if (prev.height === height && prev.keyboardHeight === keyboardHeight) return prev;
return { height, keyboardHeight };
});
});
};
window.visualViewport.addEventListener('resize', handleChange);
window.visualViewport.addEventListener('scroll', handleChange, { passive: true });
handleChange();
return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
}
window.visualViewport?.removeEventListener('resize', handleChange);
window.visualViewport?.removeEventListener('scroll', handleChange);
};
}, []);
return state;
};