fix: use JavaScript-based hover for checkpoint menu visibility (#10056)

This commit is contained in:
Hannes Rudolph 2025-12-12 15:06:16 -07:00 • committed by GitHub
parent 4771de1ebc
commit a3b258ad62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 11 deletions

View file

@ -18,6 +18,7 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
const isCurrent = currentHash === props.commitHash
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const [isClosing, setIsClosing] = useState(false)
const [isHovering, setIsHovering] = useState(false)
const closeTimer = useRef<number | null>(null)
useEffect(() => {
@ -46,7 +47,16 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
}
}
const menuVisible = isPopoverOpen || isClosing
const handleMouseEnter = () => {
setIsHovering(true)
}
const handleMouseLeave = () => {
setIsHovering(false)
}
// Menu is visible when hovering, popover is open, or briefly after popover closes
const menuVisible = isHovering || isPopoverOpen || isClosing
const metadata = useMemo(() => {
if (!checkpoint) {
@ -67,7 +77,10 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
}
return (
<div className="group flex items-center justify-between gap-2 pt-2 pb-3 ">
<div
className="flex items-center justify-between gap-2 pt-2 pb-3"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}>
<div className="flex items-center gap-2 text-blue-400 whitespace-nowrap">
<GitCommitVertical className="w-4" />
<span className="font-semibold">{t("chat:checkpoint.regular")}</span>
@ -80,10 +93,8 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
"linear-gradient(90deg, rgba(0, 188, 255, .65), rgba(0, 188, 255, .65) 80%, rgba(0, 188, 255, 0) 99%)",
}}></span>
{/* Keep menu visible while popover is open or briefly after close to prevent jump */}
<div
data-testid="checkpoint-menu-container"
className={cn("h-4 -mt-2", menuVisible ? "block" : "hidden group-hover:block")}>
{/* Keep menu visible while hovering, popover is open, or briefly after close to prevent jump */}
<div data-testid="checkpoint-menu-container" className={cn("h-4 -mt-2", menuVisible ? "block" : "hidden")}>
<CheckpointMenu
ts={props.ts}
commitHash={props.commitHash}

View file

@ -28,7 +28,7 @@ vi.mock("@/components/ui", () => {
}
})
import { render, waitFor, screen } from "@/utils/test-utils"
import { render, waitFor, screen, fireEvent } from "@/utils/test-utils"
import React from "react"
import userEvent from "@testing-library/user-event"
import { CheckpointSaved } from "../CheckpointSaved"
@ -54,7 +54,7 @@ describe("CheckpointSaved popover visibility", () => {
const getMenu = () => getByTestId("checkpoint-menu-container") as HTMLElement
// Initially hidden (relies on group-hover)
// Initially hidden (not hovering)
expect(getMenu()).toBeTruthy()
expect(getMenu().className).toContain("hidden")
@ -80,7 +80,12 @@ describe("CheckpointSaved popover visibility", () => {
})
it("resets confirm state when popover closes", async () => {
const { getByTestId } = render(<CheckpointSaved {...baseProps} />)
const { getByTestId, container } = render(<CheckpointSaved {...baseProps} />)
const getParentDiv = () =>
container.querySelector("[class*='flex items-center justify-between']") as HTMLElement
// Hover to make menu visible
fireEvent.mouseEnter(getParentDiv())
// Open the popover
await waitForOpenHandler()
@ -106,10 +111,12 @@ describe("CheckpointSaved popover visibility", () => {
})
it("closes popover after preview and after confirm restore", async () => {
const { getByTestId } = render(<CheckpointSaved {...baseProps} />)
const { getByTestId, container } = render(<CheckpointSaved {...baseProps} />)
const popoverRoot = () => getByTestId("restore-popover")
const menuContainer = () => getByTestId("checkpoint-menu-container")
const getParentDiv = () =>
container.querySelector("[class*='flex items-center justify-between']") as HTMLElement
// Open
await waitForOpenHandler()
@ -125,11 +132,16 @@ describe("CheckpointSaved popover visibility", () => {
expect(popoverRoot().getAttribute("data-open")).toBe("false")
expect(menuContainer().className).toContain("block")
})
// Simulate mouse leaving the component to trigger hide
fireEvent.mouseLeave(getParentDiv())
await waitFor(() => {
expect(menuContainer().className).toContain("hidden")
})
// Reopen
// Hover to make menu visible again, then reopen
fireEvent.mouseEnter(getParentDiv())
lastOnOpenChange?.(true)
await waitFor(() => {
expect(popoverRoot().getAttribute("data-open")).toBe("true")
@ -141,8 +153,36 @@ describe("CheckpointSaved popover visibility", () => {
await waitFor(() => {
expect(popoverRoot().getAttribute("data-open")).toBe("false")
})
// Simulate mouse leaving the component to trigger hide
fireEvent.mouseLeave(getParentDiv())
await waitFor(() => {
expect(menuContainer().className).toContain("hidden")
})
})
it("shows menu on hover and hides when mouse leaves", async () => {
const { getByTestId, container } = render(<CheckpointSaved {...baseProps} />)
const getMenu = () => getByTestId("checkpoint-menu-container") as HTMLElement
const getParentDiv = () =>
container.querySelector("[class*='flex items-center justify-between']") as HTMLElement
// Initially hidden (not hovering)
expect(getMenu().className).toContain("hidden")
// Hover over the component
fireEvent.mouseEnter(getParentDiv())
await waitFor(() => {
expect(getMenu().className).toContain("block")
expect(getMenu().className).not.toContain("hidden")
})
// Mouse leaves the component
fireEvent.mouseLeave(getParentDiv())
await waitFor(() => {
expect(getMenu().className).toContain("hidden")
})
})
})