Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hover-card-focus-pointer-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zag-js/hover-card": patch
---

Keep the hover card open when the pointer leaves a trigger that remains focused.
18 changes: 18 additions & 0 deletions e2e/hover-card-multiple-trigger.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ test.describe("hover-card / multiple triggers", () => {
await expect(page.locator(content)).toContainText("Alice Johnson")
})

test("should remain open after pointer leaves the focused active trigger", async ({ page }) => {
await page.waitForLoadState("networkidle")
await page.locator(trigger(2)).focus()
await expect(page.locator(trigger(2))).toBeFocused()
await expect(page.locator(content)).toBeVisible()
await expect(page.locator(content)).toContainText("Bob Smith")

await page.hover(trigger(2))
await page.mouse.move(0, 0)
await page.waitForTimeout(500)
await expect(page.locator(trigger(2))).toBeFocused()
await expect(page.locator(content)).toBeVisible()
await expect(page.locator(content)).toContainText("Bob Smith")

await page.locator(trigger(2)).evaluate((element) => element.blur())
await expect(page.locator(content)).toBeHidden()
})

test("should close hover card on escape", async ({ page }) => {
await page.hover(trigger(1))
await expect(page.locator(content)).toBeVisible()
Expand Down
15 changes: 15 additions & 0 deletions e2e/hover-card.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ test("should remain open after blurring trigger if pointer opens card", async ({
await expect(page.locator(content)).not.toBeVisible()
})

test("should remain open after pointer leaves a focused trigger", async ({ page }) => {
await page.focus(trigger)
await page.waitForSelector(content)

await page.hover(trigger)
await page.hover(testText)
await page.waitForTimeout(500)

await expect(page.locator(trigger)).toBeFocused()
await expect(page.locator(content)).toBeVisible()

await page.locator(trigger).evaluate((element) => element.blur())
await expect(page.locator(content)).not.toBeVisible()
})

test("should remain open after moving from trigger to content", async ({ page }) => {
await page.hover(trigger)
await page.waitForSelector(content)
Expand Down
18 changes: 15 additions & 3 deletions packages/machines/hover-card/src/hover-card.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export const machine = createMachine<HoverCardSchema>({
target: "closed",
},
POINTER_LEAVE: [
{
guard: "isTriggerFocused",
actions: ["clearIsPointer"],
},
{
guard: "isOpenControlled",
// We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point)
Expand Down Expand Up @@ -161,9 +165,15 @@ export const machine = createMachine<HoverCardSchema>({
POINTER_ENTER: {
actions: ["setIsPointer"],
},
POINTER_LEAVE: {
target: "closing",
},
POINTER_LEAVE: [
{
guard: "isTriggerFocused",
actions: ["clearIsPointer"],
},
{
target: "closing",
},
],
CLOSE: [
{
guard: "isOpenControlled",
Expand Down Expand Up @@ -231,6 +241,8 @@ export const machine = createMachine<HoverCardSchema>({
implementations: {
guards: {
isPointer: ({ context }) => !!context.get("isPointer"),
isTriggerFocused: ({ context, scope }) =>
scope.isActiveElement(dom.getActiveTriggerEl(scope, context.get("triggerValue"))),
isOpenControlled: ({ prop }) => prop("open") != null,
},

Expand Down
Loading