fix(chat): keep sent messages in view

This commit is contained in:
2026-07-15 19:14:36 +08:00
parent 47876484fc
commit cf239a51de
8 changed files with 122 additions and 1 deletions
@@ -145,6 +145,51 @@ describe("ChatArea scrolling", () => {
expect(scrollNode.scrollTop).toBe(100);
});
it("forces the latest user message into view after the keyboard resizes", () => {
scrollHeight = 1_000;
clientHeight = 300;
const history = [createMessage("history-1")];
renderChatArea(root, history, true, { scrollToBottomSignal: 0 });
const scrollNode = getScrollNode(container);
clientHeight = 200;
scrollNode.scrollTop = 700;
act(() => scrollNode.dispatchEvent(new Event("scroll", { bubbles: true })));
scrollHeight = 1_200;
renderChatArea(
root,
[...history, createUserMessage("sent-1")],
true,
{ scrollToBottomSignal: 1 },
);
expect(scrollNode.scrollTop).toBe(1_000);
});
it("scrolls below a promotion after the user sends a message", () => {
scrollHeight = 1_000;
clientHeight = 300;
const history = [createMessage("history-1")];
const promotion = createMessage("promotion-1");
renderChatArea(root, [...history, promotion], true, {
scrollToBottomSignal: 0,
});
const scrollNode = getScrollNode(container);
scrollNode.scrollTop = 100;
act(() => scrollNode.dispatchEvent(new Event("scroll", { bubbles: true })));
scrollHeight = 1_500;
renderChatArea(
root,
[...history, createUserMessage("sent-1"), promotion],
true,
{ scrollToBottomSignal: 1 },
);
expect(scrollNode.scrollTop).toBe(1_200);
});
it("preserves the visible message after older history is prepended", () => {
scrollHeight = 1_000;
clientHeight = 300;
@@ -184,6 +229,7 @@ function renderChatArea(
canLoadMoreHistory?: boolean;
isLoadingMoreHistory?: boolean;
onLoadMoreHistory?: () => void;
scrollToBottomSignal?: number;
} = {},
): void {
act(() => {
@@ -227,6 +273,15 @@ function createMessage(id: string): UiMessage {
};
}
function createUserMessage(id: string): UiMessage {
return {
id,
content: `Message ${id}`,
isFromAI: false,
date: "2026-07-15",
};
}
function getScrollNode(container: HTMLElement): HTMLElement {
const node = container.querySelector<HTMLElement>(
'[aria-label="Chat messages"]',