feat: Better autoscroll - for #909 (#1054)

This commit is contained in:
Alex Hancock
2025-02-03 16:03:47 -05:00
committed by GitHub
parent 26b494b9f7
commit a0a7a5b984
3 changed files with 87 additions and 46 deletions

View File

@@ -4,7 +4,7 @@ import GooseLogo from './GooseLogo';
const LoadingGoose = () => {
return (
<div className="w-full pb-[2px]">
<div className="flex items-center text-xs text-textStandard mb-2 pl-4 animate-[appear_250ms_ease-in_forwards]">
<div className="flex items-center text-xs text-textStandard mb-2 mt-2 pl-4 animate-[appear_250ms_ease-in_forwards]">
<GooseLogo className="mr-2" size="small" hover={false} />
goose is working on it..
</div>

View File

@@ -3,22 +3,86 @@ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '../../utils';
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
export interface ScrollAreaHandle {
scrollToBottom: () => void;
}
interface ScrollAreaProps extends React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> {
autoScroll?: boolean;
}
const ScrollArea = React.forwardRef<ScrollAreaHandle, ScrollAreaProps>(
({ className, children, autoScroll = false, ...props }, ref) => {
const rootRef = React.useRef<React.ElementRef<typeof ScrollAreaPrimitive.Root>>(null);
const viewportRef = React.useRef<HTMLDivElement>(null);
const viewportEndRef = React.useRef<HTMLDivElement>(null);
const [isFollowing, setIsFollowing] = React.useState(true);
const scrollToBottom = React.useCallback(() => {
if (viewportEndRef.current) {
viewportEndRef.current.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest',
});
setIsFollowing(true);
}
}, []);
// Expose the scrollToBottom method to parent components
React.useImperativeHandle(
ref,
() => ({
scrollToBottom,
}),
[scrollToBottom]
);
// Handle scroll events to update isFollowing state
const handleScroll = React.useCallback(() => {
if (!viewportRef.current) return;
const viewport = viewportRef.current;
const { scrollHeight, scrollTop, clientHeight } = viewport;
const scrollBottom = scrollTop + clientHeight;
const newIsFollowing = scrollHeight === scrollBottom;
// react will internally optimize this to not re-store the same values
setIsFollowing(newIsFollowing);
}, []);
React.useEffect(() => {
if (!autoScroll || !isFollowing) return;
scrollToBottom();
}, [children, autoScroll, isFollowing, scrollToBottom]);
// Add scroll event listener
React.useEffect(() => {
const viewport = viewportRef.current;
if (!viewport) return;
viewport.addEventListener('scroll', handleScroll);
return () => viewport.removeEventListener('scroll', handleScroll);
}, [handleScroll]);
return (
<ScrollAreaPrimitive.Root
ref={rootRef}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport ref={viewportRef} className="h-full w-full rounded-[inherit]">
{children}
{autoScroll && <div ref={viewportEndRef} style={{ height: '1px' }} />}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
);
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<