fix: scroll behavior, browser back navigation, and weather widget sizing (#1336)
This commit is contained in:
parent
e90a6ee209
commit
b3a155d2a4
6 changed files with 157 additions and 121 deletions
|
|
@ -1,102 +1,119 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
type ScrollFlag = ScrollBehavior | false;
|
||||
|
||||
export function useScrollToBottom() {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||
const isAtBottomRef = useRef(true);
|
||||
const isUserScrollingRef = useRef(false);
|
||||
|
||||
const { data: scrollBehavior = false, mutate: setScrollBehavior } =
|
||||
useSWR<ScrollFlag>("messages:should-scroll", null, { fallbackData: false });
|
||||
// Keep ref in sync with state
|
||||
useEffect(() => {
|
||||
isAtBottomRef.current = isAtBottom;
|
||||
}, [isAtBottom]);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
const checkIfAtBottom = useCallback(() => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||
|
||||
// Check if we are within 100px of the bottom (like v0 does)
|
||||
setIsAtBottom(scrollTop + clientHeight >= scrollHeight - 100);
|
||||
return scrollTop + clientHeight >= scrollHeight - 100;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = containerRef.current;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
handleScroll();
|
||||
});
|
||||
containerRef.current.scrollTo({
|
||||
top: containerRef.current.scrollHeight,
|
||||
behavior,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const mutationObserver = new MutationObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
handleScroll();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
resizeObserver.observe(container);
|
||||
mutationObserver.observe(container, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["style", "class", "data-state"],
|
||||
});
|
||||
|
||||
handleScroll();
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
mutationObserver.disconnect();
|
||||
};
|
||||
}, [handleScroll]);
|
||||
|
||||
// Handle user scroll events
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.addEventListener("scroll", handleScroll);
|
||||
handleScroll(); // Check initial state
|
||||
let scrollTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const handleScroll = () => {
|
||||
// Mark as user scrolling
|
||||
isUserScrollingRef.current = true;
|
||||
clearTimeout(scrollTimeout);
|
||||
|
||||
// Update isAtBottom state
|
||||
const atBottom = checkIfAtBottom();
|
||||
setIsAtBottom(atBottom);
|
||||
isAtBottomRef.current = atBottom;
|
||||
|
||||
// Reset user scrolling flag after scroll ends
|
||||
scrollTimeout = setTimeout(() => {
|
||||
isUserScrollingRef.current = false;
|
||||
}, 150);
|
||||
};
|
||||
|
||||
container.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => {
|
||||
container.removeEventListener("scroll", handleScroll);
|
||||
clearTimeout(scrollTimeout);
|
||||
};
|
||||
}, [handleScroll]);
|
||||
}, [checkIfAtBottom]);
|
||||
|
||||
// Auto-scroll when content changes
|
||||
useEffect(() => {
|
||||
if (scrollBehavior && containerRef.current) {
|
||||
const container = containerRef.current;
|
||||
const scrollOptions: ScrollToOptions = {
|
||||
top: container.scrollHeight,
|
||||
behavior: scrollBehavior,
|
||||
};
|
||||
container.scrollTo(scrollOptions);
|
||||
setScrollBehavior(false);
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
}, [scrollBehavior, setScrollBehavior]);
|
||||
|
||||
const scrollToBottom = useCallback(
|
||||
(currentScrollBehavior: ScrollBehavior = "smooth") => {
|
||||
setScrollBehavior(currentScrollBehavior);
|
||||
},
|
||||
[setScrollBehavior]
|
||||
);
|
||||
const scrollIfNeeded = () => {
|
||||
// Only auto-scroll if user was at bottom and isn't actively scrolling
|
||||
if (isAtBottomRef.current && !isUserScrollingRef.current) {
|
||||
requestAnimationFrame(() => {
|
||||
container.scrollTo({
|
||||
top: container.scrollHeight,
|
||||
behavior: "instant",
|
||||
});
|
||||
setIsAtBottom(true);
|
||||
isAtBottomRef.current = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for DOM changes
|
||||
const mutationObserver = new MutationObserver(scrollIfNeeded);
|
||||
mutationObserver.observe(container, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
});
|
||||
|
||||
// Watch for size changes
|
||||
const resizeObserver = new ResizeObserver(scrollIfNeeded);
|
||||
resizeObserver.observe(container);
|
||||
|
||||
// Also observe children for size changes
|
||||
for (const child of container.children) {
|
||||
resizeObserver.observe(child);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
function onViewportEnter() {
|
||||
setIsAtBottom(true);
|
||||
isAtBottomRef.current = true;
|
||||
}
|
||||
|
||||
function onViewportLeave() {
|
||||
setIsAtBottom(false);
|
||||
isAtBottomRef.current = false;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue