Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit a3b258a

Browse files
fix: use JavaScript-based hover for checkpoint menu visibility (#10056)
1 parent 4771de1 commit a3b258a

2 files changed

Lines changed: 62 additions & 11 deletions

File tree

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
1818
const isCurrent = currentHash === props.commitHash
1919
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
2020
const [isClosing, setIsClosing] = useState(false)
21+
const [isHovering, setIsHovering] = useState(false)
2122
const closeTimer = useRef<number | null>(null)
2223

2324
useEffect(() => {
@@ -46,7 +47,16 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
4647
}
4748
}
4849

49-
const menuVisible = isPopoverOpen || isClosing
50+
const handleMouseEnter = () => {
51+
setIsHovering(true)
52+
}
53+
54+
const handleMouseLeave = () => {
55+
setIsHovering(false)
56+
}
57+
58+
// Menu is visible when hovering, popover is open, or briefly after popover closes
59+
const menuVisible = isHovering || isPopoverOpen || isClosing
5060

5161
const metadata = useMemo(() => {
5262
if (!checkpoint) {
@@ -67,7 +77,10 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
6777
}
6878

6979
return (
70-
<div className="group flex items-center justify-between gap-2 pt-2 pb-3 ">
80+
<div
81+
className="flex items-center justify-between gap-2 pt-2 pb-3"
82+
onMouseEnter={handleMouseEnter}
83+
onMouseLeave={handleMouseLeave}>
7184
<div className="flex items-center gap-2 text-blue-400 whitespace-nowrap">
7285
<GitCommitVertical className="w-4" />
7386
<span className="font-semibold">{t("chat:checkpoint.regular")}</span>
@@ -80,10 +93,8 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
8093
"linear-gradient(90deg, rgba(0, 188, 255, .65), rgba(0, 188, 255, .65) 80%, rgba(0, 188, 255, 0) 99%)",
8194
}}></span>
8295

83-
{/* Keep menu visible while popover is open or briefly after close to prevent jump */}
84-
<div
85-
data-testid="checkpoint-menu-container"
86-
className={cn("h-4 -mt-2", menuVisible ? "block" : "hidden group-hover:block")}>
96+
{/* Keep menu visible while hovering, popover is open, or briefly after close to prevent jump */}
97+
<div data-testid="checkpoint-menu-container" className={cn("h-4 -mt-2", menuVisible ? "block" : "hidden")}>
8798
<CheckpointMenu
8899
ts={props.ts}
89100
commitHash={props.commitHash}

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ vi.mock("@/components/ui", () => {
2828
}
2929
})
3030

31-
import { render, waitFor, screen } from "@/utils/test-utils"
31+
import { render, waitFor, screen, fireEvent } from "@/utils/test-utils"
3232
import React from "react"
3333
import userEvent from "@testing-library/user-event"
3434
import { CheckpointSaved } from "../CheckpointSaved"
@@ -54,7 +54,7 @@ describe("CheckpointSaved popover visibility", () => {
5454

5555
const getMenu = () => getByTestId("checkpoint-menu-container") as HTMLElement
5656

57-
// Initially hidden (relies on group-hover)
57+
// Initially hidden (not hovering)
5858
expect(getMenu()).toBeTruthy()
5959
expect(getMenu().className).toContain("hidden")
6060

@@ -80,7 +80,12 @@ describe("CheckpointSaved popover visibility", () => {
8080
})
8181

8282
it("resets confirm state when popover closes", async () => {
83-
const { getByTestId } = render(<CheckpointSaved {...baseProps} />)
83+
const { getByTestId, container } = render(<CheckpointSaved {...baseProps} />)
84+
const getParentDiv = () =>
85+
container.querySelector("[class*='flex items-center justify-between']") as HTMLElement
86+
87+
// Hover to make menu visible
88+
fireEvent.mouseEnter(getParentDiv())
8489

8590
// Open the popover
8691
await waitForOpenHandler()
@@ -106,10 +111,12 @@ describe("CheckpointSaved popover visibility", () => {
106111
})
107112

108113
it("closes popover after preview and after confirm restore", async () => {
109-
const { getByTestId } = render(<CheckpointSaved {...baseProps} />)
114+
const { getByTestId, container } = render(<CheckpointSaved {...baseProps} />)
110115

111116
const popoverRoot = () => getByTestId("restore-popover")
112117
const menuContainer = () => getByTestId("checkpoint-menu-container")
118+
const getParentDiv = () =>
119+
container.querySelector("[class*='flex items-center justify-between']") as HTMLElement
113120

114121
// Open
115122
await waitForOpenHandler()
@@ -125,11 +132,16 @@ describe("CheckpointSaved popover visibility", () => {
125132
expect(popoverRoot().getAttribute("data-open")).toBe("false")
126133
expect(menuContainer().className).toContain("block")
127134
})
135+
136+
// Simulate mouse leaving the component to trigger hide
137+
fireEvent.mouseLeave(getParentDiv())
138+
128139
await waitFor(() => {
129140
expect(menuContainer().className).toContain("hidden")
130141
})
131142

132-
// Reopen
143+
// Hover to make menu visible again, then reopen
144+
fireEvent.mouseEnter(getParentDiv())
133145
lastOnOpenChange?.(true)
134146
await waitFor(() => {
135147
expect(popoverRoot().getAttribute("data-open")).toBe("true")
@@ -141,8 +153,36 @@ describe("CheckpointSaved popover visibility", () => {
141153
await waitFor(() => {
142154
expect(popoverRoot().getAttribute("data-open")).toBe("false")
143155
})
156+
157+
// Simulate mouse leaving the component to trigger hide
158+
fireEvent.mouseLeave(getParentDiv())
159+
144160
await waitFor(() => {
145161
expect(menuContainer().className).toContain("hidden")
146162
})
147163
})
164+
165+
it("shows menu on hover and hides when mouse leaves", async () => {
166+
const { getByTestId, container } = render(<CheckpointSaved {...baseProps} />)
167+
168+
const getMenu = () => getByTestId("checkpoint-menu-container") as HTMLElement
169+
const getParentDiv = () =>
170+
container.querySelector("[class*='flex items-center justify-between']") as HTMLElement
171+
172+
// Initially hidden (not hovering)
173+
expect(getMenu().className).toContain("hidden")
174+
175+
// Hover over the component
176+
fireEvent.mouseEnter(getParentDiv())
177+
await waitFor(() => {
178+
expect(getMenu().className).toContain("block")
179+
expect(getMenu().className).not.toContain("hidden")
180+
})
181+
182+
// Mouse leaves the component
183+
fireEvent.mouseLeave(getParentDiv())
184+
await waitFor(() => {
185+
expect(getMenu().className).toContain("hidden")
186+
})
187+
})
148188
})

0 commit comments

Comments
 (0)