fix(ui): update viewport height without visualViewport (#1314)

* fix(ui): update viewport height without visualViewport

* fix(ui): initialize visual viewport state from helper

---------

Co-authored-by: Isaac Sanchez <isanchez-hawkins@arize.com>
This commit is contained in:
Isaac Sanchez-Hawkins
2026-05-19 16:58:18 +03:00
committed by GitHub
co-authored by Isaac Sanchez
parent 46bef9b0c7
commit 9ed44326f8
2 changed files with 65 additions and 17 deletions
@@ -0,0 +1,35 @@
import { describe, expect, test } from 'bun:test';
import { getVisualViewportState } from './useVisualViewport';
const withWindow = (value: { innerHeight: number; visualViewport?: { height: number } | null }, run: () => void) => {
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
Object.defineProperty(globalThis, 'window', {
configurable: true,
value,
});
try {
run();
} finally {
if (originalWindow) {
Object.defineProperty(globalThis, 'window', originalWindow);
} else {
Reflect.deleteProperty(globalThis, 'window');
}
}
};
describe('getVisualViewportState', () => {
test('falls back to innerHeight when visualViewport is unavailable', () => {
withWindow({ innerHeight: 812 }, () => {
expect(getVisualViewportState()).toEqual({ height: 812, keyboardHeight: 0 });
});
});
test('derives keyboard height from visualViewport height', () => {
withWindow({ innerHeight: 812, visualViewport: { height: 512 } }, () => {
expect(getVisualViewportState()).toEqual({ height: 512, keyboardHeight: 300 });
});
});
});
+30 -17
View File
@@ -5,46 +5,59 @@ export interface VisualViewportState {
keyboardHeight: number;
}
const getInitialHeight = (): number => {
if (typeof window === 'undefined') return 0;
return window.visualViewport?.height ?? window.innerHeight;
export const getVisualViewportState = (): VisualViewportState => {
if (typeof window === 'undefined') {
return { height: 0, keyboardHeight: 0 };
}
const height = window.visualViewport?.height ?? window.innerHeight;
const keyboardHeight = window.visualViewport
? Math.max(0, window.innerHeight - height)
: 0;
return { height, keyboardHeight };
};
export const useVisualViewport = (): VisualViewportState => {
const [state, setState] = React.useState<VisualViewportState>(() => ({
height: getInitialHeight(),
keyboardHeight: 0,
}));
const [state, setState] = React.useState<VisualViewportState>(getVisualViewportState);
const rafIdRef = React.useRef<number | null>(null);
React.useEffect(() => {
if (typeof window === 'undefined' || !window.visualViewport) return;
if (typeof window === 'undefined') return;
const visualViewport = window.visualViewport;
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);
const nextState = getVisualViewportState();
setState((prev) => {
if (prev.height === height && prev.keyboardHeight === keyboardHeight) return prev;
return { height, keyboardHeight };
if (prev.height === nextState.height && prev.keyboardHeight === nextState.keyboardHeight) return prev;
return nextState;
});
});
};
window.visualViewport.addEventListener('resize', handleChange);
window.visualViewport.addEventListener('scroll', handleChange, { passive: true });
if (visualViewport) {
visualViewport.addEventListener('resize', handleChange);
visualViewport.addEventListener('scroll', handleChange, { passive: true });
} else {
window.addEventListener('resize', handleChange);
}
handleChange();
return () => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
}
window.visualViewport?.removeEventListener('resize', handleChange);
window.visualViewport?.removeEventListener('scroll', handleChange);
if (visualViewport) {
visualViewport.removeEventListener('resize', handleChange);
visualViewport.removeEventListener('scroll', handleChange);
} else {
window.removeEventListener('resize', handleChange);
}
};
}, []);