Skip to content

Commit e7e10e7

Browse files
kai392RealDiligentcursoragent
authored
fix(miner-ui): keep mobile chat sheet mounted so conversation state survives (#7792) (#7885)
* fix(miner-ui): keep mobile chat sheet mounted so conversation state survives (#7792) Pass forceMount through SheetContent to Radix Portal/Overlay/Content so closing the mobile chat sheet matches desktop's hidden-but-mounted rail behavior. Closes #7792 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(miner-ui): keep mobile chat sheet mounted so conversation state survives (#7792) Pass forceMount through SheetContent to Radix Portal/Overlay/Content so closing the mobile chat sheet matches desktop's hidden-but-mounted rail behavior. Closes #7792 Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: RealDiligent <brave.challenge007@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b76c91d commit e7e10e7

3 files changed

Lines changed: 64 additions & 9 deletions

File tree

apps/loopover-miner-ui/src/chat-rail.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { fireEvent, render, screen } from "@testing-library/react";
2+
import * as React from "react";
23
import { afterEach, describe, expect, it, vi } from "vitest";
34

45
// The TanStack Router lib is not under test here — RootShell's own rail-state persistence is. Stub Link so the
@@ -14,6 +15,15 @@ vi.mock("@tanstack/react-router", async () => {
1415
};
1516
});
1617

18+
// Stateful stub so the #7792 close/reopen test can assert in-rail React state survives the mobile sheet cycle
19+
// without standing up the real chat backend / streaming stack.
20+
vi.mock("./components/chat/conversation", () => ({
21+
ChatConversation: () => {
22+
const [draft, setDraft] = React.useState("");
23+
return <input aria-label="chat draft" value={draft} onChange={(event) => setDraft(event.target.value)} />;
24+
},
25+
}));
26+
1727
import { ChatRail } from "./components/chat-rail";
1828
import { RootShell } from "./routes/__root";
1929

@@ -81,6 +91,25 @@ describe("ChatRail (#6513)", () => {
8191
expect(screen.getByRole("dialog")).toBeTruthy(); // Sheet content
8292
expect(screen.queryByRole("complementary")).toBeNull(); // never the docked panel on mobile
8393
});
94+
95+
it("preserves chat draft state across a mobile sheet close/reopen cycle (#7792)", () => {
96+
setViewport(400);
97+
const onOpenChange = vi.fn();
98+
const { rerender } = render(<ChatRail open onOpenChange={onOpenChange} />);
99+
100+
const draft = screen.getByRole("textbox", { name: /chat draft/i });
101+
fireEvent.change(draft, { target: { value: "still typing…" } });
102+
expect((draft as HTMLInputElement).value).toBe("still typing…");
103+
104+
// Close the sheet (same open=false transition accidental tap-outside / Escape / toggle would cause).
105+
rerender(<ChatRail open={false} onOpenChange={onOpenChange} />);
106+
// forceMount keeps the dialog content in the tree even while closed.
107+
expect(screen.getByRole("dialog", { hidden: true })).toBeTruthy();
108+
109+
// Reopen — draft must still be there (RailBody never unmounted).
110+
rerender(<ChatRail open onOpenChange={onOpenChange} />);
111+
expect((screen.getByRole("textbox", { name: /chat draft/i }) as HTMLInputElement).value).toBe("still typing…");
112+
});
84113
});
85114

86115
describe("RootShell chat-rail integration (#6513)", () => {

apps/loopover-miner-ui/src/components/chat-rail.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ export function ChatRail({ open, onOpenChange }: ChatRailProps) {
4747
Chat
4848
</Button>
4949
<Sheet open={open} onOpenChange={onOpenChange}>
50-
<SheetContent id={RAIL_PANEL_ID} side="right" className="w-[380px] p-0">
50+
{/* forceMount: keep RailBody mounted when the sheet closes so chat state survives, matching the
51+
desktop `<aside hidden={!open}>` contract (#7792). SheetContent already forwards unknown props
52+
onto Radix Dialog.Content, which honors forceMount. */}
53+
<SheetContent id={RAIL_PANEL_ID} side="right" className="w-[380px] p-0" forceMount>
5154
<SheetHeader className="sr-only">
5255
<SheetTitle>Chat</SheetTitle>
5356
<SheetDescription>Ask about this miner&rsquo;s local state.</SheetDescription>

packages/loopover-ui-kit/src/components/sheet.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,18 @@ interface SheetContentProps
5757
const SheetContent = React.forwardRef<
5858
React.ElementRef<typeof SheetPrimitive.Content>,
5959
SheetContentProps
60-
>(({ side = "right", className, children, ...props }, ref) => (
61-
<SheetPortal>
62-
<SheetOverlay />
63-
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
60+
>(({ side = "right", className, children, forceMount, ...props }, ref) => (
61+
// When callers pass forceMount (e.g. chat-rail mobile sheet, #7792), forward it to Portal + Overlay +
62+
// Content together: Content-only forceMount is a no-op if Portal's Presence has already unmounted the
63+
// subtree. Default (forceMount undefined) keeps the prior unmount-on-close behavior for every other sheet.
64+
<SheetPortal forceMount={forceMount}>
65+
<SheetOverlay forceMount={forceMount} />
66+
<SheetPrimitive.Content
67+
ref={ref}
68+
forceMount={forceMount}
69+
className={cn(sheetVariants({ side }), className)}
70+
{...props}
71+
>
6472
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
6573
<X className="h-4 w-4" />
6674
<span className="sr-only">Close</span>
@@ -71,14 +79,29 @@ const SheetContent = React.forwardRef<
7179
));
7280
SheetContent.displayName = SheetPrimitive.Content.displayName;
7381

74-
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
75-
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
82+
const SheetHeader = ({
83+
className,
84+
...props
85+
}: React.HTMLAttributes<HTMLDivElement>) => (
86+
<div
87+
className={cn(
88+
"flex flex-col space-y-2 text-center sm:text-left",
89+
className,
90+
)}
91+
{...props}
92+
/>
7693
);
7794
SheetHeader.displayName = "SheetHeader";
7895

79-
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
96+
const SheetFooter = ({
97+
className,
98+
...props
99+
}: React.HTMLAttributes<HTMLDivElement>) => (
80100
<div
81-
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
101+
className={cn(
102+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
103+
className,
104+
)}
82105
{...props}
83106
/>
84107
);

0 commit comments

Comments
 (0)