fix(tooltip): update refs#3258
Merged
Merged
Conversation
🦋 Changeset detectedLatest commit: 7dbcaef The changes in this PR will be included in the next version bump. This PR includes changesets to release 97 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
ByronDWall
approved these changes
Jun 12, 2026
ByronDWall
left a comment
Contributor
There was a problem hiding this comment.
cursed, but now maybe workably cursed?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
follows on from #3256
fix(tooltip): close reliably when
mouseleaveis missed oranimationendnever firesProblem
Image-preview tooltips in Merchant Center (Product → Variants) can get permanently
stuck open after hover. The enlarged preview appears and never dismisses, requiring
a full page reload to clear.
Four root causes were identified:
1. Opened-wedge (confirmed in production — the decisive bug)
A tooltip can get stuck in the
openedstate becausehandleLeaveis never called.React 19's synthetic event delegation can silently drop the
mouseleaveevent for aspecific instance. Once in
opened, the component had no recovery path: no scheduledtimer, no
animationendlistener, nothing. The tooltip stayed visible indefinitely.Confirmed via DevTools on a live Variants page:
querySelectorAll('[data-testid=...]').length === 1)openedstate withgrowInanimation,opacity: 1getEventListeners(wrapper)returned{}— no close listener was ever attached2. Exiting-wedge
Even when
handleLeavedid run and schedule theexitingtransition, the tooltipcould get stuck in
exitingforever ifanimationendnever fired (e.g. a styleoverride suppressed
growOut, orprefers-reduced-motionwas active). There was nofallback — if the event was missed, the tooltip never unmounted.
3. Unscoped and accumulating
animationendlistenerThe listener accepted any bubbled
animationend— including descendant animationsfrom consumers (e.g. the
showkeyframe in MC'sImageContainer) — and accumulatedacross enter/leave cycles with no cleanup. Whether the tooltip closed depended on a
non-deterministic race between
growOut, the consumer's keyframe, and a null-escapehatch.
4. Exit animation interrupted by late-arriving leave events
Any
handleLeavecall that arrived whilegrowOutwas already playing (a delayedReact synthetic
mouseleave, or amouseleavefrom the tooltip body) would callhandleClose()immediately, aborting the animation mid-play. The abrupt unmountcould then trigger a stray
mouseoveron the trigger, reopening the tooltip andproducing a visible double-animation on close.
Fix
Native
mouseleavefallback whileopenedWhen state is
opened, a nativemouseleavelistener is attached directly to thewrapper DOM node via
popperInstance.reference. This bypasses React's event delegationand fires regardless of whether the synthetic
onMouseLeavewas delivered. The effectis keyed on
state, so the listener is removed the moment state transitions away fromopened.The effect also attaches a mirrored
mouseleavelistener on the popper element itself(
popperInstance.popper) so that image previews remain open while the user hoversover them. Moving the mouse from the trigger onto the preview suppresses the close;
moving off both elements triggers it.
Note:
use-popperuses callback refs internally, soreference.ref.currentandpopper.ref.currentare alwaysundefinedat runtime — DOM nodes are read frompopperInstance.reference/popperInstance.popperinstead.animationendlistener moved to auseEffectwith scoped handling and fallbackThe
querySelector+addEventListenercall is removed fromhandleLeaveandreplaced with a dedicated
useEffectthat runs whenstate === 'exiting'. Thelistener now checks
event.target === tooltipElementto ignore bubbled events fromdescendant animations. A
setTimeout(close, 100)fallback ensures the tooltip alwaysunmounts even if
animationendis never delivered. Aclosedguard preventsdouble-close. The effect's cleanup removes the listener and clears the timer, so
nothing accumulates across cycles.
handleLeaveguarded against interrupting the exit animationAdded
else if (state !== 'exiting')tohandleLeave's fallback branch so thatlate-arriving leave events (from the popper listener or a delayed synthetic event)
cannot call
handleClose()whilegrowOutis in progress.Files changed
packages/components/tooltip/src/tooltip.tsxpackages/components/tooltip/src/tooltip.spec.js— updatedcloseAndValidateTooltipto use
waitForinstead ofwaitForTimeout+ manualfireEvent.animationEnd. Injsdom there is no real Popper instance, so the
exitingeffect callshandleClose()directly; no animation event needs to be dispatched manually.
Backward compatibility
animationend-based close path is preserved for real browsers where Popper andthe DOM are fully set up; the fallback only fires when the event is missed.
isOpenprop) are unaffected — the native fallback guards on!isControlled.