Skip to content

Commit b4afe26

Browse files
committed
refactor: setTimeout -> raf
1 parent 6df6320 commit b4afe26

File tree

4 files changed

+25
-40
lines changed

4 files changed

+25
-40
lines changed

.changeset/neat-pigs-dance.md

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,5 @@
22
"@zag-js/toast": patch
33
---
44

5-
fix(toast): prevent toasts from collapsing when pointer is hovering
6-
7-
Fixed an issue where dismissing a toast by clicking the close button while hovering over the toast group would cause
8-
toasts to immediately collapse, even though the cursor was still within the group.
9-
10-
**Root causes:**
11-
12-
1. **All browsers:** Focus restoration to the trigger button causes the browser to recalculate hover state, removing the
13-
`:hover` pseudo-class momentarily
14-
2. **Some browsers (particularly Firefox):** DOM mutations (removing toasts) can trigger spurious `mouseleave`/
15-
`mouseenter` events even when the mouse hasn't moved, causing flickering when multiple toasts are dismissed rapidly
16-
17-
**Solution:**
18-
19-
- Add `isPointerWithin` tracking to only restore focus when pointer has actually left the toast group
20-
- Add `ignoringMouseEvents` flag that blocks all mouse events for 100ms after toast removal
21-
- This prevents spurious event cycles during DOM mutations from triggering unwanted expand/collapse actions
22-
23-
This maintains the expected hover behavior across all browsers while preserving accessibility for keyboard users.
5+
Fix issue where toasts collapse immediately when dismissing while hovering, by tracking pointer state and temporarily
6+
ignoring spurious mouse events during DOM mutations using requestAnimationFrame.

packages/machines/toast/src/toast-group.connect.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,24 +40,15 @@ export function groupConnect<T extends PropTypes, O = any>(
4040
role: "region",
4141
style: getGroupPlacementStyle(service, placement),
4242
onMouseEnter() {
43-
// Ignore mouse events briefly after toast removal to prevent spurious events during DOM mutations
44-
if (refs.get("ignoringMouseEvents")) {
45-
return
46-
}
43+
if (refs.get("ignoreMouseTimer") !== null) return
4744
send({ type: "REGION.POINTER_ENTER", placement })
4845
},
4946
onMouseMove() {
50-
// Ignore mouse events briefly after toast removal to prevent spurious events during DOM mutations
51-
if (refs.get("ignoringMouseEvents")) {
52-
return
53-
}
47+
if (refs.get("ignoreMouseTimer") !== null) return
5448
send({ type: "REGION.POINTER_ENTER", placement })
5549
},
5650
onMouseLeave() {
57-
// Ignore mouse events briefly after toast removal to prevent spurious events during DOM mutations
58-
if (refs.get("ignoringMouseEvents")) {
59-
return
60-
}
51+
if (refs.get("ignoreMouseTimer") !== null) return
6152
send({ type: "REGION.POINTER_LEAVE", placement })
6253
},
6354
onFocus(event) {

packages/machines/toast/src/toast-group.machine.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const groupMachine = createMachine({
2727
lastFocusedEl: null,
2828
isFocusWithin: false,
2929
isPointerWithin: false,
30-
ignoringMouseEvents: false,
30+
ignoreMouseTimer: null,
3131
dismissableCleanup: undefined,
3232
}
3333
},
@@ -62,7 +62,7 @@ export const groupMachine = createMachine({
6262
})
6363
},
6464

65-
exit: ["clearDismissableBranch", "clearLastFocusedEl"],
65+
exit: ["clearDismissableBranch", "clearLastFocusedEl", "clearIgnoreMouseTimer"],
6666

6767
on: {
6868
"DOC.HOTKEY": {
@@ -263,12 +263,23 @@ export const groupMachine = createMachine({
263263
refs.set("isFocusWithin", false)
264264
},
265265
ignoreMouseEventsTemporarily({ refs }) {
266-
// Ignore mouse events briefly after toast removal to prevent spurious events
267-
// during DOM mutations (particularly in Firefox, but applied universally for consistency)
268-
refs.set("ignoringMouseEvents", true)
269-
setTimeout(() => {
270-
refs.set("ignoringMouseEvents", false)
271-
}, 100)
266+
const existingTimer = refs.get("ignoreMouseTimer")
267+
if (existingTimer !== null) {
268+
cancelAnimationFrame(existingTimer)
269+
}
270+
271+
const timerId = requestAnimationFrame(() => {
272+
refs.set("ignoreMouseTimer", null)
273+
})
274+
275+
refs.set("ignoreMouseTimer", timerId)
276+
},
277+
clearIgnoreMouseTimer({ refs }) {
278+
const timerId = refs.get("ignoreMouseTimer")
279+
if (timerId !== null) {
280+
cancelAnimationFrame(timerId)
281+
refs.set("ignoreMouseTimer", null)
282+
}
272283
},
273284
},
274285
},

packages/machines/toast/src/toast.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export type ToastGroupSchema = {
247247
lastFocusedEl: HTMLElement | null
248248
isFocusWithin: boolean
249249
isPointerWithin: boolean
250-
ignoringMouseEvents: boolean
250+
ignoreMouseTimer: number | null
251251
}
252252
guard: string
253253
effect: string

0 commit comments

Comments
 (0)