Skip to content

Commit 5019927

Browse files
committed
refactor(bubble)!: rewrite bubble auto scroll logic
1 parent 139eb51 commit 5019927

File tree

4 files changed

+93
-15
lines changed

4 files changed

+93
-15
lines changed

.changes/add-inputcount-component.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44

55
Add `InputCount` component.
66

7-
-Remove the input count div container and wrap it into a new component, allowing users to customize the input limit.
8-
-Remove the justify-center style from the div container and add ml-auto to the SenderButton to ensure button remains on the right side.
7+
- Remove the input count div container and wrap it into a new component, allowing users to customize the input limit.
8+
- Remove the justify-center style from the div container and add ml-auto to the SenderButton to ensure button remains on the right side.

.changes/refactor-bubble-list.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@matechat/react": patch:refactor
3+
---
4+
5+
Rewrite auto scroll logic of `BubbleList` component:
6+
7+
- Use `ResizeObserver` to detect content size changes and scroll accordingly.
8+
- Add `scrollContainer` method to scroll to bottom when content size changes.
9+
- Introduce `pauseScroll` to prevent unnecessary scrolls during updates.

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
- [`6a494c2`](https://github.com/DevCloudFE/matechat-react/commit/6a494c2e4e4c117c404e42e362e4b9a3535aa62e) ([#47](https://github.com/DevCloudFE/matechat-react/pull/47) by [@xx-yoke](https://github.com/DevCloudFE/matechat-react/../../xx-yoke)) Add `InputCount` component.
88

9-
\-Remove the input count div container and wrap it into a new component, allowing users to customize the input limit.
10-
\-Remove the justify-center style from the div container and add ml-auto to the SenderButton to ensure button remains on the right side.
9+
- Remove the input count div container and wrap it into a new component, allowing users to customize the input limit.
10+
- Remove the justify-center style from the div container and add ml-auto to the SenderButton to ensure button remains on the right side.
1111

1212
## \[0.1.0-alpha.7]
1313

src/bubble.tsx

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import "./tailwind.css";
33

44
import clsx from "clsx";
55
import type React from "react";
6-
import { useEffect, useRef, useState } from "react";
6+
import { useCallback, useEffect, useRef, useState } from "react";
77
import Markdown from "react-markdown";
88
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
99
import {
@@ -240,6 +240,11 @@ export interface BubbleListProps extends React.ComponentProps<"div"> {
240240
};
241241
footer?: React.ReactNode;
242242
pending?: React.ReactNode;
243+
/**
244+
* The height threshold for triggering scroll behavior.
245+
* @default 8
246+
*/
247+
threshold?: number;
243248
}
244249

245250
export function BubbleList({
@@ -254,32 +259,97 @@ export function BubbleList({
254259
align: "left",
255260
},
256261
isPending = true,
262+
messages,
263+
threshold = 8,
257264
...props
258265
}: BubbleListProps) {
259-
const { messages } = props;
260-
const lastMessageRef = useRef<HTMLDivElement>(null);
266+
const containerRef = useRef<HTMLDivElement>(null);
267+
const contentRef = useRef<HTMLDivElement>(null);
268+
269+
const pauseScroll = useRef<boolean>(false);
270+
const contentRect = useRef<DOMRect>(new DOMRect());
271+
272+
const scrollContainer = useCallback((smooth?: boolean) => {
273+
if (pauseScroll.current) return;
274+
275+
containerRef.current?.scrollTo({
276+
top: containerRef.current?.scrollHeight,
277+
behavior: smooth === false ? "instant" : "smooth",
278+
});
279+
}, []);
261280

262-
// biome-ignore lint/correctness/useExhaustiveDependencies: This effect runs only when messages change
263281
useEffect(() => {
264-
if (lastMessageRef.current) {
265-
lastMessageRef.current.scrollIntoView({
266-
behavior: "smooth",
267-
block: "end",
268-
});
282+
if (!containerRef.current || !contentRef.current) return;
283+
284+
const observer = new ResizeObserver((entries) => {
285+
for (const entry of entries) {
286+
const { height, width } = entry.contentRect;
287+
if (
288+
Math.abs(contentRect.current.height - height) > threshold ||
289+
Math.abs(contentRect.current.width - width) > threshold
290+
) {
291+
contentRect.current = entry.contentRect;
292+
scrollContainer();
293+
}
294+
}
295+
});
296+
297+
observer.observe(containerRef.current);
298+
observer.observe(contentRef.current);
299+
300+
return () => observer.disconnect();
301+
}, [scrollContainer, threshold]);
302+
303+
const isScrollAtBottom = useCallback(() => {
304+
const container = containerRef.current;
305+
if (!container) return true;
306+
307+
return (
308+
Math.abs(
309+
container.scrollTop + container.clientHeight - container.scrollHeight,
310+
) < threshold
311+
);
312+
}, [threshold]);
313+
314+
const handleWheel = useCallback(() => {
315+
const container = containerRef.current;
316+
if (!container) return;
317+
318+
if (isScrollAtBottom()) {
319+
pauseScroll.current = false;
320+
} else {
321+
pauseScroll.current = true;
269322
}
270-
}, [messages, isPending]);
323+
}, [isScrollAtBottom]);
271324

272325
return (
273326
<div
274327
data-slot="bubble-list"
275328
className={twMerge(
276329
clsx("flex flex-col overflow-y-auto flex-1 gap-4", className),
277330
)}
331+
ref={containerRef}
332+
onWheel={handleWheel}
333+
onTouchStart={() => {
334+
pauseScroll.current = true;
335+
}}
336+
onTouchEnd={() => {
337+
if (isScrollAtBottom()) {
338+
pauseScroll.current = false;
339+
scrollContainer(false);
340+
} else {
341+
pauseScroll.current = true;
342+
}
343+
}}
344+
onTouchMove={() => {
345+
pauseScroll.current = true;
346+
}}
278347
{...props}
279348
>
280349
<div
281350
data-slot="bubble-items"
282351
className="flex flex-col max-w-full flex-1 gap-4"
352+
ref={contentRef}
283353
>
284354
{messages.map((message, index) => (
285355
<div
@@ -310,7 +380,6 @@ export function BubbleList({
310380
? "solid"
311381
: "transparent"
312382
}
313-
ref={index === messages.length - 1 ? lastMessageRef : undefined}
314383
/>
315384
</div>
316385
))}

0 commit comments

Comments
 (0)