scroll-to-bottom ↓ button above compose when scrolled up
- Appears when >150px from bottom - Smooth scrolls on click - Sending a message auto-scrolls to bottom - Proper scroll tracking via viewport event listener Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ export default function App() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [replyTo, setReplyTo] = useState<{ id: string; username: string; content: string } | null>(null);
|
const [replyTo, setReplyTo] = useState<{ id: string; username: string; content: string } | null>(null);
|
||||||
const [sheetOpen, setSheetOpen] = useState(false);
|
const [sheetOpen, setSheetOpen] = useState(false);
|
||||||
|
const [showScrollDown, setShowScrollDown] = useState(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const prevMsgCountRef = useRef(0);
|
const prevMsgCountRef = useRef(0);
|
||||||
const activeChannelRef = useRef(activeChannelId);
|
const activeChannelRef = useRef(activeChannelId);
|
||||||
@@ -97,14 +98,36 @@ export default function App() {
|
|||||||
loadMessages();
|
loadMessages();
|
||||||
}, [activeChannelId, loadMessages]);
|
}, [activeChannelId, loadMessages]);
|
||||||
|
|
||||||
|
function getViewport() {
|
||||||
|
const el = scrollRef.current as unknown as HTMLElement | null;
|
||||||
|
return el?.querySelector('[data-slot="scroll-area-viewport"]') as HTMLElement | null ?? el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom(smooth = false) {
|
||||||
|
const vp = getViewport();
|
||||||
|
if (vp) vp.scrollTo({ top: vp.scrollHeight, behavior: smooth ? "smooth" : "instant" });
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-scroll only on new messages
|
// Auto-scroll only on new messages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length > prevMsgCountRef.current && scrollRef.current) {
|
if (messages.length > prevMsgCountRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
prevMsgCountRef.current = messages.length;
|
prevMsgCountRef.current = messages.length;
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// Track scroll position for scroll-down button
|
||||||
|
useEffect(() => {
|
||||||
|
const vp = getViewport();
|
||||||
|
if (!vp) return;
|
||||||
|
function onScroll() {
|
||||||
|
const v = vp!;
|
||||||
|
setShowScrollDown(v.scrollHeight - v.scrollTop - v.clientHeight > 150);
|
||||||
|
}
|
||||||
|
vp.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
return () => vp.removeEventListener("scroll", onScroll);
|
||||||
|
});
|
||||||
|
|
||||||
const messagesById = new Map(messages.map((m) => [m.id, m]));
|
const messagesById = new Map(messages.map((m) => [m.id, m]));
|
||||||
const activeChannel = channels.find((c) => c.id === activeChannelId);
|
const activeChannel = channels.find((c) => c.id === activeChannelId);
|
||||||
|
|
||||||
@@ -208,6 +231,19 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Scroll-to-bottom button */}
|
||||||
|
{showScrollDown && (
|
||||||
|
<div className="flex justify-center -mt-6 mb-1 relative z-10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => scrollToBottom(true)}
|
||||||
|
className="w-8 h-8 flex items-center justify-center border-2 border-border bg-card text-muted-foreground hover:text-primary hover:border-primary transition-colors shadow-lg text-sm"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeChannelId && (
|
{activeChannelId && (
|
||||||
<ComposeBox
|
<ComposeBox
|
||||||
key={activeChannelId}
|
key={activeChannelId}
|
||||||
@@ -216,14 +252,7 @@ export default function App() {
|
|||||||
onClearReply={() => setReplyTo(null)}
|
onClearReply={() => setReplyTo(null)}
|
||||||
onMessageSent={() => {
|
onMessageSent={() => {
|
||||||
loadMessages();
|
loadMessages();
|
||||||
// Force scroll to bottom after sending
|
setTimeout(() => scrollToBottom(), 100);
|
||||||
setTimeout(() => {
|
|
||||||
if (scrollRef.current) {
|
|
||||||
const el = scrollRef.current as unknown as HTMLElement;
|
|
||||||
const viewport = el.querySelector('[data-slot="scroll-area-viewport"]') || el;
|
|
||||||
viewport.scrollTop = viewport.scrollHeight;
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user