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>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
76eef473aa
commit
48af84222a
@@ -20,6 +20,7 @@ import { useUIStore } from '@/stores/useUIStore';
|
|||||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
|
import { useVisualViewport } from '@/hooks/useVisualViewport';
|
||||||
import { useI18n } from '@/lib/i18n';
|
import { useI18n } from '@/lib/i18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { isDesktopShell } from '@/lib/desktop';
|
import { isDesktopShell } from '@/lib/desktop';
|
||||||
@@ -84,6 +85,7 @@ export const MainLayout: React.FC = () => {
|
|||||||
const multiRunLauncherPrefillPrompt = useUIStore((state) => state.multiRunLauncherPrefillPrompt);
|
const multiRunLauncherPrefillPrompt = useUIStore((state) => state.multiRunLauncherPrefillPrompt);
|
||||||
|
|
||||||
const { isMobile, isTablet } = useDeviceInfo();
|
const { isMobile, isTablet } = useDeviceInfo();
|
||||||
|
const visualViewport = useVisualViewport();
|
||||||
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
||||||
const sidebarWidth = useUIStore((state) => state.sidebarWidth);
|
const sidebarWidth = useUIStore((state) => state.sidebarWidth);
|
||||||
const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth);
|
const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth);
|
||||||
@@ -390,10 +392,11 @@ export const MainLayout: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
data-page-scroll-lock="true"
|
data-page-scroll-lock="true"
|
||||||
className={cn(
|
className={cn(
|
||||||
'main-content-safe-area h-[100dvh]',
|
'main-content-safe-area',
|
||||||
isMobile ? 'flex flex-col' : 'flex',
|
isMobile ? 'flex flex-col' : 'flex h-[100dvh]',
|
||||||
'bg-background'
|
'bg-background'
|
||||||
)}
|
)}
|
||||||
|
style={isMobile && visualViewport.height > 0 ? { height: visualViewport.height } : undefined}
|
||||||
>
|
>
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
<HelpDialog />
|
<HelpDialog />
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user