Skip to content

Commit c606003

Browse files
committed
fix(checkpoints): control Popover from parent to prevent anchor loss; add focused test for visibility logic (#8220)
1 parent 6c9d989 commit c606003

File tree

3 files changed

+96
-16
lines changed

3 files changed

+96
-16
lines changed

webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,30 @@ type CheckpointMenuProps = {
1313
commitHash: string
1414
currentHash?: string
1515
checkpoint: Checkpoint
16-
onPopoverOpenChange?: (open: boolean) => void
16+
open?: boolean
17+
onOpenChange?: (open: boolean) => void
1718
}
1819

1920
export const CheckpointMenu = ({
2021
ts,
2122
commitHash,
2223
currentHash,
2324
checkpoint,
24-
onPopoverOpenChange,
25+
open,
26+
onOpenChange,
2527
}: CheckpointMenuProps) => {
2628
const { t } = useTranslation()
27-
const [isOpen, setIsOpen] = useState(false)
29+
const [internalOpen, setInternalOpen] = useState(false)
2830
const [isConfirming, setIsConfirming] = useState(false)
2931
const portalContainer = useRooPortal("roo-portal")
3032

3133
const isCurrent = currentHash === commitHash
3234

3335
const previousCommitHash = checkpoint?.from
3436

37+
const isOpen = open ?? internalOpen
38+
const setOpen = onOpenChange ?? setInternalOpen
39+
3540
const onCheckpointDiff = useCallback(() => {
3641
vscode.postMessage({
3742
type: "checkpointDiff",
@@ -41,13 +46,21 @@ export const CheckpointMenu = ({
4146

4247
const onPreview = useCallback(() => {
4348
vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "preview" } })
44-
setIsOpen(false)
45-
}, [ts, commitHash])
49+
setOpen(false)
50+
}, [ts, commitHash, setOpen])
4651

4752
const onRestore = useCallback(() => {
4853
vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "restore" } })
49-
setIsOpen(false)
50-
}, [ts, commitHash])
54+
setOpen(false)
55+
}, [ts, commitHash, setOpen])
56+
57+
const handleOpenChange = useCallback(
58+
(open: boolean) => {
59+
setOpen(open)
60+
setIsConfirming(false)
61+
},
62+
[setOpen],
63+
)
5164

5265
return (
5366
<div className="flex flex-row gap-1">
@@ -56,13 +69,7 @@ export const CheckpointMenu = ({
5669
<span className="codicon codicon-diff-single" />
5770
</Button>
5871
</StandardTooltip>
59-
<Popover
60-
open={isOpen}
61-
onOpenChange={(open) => {
62-
setIsOpen(open)
63-
setIsConfirming(false)
64-
onPopoverOpenChange?.(open)
65-
}}>
72+
<Popover open={isOpen} onOpenChange={handleOpenChange}>
6673
<StandardTooltip content={t("chat:checkpoint.menu.restore")}>
6774
<PopoverTrigger asChild>
6875
<Button variant="ghost" size="icon">

webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useMemo, useState } from "react"
22
import { useTranslation } from "react-i18next"
3+
import { cn } from "@/lib/utils"
34

45
import { CheckpointMenu } from "./CheckpointMenu"
56
import { checkpointSchema } from "./schema"
@@ -49,8 +50,9 @@ export const CheckpointSaved = ({ checkpoint, ...props }: CheckpointSavedProps)
4950
"linear-gradient(90deg, rgba(0, 188, 255, .65), rgba(0, 188, 255, .65) 80%, rgba(0, 188, 255, 0) 99%)",
5051
}}></span>
5152

52-
<div className={`h-4 -mt-2 ${isPopoverOpen ? "block" : "hidden group-hover:block"}`}>
53-
<CheckpointMenu {...props} checkpoint={metadata} onPopoverOpenChange={setIsPopoverOpen} />
53+
{/* Keep menu visible while popover is open; otherwise rely on group-hover */}
54+
<div className={cn("h-4 -mt-2", isPopoverOpen ? "block" : "hidden group-hover:block")}>
55+
<CheckpointMenu {...props} checkpoint={metadata} open={isPopoverOpen} onOpenChange={setIsPopoverOpen} />
5456
</div>
5557
</div>
5658
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// npx vitest run src/components/chat/checkpoints/__tests__/CheckpointSaved.spec.tsx
2+
3+
import { render, waitFor } from "@/utils/test-utils"
4+
import React from "react"
5+
import { CheckpointSaved } from "../CheckpointSaved"
6+
7+
// Capture onOpenChange from Popover to control open/close in tests
8+
let lastOnOpenChange: ((open: boolean) => void) | undefined
9+
10+
vi.mock("@/components/ui", async () => {
11+
const actual = await vi.importActual<any>("@/components/ui")
12+
return {
13+
...actual,
14+
Popover: ({
15+
children,
16+
onOpenChange,
17+
open,
18+
}: {
19+
children: React.ReactNode
20+
open?: boolean
21+
onOpenChange?: (open: boolean) => void
22+
}) => {
23+
lastOnOpenChange = onOpenChange
24+
return (
25+
<div data-testid="popover-root" data-open={open}>
26+
{children}
27+
</div>
28+
)
29+
},
30+
PopoverTrigger: ({ children }: { children: React.ReactNode }) => (
31+
<div data-testid="popover-trigger">{children}</div>
32+
),
33+
PopoverContent: ({ children }: { children: React.ReactNode }) => (
34+
<div data-testid="popover-content">{children}</div>
35+
),
36+
}
37+
})
38+
39+
describe("CheckpointSaved popover visibility", () => {
40+
const baseProps = {
41+
ts: 123,
42+
commitHash: "abc123",
43+
currentHash: "zzz999",
44+
checkpoint: { from: "prev123", to: "abc123" } as Record<string, unknown>,
45+
}
46+
47+
it("shows menu while popover is open and hides when closed", async () => {
48+
const { container } = render(<CheckpointSaved {...baseProps} />)
49+
50+
const getMenu = () => container.querySelector("div.h-4.-mt-2") as HTMLElement
51+
52+
// Initially hidden (relies on group-hover)
53+
expect(getMenu()).toBeTruthy()
54+
expect(getMenu().className).toContain("hidden")
55+
56+
// Open via captured handler
57+
lastOnOpenChange?.(true)
58+
59+
await waitFor(() => {
60+
expect(getMenu().className).toContain("block")
61+
expect(getMenu().className).not.toContain("hidden")
62+
})
63+
64+
// Close via captured handler
65+
lastOnOpenChange?.(false)
66+
67+
await waitFor(() => {
68+
expect(getMenu().className).toContain("hidden")
69+
})
70+
})
71+
})

0 commit comments

Comments
 (0)