Fix mobile keyboard pushing chat off-screen with visualViewport and body scroll lock
This commit is contained in:
+53
-22
@@ -32,6 +32,7 @@ export default function Chat() {
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [sending, setSending] = useState(false);
|
||||
const [viewportHeight, setViewportHeight] = useState<number | undefined>(undefined);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -52,6 +53,46 @@ export default function Chat() {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior });
|
||||
}, []);
|
||||
|
||||
// Lock body scroll and track visual viewport height for mobile keyboard
|
||||
useEffect(() => {
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
html.style.overflow = 'hidden';
|
||||
html.style.height = '100%';
|
||||
body.style.overflow = 'hidden';
|
||||
body.style.height = '100%';
|
||||
|
||||
const vv = window.visualViewport;
|
||||
if (vv) {
|
||||
const update = () => {
|
||||
setViewportHeight(vv.height);
|
||||
// Scroll page back to top in case browser scrolled it
|
||||
window.scrollTo(0, 0);
|
||||
if (isAtBottom.current) {
|
||||
requestAnimationFrame(() => scrollToBottom('instant'));
|
||||
}
|
||||
};
|
||||
update();
|
||||
vv.addEventListener('resize', update);
|
||||
vv.addEventListener('scroll', () => window.scrollTo(0, 0));
|
||||
|
||||
return () => {
|
||||
html.style.overflow = '';
|
||||
html.style.height = '';
|
||||
body.style.overflow = '';
|
||||
body.style.height = '';
|
||||
vv.removeEventListener('resize', update);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
html.style.overflow = '';
|
||||
html.style.height = '';
|
||||
body.style.overflow = '';
|
||||
body.style.height = '';
|
||||
};
|
||||
}, [scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (authLoading) return;
|
||||
if (!user) {
|
||||
@@ -77,22 +118,6 @@ export default function Chat() {
|
||||
}
|
||||
}, [messages, scrollToBottom]);
|
||||
|
||||
// Handle visual viewport resize (mobile keyboard open/close)
|
||||
useEffect(() => {
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
|
||||
const handleResize = () => {
|
||||
if (isAtBottom.current) {
|
||||
// Use instant scroll when keyboard animates to avoid janky smooth scroll
|
||||
scrollToBottom('instant');
|
||||
}
|
||||
};
|
||||
|
||||
vv.addEventListener('resize', handleResize);
|
||||
return () => vv.removeEventListener('resize', handleResize);
|
||||
}, [scrollToBottom]);
|
||||
|
||||
async function handleSend(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || !friendId || sending) return;
|
||||
@@ -120,11 +145,17 @@ export default function Chat() {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
// Use visualViewport height when available (handles mobile keyboard),
|
||||
// subtract header height (3.5rem = 56px)
|
||||
const containerStyle: React.CSSProperties = viewportHeight
|
||||
? { height: viewportHeight - 56 }
|
||||
: { height: 'calc(100dvh - 3.5rem)' };
|
||||
|
||||
return (
|
||||
<PageLayout noPadding>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Chat header - sticky at top */}
|
||||
<div className="sticky top-0 z-10 flex items-center gap-3 border-b border-border bg-background px-4 py-3">
|
||||
<div className="flex flex-col" style={containerStyle}>
|
||||
{/* Chat header */}
|
||||
<div className="flex shrink-0 items-center gap-3 border-b border-border bg-background px-4 py-3">
|
||||
<Link to="/friends">
|
||||
<Button variant="ghost" size="sm">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
@@ -143,7 +174,7 @@ export default function Chat() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages - scrollable area fills remaining space */}
|
||||
{/* Messages */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
onScroll={handleScroll}
|
||||
@@ -186,8 +217,8 @@ export default function Chat() {
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input - pinned to bottom */}
|
||||
<form onSubmit={handleSend} className="flex gap-2 border-t border-border bg-background px-4 py-3">
|
||||
{/* Input */}
|
||||
<form onSubmit={handleSend} className="flex shrink-0 gap-2 border-t border-border bg-background px-4 py-3">
|
||||
<Input
|
||||
placeholder="Type a message..."
|
||||
value={input}
|
||||
|
||||
Reference in New Issue
Block a user