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 [messages, setMessages] = useState<Message[]>([]);
const [isAutoAddEnabled, setIsAutoAddEnabled] = useState(false);
const [enabled, setEnabled] = useState(false);
const intervalRef = useRef<NodeJS.Timeout>();
const { isAutoScrollEnabled, scrollToBottom, scrollToTop, enableAutoScroll, disableAutoScroll } =
useAutoScroll(containerRef);
useAutoScroll(containerRef, { enabled, observeSubTree: true });
const addMessage = () => {
const newMessage: Message = {
@ -80,6 +81,13 @@ const AutoScrollDemo = () => {
}`}>
Auto Add {isAutoAddEnabled ? 'ON' : 'OFF'}
</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 className="flex flex-wrap items-center gap-4">
@ -210,7 +218,7 @@ export const ScrollAreaComponentWithAutoScroll: Story = {
scrollToTop,
enableAutoScroll,
disableAutoScroll
} = useAutoScroll(containerRef, { observeDeepChanges: true });
} = useAutoScroll(containerRef, {});
const addCard = useCallback(() => {
setCards((prev) => [...prev, generateCard(prev.length + 1)]);
@ -309,7 +317,7 @@ export const RapidTextAppend: Story = {
const intervalRef = useRef<NodeJS.Timeout>();
const { isAutoScrollEnabled, enableAutoScroll, disableAutoScroll } = useAutoScroll(
containerRef,
{ observeDeepChanges: true }
{ observeSubTree: true, observeCharacterData: true, observeAttributes: false }
);
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 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.
@ -72,7 +74,9 @@ export const useAutoScroll = (
enabled = true,
bottomThreshold = 50,
chaseEasing = 0.2,
observeDeepChanges = true,
observeSubTree = true,
observeCharacterData = false,
observeAttributes = false,
animationCooldown = 500
} = options;
@ -150,7 +154,8 @@ export const useAutoScroll = (
// Set up the mutation observer
useEffect(() => {
const container = containerRef.current;
if (!container || !isAutoScrollEnabled || !enabled) return;
if (!container || !isAutoScrollEnabled) return;
// Clean up previous observer if it exists
if (observerRef.current) {
@ -169,9 +174,9 @@ export const useAutoScroll = (
// Configure observer to watch for changes
const observerConfig = {
childList: true,
subtree: observeDeepChanges,
characterData: observeDeepChanges,
attributes: observeDeepChanges
subtree: observeSubTree,
characterData: observeCharacterData,
attributes: observeAttributes
};
// Start observing
@ -201,19 +206,22 @@ export const useAutoScroll = (
isAutoScrollEnabled,
containerRef,
startScrollAnimation,
observeDeepChanges,
observeSubTree,
observeCharacterData,
observeAttributes,
animationCooldown
]);
// Listen for userinitiated events. Only disable autoscroll if the container isn't near the bottom.
useEffect(() => {
const container = containerRef.current;
if (!container || !isAutoScrollEnabled || !enabled) return;
if (!container || !isAutoScrollEnabled) return;
const disableAutoScrollHandler = () => {
// Only disable autoscroll if we're not near the bottom.
if (!isAtBottom(container, bottomThreshold)) {
setIsAutoScrollEnabled(false);
console.log('disableAutoScrollHandler', isAutoScrollEnabled, enabled);
// Stop any ongoing animations
if (rAFIdRef.current) {
@ -235,7 +243,7 @@ export const useAutoScroll = (
container.removeEventListener('touchstart', 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.
useEffect(() => {
@ -252,11 +260,7 @@ export const useAutoScroll = (
return () => {
container.removeEventListener('scroll', onScroll);
};
}, [containerRef, bottomThreshold]);
useEffect(() => {
setIsAutoScrollEnabled(enabled);
}, [enabled]);
}, [containerRef, isAutoScrollEnabled, bottomThreshold]);
// Exposed functions.

View File

@ -13,26 +13,20 @@ const autoClass = 'mx-auto max-w-[600px] w-full';
export const ChatContent: React.FC<{}> = React.memo(() => {
const chatId = useChatIndividualContextSelector((state) => state.chatId);
const chatMessageIds = useChatIndividualContextSelector((state) => state.chatMessageIds);
const containerRef = useRef<HTMLElement | null>(null);
const [autoMessages, setAutoMessages] = useState<string[]>([]);
const { isAutoScrollEnabled, scrollToBottom } = useAutoScroll(containerRef, {
observeDeepChanges: true
const { isAutoScrollEnabled, scrollToBottom, enableAutoScroll } = useAutoScroll(containerRef, {
observeSubTree: true,
enabled: false
});
useEffect(() => {
const container = document.getElementById(CHAT_CONTENT_CONTAINER_ID);
if (!container) return;
console.log('ADD IN A TODO ABOUT IS COMPLETED STREAM');
containerRef.current = container;
setInterval(() => {
setAutoMessages((prev) => [...prev, 'This is a test ' + prev.length]);
}, 22220);
enableAutoScroll();
}, []);
console.log('isAutoScrollEnabled', isAutoScrollEnabled);
return (
<>
<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 || ''} />
</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>
<ChatInputWrapper>

View File

@ -9,18 +9,23 @@ export const ChatScrollToBottom: React.FC<{
scrollToBottom: ReturnType<typeof useAutoScroll>['scrollToBottom'];
}> = React.memo(({ isAutoScrollEnabled, scrollToBottom }) => {
return (
<AppTooltip title="Stick to bottom">
<button
onClick={scrollToBottom}
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',
isAutoScrollEnabled
? 'pointer-events-none scale-90 opacity-0'
: 'pointer-events-auto scale-100 cursor-pointer opacity-100'
)}>
<ChevronDown />
</button>
</AppTooltip>
<div
className={cn(
'absolute -top-9 right-3 z-10',
isAutoScrollEnabled
? 'pointer-events-none scale-90 opacity-0'
: '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 />
</button>
</AppTooltip>
</div>
);
});