Combination of Toolbar and Tooltip causes glitching on Tooltip
Provide a general summary of the issue here
I have a use case where I use the Toolbar component with buttons that each have a tooltip. When I tab into the toolbar and then tab again to exit it, then the tooltip of the last button flashes briefly and the tooltip of the button that had focus is rendered at the top left of the viewport.
Expected Behavior?
When I tab out of the toolbar the last tooltip should not be shown and the tooltip of the first button should not be shown in the top left corner of the viewport.
Current Behavior
I've created a reproduction in Stackblitz: reproduction
I also made a screen recording showcasing the issue:
https://github.com/user-attachments/assets/9545fd74-9c56-4142-b848-57cb89be8d82
Possible Solution
I analysed the issue with AI and it came up with following explanation and work around. The work around is visual and doesn't fix the issue, but makes it less noticeable at least.
Cause
useToolbar's Tab handler callsfocusManager.focusLast()and returns withoutpreventDefault, so the last control is focused before the browser carries focus onward. The tooltip's focus path opens immediately,useTooltipTrigger.onFocuscallsstate.open(true), and the subsequent blur closes it, so an open and a close arrive back to back within one keydown.
A tooltip overlay then ends up rendered while the trigger state is closed. In that state
useOverlayPosition.updatePositionreturns early on!isOpen, andplacementisposition?.placement ?? null, so the overlay is never measured. That produces exactly the observed DOM: no inline position, nodata-placement, painted at the document origin.
What keeps the node alive afterwards is not confirmed. One candidate is
useExitAnimation, whoseuseAnimationlayout effect has deps[ref, isActive, onEnd]. All three are stable, so the effect runs at most once per exit and a missedonEndwould strand the node.
Workaround
Hide the overlay whenever the
placementrender prop is null, which is exactly the condition "mounted but never measured". React Aria already treatsplacementas the readiness signal,isEnteringisuseEnterAnimation(tooltipRef, !!placement).
<Tooltip className={({ placement }) => (placement ? "" : "invisible")} />Use
visibility: hidden, notdisplay: none.updatePositionmeasures the overlay withgetBoundingClientRect, so removing it from layout gives a wrong position on the normal open path, where the first commit also hasplacement === nullfor one frame.
This suppresses the misplaced tooltip in both presentations. It does not address the underlying state, and the last control's tooltip still flashes briefly on the way out, since that one is genuinely measured before it closes.
https://github.com/user-attachments/assets/d239961f-67a7-4910-a74d-8924f2d24fdd
Context
For work I'm building a "bulk action bar" that will be overlayed over a table when multiple items are selected. The buttons are small icon buttons that get a tooltip to explain the behavior of each button.
️ Steps to Reproduce
I've created a reproduction in Stackblitz: reproduction
Version
react-aria-components 1.20.0
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
MacOS
Your Company/Team
No response
Tracking Issue
No response
Source: adobe/react-spectrum