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 [replyTo, setReplyTo] = useState<{ id: string; username: string; content: string } | null>(null);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const [showScrollDown, setShowScrollDown] = useState(false);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const prevMsgCountRef = useRef(0);
|
||||
const activeChannelRef = useRef(activeChannelId);
|
||||
@@ -97,14 +98,36 @@ export default function App() {
|
||||
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
|
||||
useEffect(() => {
|
||||
if (messages.length > prevMsgCountRef.current && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
if (messages.length > prevMsgCountRef.current) {
|
||||
scrollToBottom();
|
||||
}
|
||||
prevMsgCountRef.current = messages.length;
|
||||
}, [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 activeChannel = channels.find((c) => c.id === activeChannelId);
|
||||
|
||||
@@ -208,6 +231,19 @@ export default function App() {
|
||||
)}
|
||||
</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 && (
|
||||
<ComposeBox
|
||||
key={activeChannelId}
|
||||
@@ -216,14 +252,7 @@ export default function App() {
|
||||
onClearReply={() => setReplyTo(null)}
|
||||
onMessageSent={() => {
|
||||
loadMessages();
|
||||
// Force scroll to bottom after sending
|
||||
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);
|
||||
setTimeout(() => scrollToBottom(), 100);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user