Fix useScrollToBottom hook for rapid content changes (#7715)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
tofarr 2025-04-05 18:05:53 -06:00 committed by GitHub
parent 0ab9d97f2d
commit c71ef11a25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 59 additions and 21 deletions

View File

@ -127,7 +127,7 @@ export function ChatInterface() {
<div
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2 fast-smooth-scroll"
>
{isLoadingMessages && (
<div className="flex justify-center">

View File

@ -20,7 +20,7 @@ export function JupyterEditor({ maxWidth }: JupyterEditorProps) {
<div className="flex-1 h-full flex flex-col" style={{ maxWidth }}>
<div
data-testid="jupyter-container"
className="flex-1 overflow-y-auto"
className="flex-1 overflow-y-auto fast-smooth-scroll"
ref={jupyterRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
>

View File

@ -131,7 +131,10 @@ function SecurityInvariant() {
{t(I18nKey.INVARIANT$EXPORT_TRACE_LABEL)}
</Button>
</div>
<div className="flex-1 p-4 max-h-screen overflow-y-auto" ref={logsRef}>
<div
className="flex-1 p-4 max-h-screen overflow-y-auto fast-smooth-scroll"
ref={logsRef}
>
{logs.map((log: SecurityAnalyzerLog, index: number) => (
<div
key={index}

View File

@ -1,41 +1,70 @@
import { RefObject, useEffect, useState } from "react";
import { RefObject, useEffect, useState, useCallback } from "react";
export function useScrollToBottom(scrollRef: RefObject<HTMLDivElement | null>) {
// for auto-scroll
// Track whether we should auto-scroll to the bottom when content changes
const [shouldScrollToBottom, setShouldScrollToBottom] = useState(true);
const [autoScroll, setAutoScroll] = useState(true);
// Track whether the user is currently at the bottom of the scroll area
const [hitBottom, setHitBottom] = useState(true);
const onChatBodyScroll = (e: HTMLElement) => {
const bottomHeight = e.scrollTop + e.clientHeight;
// Check if the scroll position is at the bottom
const isAtBottom = useCallback((element: HTMLElement): boolean => {
const bottomThreshold = 10; // Pixels from bottom to consider "at bottom"
const bottomPosition = element.scrollTop + element.clientHeight;
return bottomPosition >= element.scrollHeight - bottomThreshold;
}, []);
const isHitBottom = bottomHeight >= e.scrollHeight - 10;
// Handle scroll events
const onChatBodyScroll = useCallback(
(e: HTMLElement) => {
const isCurrentlyAtBottom = isAtBottom(e);
setHitBottom(isCurrentlyAtBottom);
setHitBottom(isHitBottom);
setAutoScroll(isHitBottom);
};
// Only update shouldScrollToBottom when user manually scrolls
// This prevents content changes from affecting our scroll behavior decision
setShouldScrollToBottom(isCurrentlyAtBottom);
},
[isAtBottom],
);
function scrollDomToBottom() {
// Scroll to bottom function with animation
const scrollDomToBottom = useCallback(() => {
const dom = scrollRef.current;
if (dom) {
requestAnimationFrame(() => {
setAutoScroll(true);
dom.scrollTo({ top: dom.scrollHeight, behavior: "auto" });
// Set shouldScrollToBottom to true when manually scrolling to bottom
setShouldScrollToBottom(true);
setHitBottom(true);
// Use smooth scrolling but with a fast duration
dom.scrollTo({
top: dom.scrollHeight,
behavior: "smooth",
});
});
}
}
}, [scrollRef]);
// auto scroll
// Auto-scroll effect that runs when content changes
useEffect(() => {
if (autoScroll) {
scrollDomToBottom();
// Only auto-scroll if the user was already at the bottom
if (shouldScrollToBottom) {
const dom = scrollRef.current;
if (dom) {
requestAnimationFrame(() => {
dom.scrollTo({
top: dom.scrollHeight,
behavior: "smooth",
});
});
}
}
});
return {
scrollRef,
autoScroll,
setAutoScroll,
autoScroll: shouldScrollToBottom,
setAutoScroll: setShouldScrollToBottom,
scrollDomToBottom,
hitBottom,
setHitBottom,

View File

@ -57,3 +57,9 @@ code {
.markdown-body td {
padding: 0.1rem 1rem;
}
/* Fast smooth scrolling for chat interface */
.fast-smooth-scroll {
scroll-behavior: smooth;
scroll-timeline: 100ms;
}