finalized auto scrolling 🏴‍☠️

This commit is contained in:
Nate Kelley 2025-04-10 12:27:22 -06:00
parent 55d2a1dc3c
commit 4344a5ee09
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
4 changed files with 51 additions and 48 deletions

View File

@ -14,9 +14,10 @@ const AutoScrollDemo = () => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [isAutoAddEnabled, setIsAutoAddEnabled] = useState(false); const [isAutoAddEnabled, setIsAutoAddEnabled] = useState(false);
const [enabled, setEnabled] = useState(false);
const intervalRef = useRef<NodeJS.Timeout>(); const intervalRef = useRef<NodeJS.Timeout>();
const { isAutoScrollEnabled, scrollToBottom, scrollToTop, enableAutoScroll, disableAutoScroll } = const { isAutoScrollEnabled, scrollToBottom, scrollToTop, enableAutoScroll, disableAutoScroll } =
useAutoScroll(containerRef); useAutoScroll(containerRef, { enabled, observeSubTree: true });
const addMessage = () => { const addMessage = () => {
const newMessage: Message = { const newMessage: Message = {
@ -80,6 +81,13 @@ const AutoScrollDemo = () => {
}`}> }`}>
Auto Add {isAutoAddEnabled ? 'ON' : 'OFF'} Auto Add {isAutoAddEnabled ? 'ON' : 'OFF'}
</button> </button>
<button
onClick={() => setEnabled(!enabled)}
className={`rounded px-4 py-2 text-white ${
enabled ? 'bg-green-500 hover:bg-green-600' : 'bg-red-500 hover:bg-red-600'
}`}>
Enabled {enabled ? 'ON' : 'OFF'}
</button>
</div> </div>
<div className="flex flex-wrap items-center gap-4"> <div className="flex flex-wrap items-center gap-4">
@ -210,7 +218,7 @@ export const ScrollAreaComponentWithAutoScroll: Story = {
scrollToTop, scrollToTop,
enableAutoScroll, enableAutoScroll,
disableAutoScroll disableAutoScroll
} = useAutoScroll(containerRef, { observeDeepChanges: true }); } = useAutoScroll(containerRef, {});
const addCard = useCallback(() => { const addCard = useCallback(() => {
setCards((prev) => [...prev, generateCard(prev.length + 1)]); setCards((prev) => [...prev, generateCard(prev.length + 1)]);
@ -309,7 +317,7 @@ export const RapidTextAppend: Story = {
const intervalRef = useRef<NodeJS.Timeout>(); const intervalRef = useRef<NodeJS.Timeout>();
const { isAutoScrollEnabled, enableAutoScroll, disableAutoScroll } = useAutoScroll( const { isAutoScrollEnabled, enableAutoScroll, disableAutoScroll } = useAutoScroll(
containerRef, containerRef,
{ observeDeepChanges: true } { observeSubTree: true, observeCharacterData: true, observeAttributes: false }
); );
const addWord = useCallback(() => { const addWord = useCallback(() => {

View File

@ -20,7 +20,9 @@ interface UseAutoScrollOptions {
* If true, the hook will observe changes to the container's content and scroll position. * If true, the hook will observe changes to the container's content and scroll position.
* If false, the hook will only observe changes to the container's scroll position. * If false, the hook will only observe changes to the container's scroll position.
*/ */
observeDeepChanges?: boolean; observeSubTree?: boolean;
observeCharacterData?: boolean;
observeAttributes?: boolean;
/** /**
* Duration in milliseconds to continue animations after a mutation. * Duration in milliseconds to continue animations after a mutation.
@ -72,7 +74,9 @@ export const useAutoScroll = (
enabled = true, enabled = true,
bottomThreshold = 50, bottomThreshold = 50,
chaseEasing = 0.2, chaseEasing = 0.2,
observeDeepChanges = true, observeSubTree = true,
observeCharacterData = false,
observeAttributes = false,
animationCooldown = 500 animationCooldown = 500
} = options; } = options;
@ -150,7 +154,8 @@ export const useAutoScroll = (
// Set up the mutation observer // Set up the mutation observer
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;
if (!container || !isAutoScrollEnabled || !enabled) return;
if (!container || !isAutoScrollEnabled) return;
// Clean up previous observer if it exists // Clean up previous observer if it exists
if (observerRef.current) { if (observerRef.current) {
@ -169,9 +174,9 @@ export const useAutoScroll = (
// Configure observer to watch for changes // Configure observer to watch for changes
const observerConfig = { const observerConfig = {
childList: true, childList: true,
subtree: observeDeepChanges, subtree: observeSubTree,
characterData: observeDeepChanges, characterData: observeCharacterData,
attributes: observeDeepChanges attributes: observeAttributes
}; };
// Start observing // Start observing
@ -201,19 +206,22 @@ export const useAutoScroll = (
isAutoScrollEnabled, isAutoScrollEnabled,
containerRef, containerRef,
startScrollAnimation, startScrollAnimation,
observeDeepChanges, observeSubTree,
observeCharacterData,
observeAttributes,
animationCooldown animationCooldown
]); ]);
// Listen for userinitiated events. Only disable autoscroll if the container isn't near the bottom. // Listen for userinitiated events. Only disable autoscroll if the container isn't near the bottom.
useEffect(() => { useEffect(() => {
const container = containerRef.current; const container = containerRef.current;
if (!container || !isAutoScrollEnabled || !enabled) return; if (!container || !isAutoScrollEnabled) return;
const disableAutoScrollHandler = () => { const disableAutoScrollHandler = () => {
// Only disable autoscroll if we're not near the bottom. // Only disable autoscroll if we're not near the bottom.
if (!isAtBottom(container, bottomThreshold)) { if (!isAtBottom(container, bottomThreshold)) {
setIsAutoScrollEnabled(false); setIsAutoScrollEnabled(false);
console.log('disableAutoScrollHandler', isAutoScrollEnabled, enabled);
// Stop any ongoing animations // Stop any ongoing animations
if (rAFIdRef.current) { if (rAFIdRef.current) {
@ -235,7 +243,7 @@ export const useAutoScroll = (
container.removeEventListener('touchstart', disableAutoScrollHandler); container.removeEventListener('touchstart', disableAutoScrollHandler);
container.removeEventListener('mousedown', disableAutoScrollHandler); container.removeEventListener('mousedown', disableAutoScrollHandler);
}; };
}, [containerRef, bottomThreshold, enabled]); }, [containerRef, bottomThreshold, isAutoScrollEnabled]);
// Listen for scroll events. If the user scrolls back close to the bottom, re-enable autoscroll. // Listen for scroll events. If the user scrolls back close to the bottom, re-enable autoscroll.
useEffect(() => { useEffect(() => {
@ -252,11 +260,7 @@ export const useAutoScroll = (
return () => { return () => {
container.removeEventListener('scroll', onScroll); container.removeEventListener('scroll', onScroll);
}; };
}, [containerRef, bottomThreshold]); }, [containerRef, isAutoScrollEnabled, bottomThreshold]);
useEffect(() => {
setIsAutoScrollEnabled(enabled);
}, [enabled]);
// Exposed functions. // Exposed functions.

View File

@ -13,26 +13,20 @@ const autoClass = 'mx-auto max-w-[600px] w-full';
export const ChatContent: React.FC<{}> = React.memo(() => { export const ChatContent: React.FC<{}> = React.memo(() => {
const chatId = useChatIndividualContextSelector((state) => state.chatId); const chatId = useChatIndividualContextSelector((state) => state.chatId);
const chatMessageIds = useChatIndividualContextSelector((state) => state.chatMessageIds); const chatMessageIds = useChatIndividualContextSelector((state) => state.chatMessageIds);
const containerRef = useRef<HTMLElement | null>(null); const containerRef = useRef<HTMLElement | null>(null);
const [autoMessages, setAutoMessages] = useState<string[]>([]);
const { isAutoScrollEnabled, scrollToBottom } = useAutoScroll(containerRef, { const { isAutoScrollEnabled, scrollToBottom, enableAutoScroll } = useAutoScroll(containerRef, {
observeDeepChanges: true observeSubTree: true,
enabled: false
}); });
useEffect(() => { useEffect(() => {
const container = document.getElementById(CHAT_CONTENT_CONTAINER_ID); const container = document.getElementById(CHAT_CONTENT_CONTAINER_ID);
if (!container) return; if (!container) return;
console.log('ADD IN A TODO ABOUT IS COMPLETED STREAM');
containerRef.current = container; containerRef.current = container;
enableAutoScroll();
setInterval(() => {
setAutoMessages((prev) => [...prev, 'This is a test ' + prev.length]);
}, 22220);
}, []); }, []);
console.log('isAutoScrollEnabled', isAutoScrollEnabled);
return ( return (
<> <>
<div className="mb-40 flex h-full w-full flex-col overflow-hidden"> <div className="mb-40 flex h-full w-full flex-col overflow-hidden">
@ -41,14 +35,6 @@ export const ChatContent: React.FC<{}> = React.memo(() => {
<ChatMessageBlock key={messageId} messageId={messageId} chatId={chatId || ''} /> <ChatMessageBlock key={messageId} messageId={messageId} chatId={chatId || ''} />
</div> </div>
))} ))}
<div className="mx-2 flex flex-wrap gap-1 overflow-hidden">
{autoMessages.map((message, index) => (
<span key={index} className="w-fit rounded border p-0.5 text-red-700">
{message}
</span>
))}
</div>
</div> </div>
<ChatInputWrapper> <ChatInputWrapper>

View File

@ -9,18 +9,23 @@ export const ChatScrollToBottom: React.FC<{
scrollToBottom: ReturnType<typeof useAutoScroll>['scrollToBottom']; scrollToBottom: ReturnType<typeof useAutoScroll>['scrollToBottom'];
}> = React.memo(({ isAutoScrollEnabled, scrollToBottom }) => { }> = React.memo(({ isAutoScrollEnabled, scrollToBottom }) => {
return ( return (
<AppTooltip title="Stick to bottom"> <div
<button
onClick={scrollToBottom}
className={cn( className={cn(
'bg-background/90 hover:bg-item-hover/90 absolute -top-9 right-3 z-10 rounded-full border p-2 shadow transition-all duration-300 hover:scale-105 hover:shadow-md', 'absolute -top-9 right-3 z-10',
isAutoScrollEnabled isAutoScrollEnabled
? 'pointer-events-none scale-90 opacity-0' ? 'pointer-events-none scale-90 opacity-0'
: 'pointer-events-auto scale-100 cursor-pointer opacity-100' : 'pointer-events-auto scale-100 cursor-pointer opacity-100'
)}> )}>
<AppTooltip title="Stick to bottom" sideOffset={12} delayDuration={500}>
<button
onClick={scrollToBottom}
className={
'bg-background/90 hover:bg-item-hover/90 cursor-pointer rounded-full border p-2 shadow transition-all duration-300 hover:scale-105 hover:shadow-md'
}>
<ChevronDown /> <ChevronDown />
</button> </button>
</AppTooltip> </AppTooltip>
</div>
); );
}); });