fix(webui): avoid mobile welcome composer overlap
This commit is contained in:
@@ -532,7 +532,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
|||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"thread-viewport-scrollbar absolute inset-0 scroll-auto scrollbar-thin",
|
"thread-viewport-scrollbar absolute inset-0 scroll-auto scrollbar-thin",
|
||||||
hasMessages && hasVerticalOverflow ? "overflow-y-auto" : "overflow-hidden",
|
hasVerticalOverflow ? "overflow-y-auto" : "overflow-hidden",
|
||||||
"[&::-webkit-scrollbar]:w-1.5",
|
"[&::-webkit-scrollbar]:w-1.5",
|
||||||
"[&::-webkit-scrollbar-thumb]:rounded-full",
|
"[&::-webkit-scrollbar-thumb]:rounded-full",
|
||||||
"[&::-webkit-scrollbar-thumb]:bg-muted-foreground/30",
|
"[&::-webkit-scrollbar-thumb]:bg-muted-foreground/30",
|
||||||
@@ -574,8 +574,11 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
|||||||
) : (
|
) : (
|
||||||
<div ref={contentRef} className="mx-auto flex min-h-full w-full max-w-[72rem] flex-col px-3 sm:px-4">
|
<div ref={contentRef} className="mx-auto flex min-h-full w-full max-w-[72rem] flex-col px-3 sm:px-4">
|
||||||
<div className="flex w-full flex-1 flex-col items-center pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-6 sm:justify-center sm:py-12">
|
<div className="flex w-full flex-1 flex-col items-center pb-[calc(0.75rem+env(safe-area-inset-bottom))] pt-6 sm:justify-center sm:py-12">
|
||||||
<div className="relative flex w-full max-w-[58rem] flex-1 flex-col items-center justify-end gap-5 sm:block sm:flex-none">
|
<div
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center sm:pointer-events-auto sm:static sm:block">
|
data-testid="thread-welcome-layout"
|
||||||
|
className="relative grid w-full max-w-[58rem] flex-1 grid-rows-[minmax(min-content,1fr)_auto] gap-8 sm:block sm:flex-none"
|
||||||
|
>
|
||||||
|
<div className="flex min-h-0 items-center justify-center sm:absolute sm:inset-x-0 sm:bottom-[calc(100%+2rem)]">
|
||||||
{emptyState}
|
{emptyState}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">{composer}</div>
|
<div className="w-full">{composer}</div>
|
||||||
|
|||||||
@@ -598,6 +598,77 @@ describe("ThreadViewport", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the welcome composer above a mobile soft keyboard", async () => {
|
||||||
|
const visualViewport = stubVisualViewport({ innerHeight: 800, height: 480 });
|
||||||
|
try {
|
||||||
|
const { container } = render(
|
||||||
|
<ThreadViewport
|
||||||
|
messages={emptyMessages}
|
||||||
|
isStreaming={false}
|
||||||
|
composer={<textarea aria-label="Message input" />}
|
||||||
|
emptyState={<div>welcome</div>}
|
||||||
|
showScrollToBottomButton={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const scroller = container.querySelector(".thread-viewport-scrollbar") as HTMLElement;
|
||||||
|
const input = screen.getByLabelText("Message input");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
input.focus();
|
||||||
|
fireEvent.focusIn(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(scroller).toHaveStyle({ bottom: "320px" }));
|
||||||
|
} finally {
|
||||||
|
visualViewport.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("separates the mobile welcome copy and composer into responsive rows", () => {
|
||||||
|
render(
|
||||||
|
<ThreadViewport
|
||||||
|
messages={emptyMessages}
|
||||||
|
isStreaming={false}
|
||||||
|
composer={<div>composer</div>}
|
||||||
|
emptyState={<div>welcome</div>}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const layout = screen.getByTestId("thread-welcome-layout");
|
||||||
|
expect(layout).toHaveClass(
|
||||||
|
"grid",
|
||||||
|
"gap-8",
|
||||||
|
"grid-rows-[minmax(min-content,1fr)_auto]",
|
||||||
|
);
|
||||||
|
expect(screen.getByText("welcome").parentElement).toHaveClass("min-h-0");
|
||||||
|
expect(screen.getByText("welcome").parentElement?.className).toContain(
|
||||||
|
"sm:bottom-[calc(100%+2rem)]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows the welcome view to scroll when a short viewport overflows", async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<ThreadViewport
|
||||||
|
messages={emptyMessages}
|
||||||
|
isStreaming={false}
|
||||||
|
composer={<div>composer</div>}
|
||||||
|
emptyState={<div>welcome</div>}
|
||||||
|
showScrollToBottomButton={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const scroller = container.querySelector(".thread-viewport-scrollbar") as HTMLElement;
|
||||||
|
Object.defineProperties(scroller, {
|
||||||
|
scrollHeight: { configurable: true, value: 620 },
|
||||||
|
clientHeight: { configurable: true, value: 320 },
|
||||||
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
window.dispatchEvent(new Event("resize"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(scroller).toHaveClass("overflow-y-auto"));
|
||||||
|
});
|
||||||
|
|
||||||
it("scrolls recent messages into view when the composer receives focus", async () => {
|
it("scrolls recent messages into view when the composer receives focus", async () => {
|
||||||
const scrollTo = vi.fn();
|
const scrollTo = vi.fn();
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
|
|||||||
Reference in New Issue
Block a user