Combination of Toolbar and Tooltip causes glitching on Tooltip
- Dominant language
- TypeScript
- Stars
- 15.9k
- Forks
- 1.6k
- Avg merge
- 3d 9m
- Merged PRs (30d)
- 59
Description
### 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](https://stackblitz.com/edit/5avnje6b?file=src%2FTooltip.tsx)
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 calls `focusManager.focusLast()` and returns without
`preventDefault`, so the last control is focused before the browser carries focus onward.
The tooltip's focus path opens immediately, `useTooltipTrigger.onFocus` calls
`state.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.updatePosition` returns early on `!isOpen`, and `placement` is
`position?.placement ?? null`, so the overlay is never measured. That produces exactly the
observed DOM: no inline position, no `data-placement`, painted at the document origin.
> What keeps the node alive afterwards is not confirmed. One candidate is `useExitAnimation`,
whose `useAnimation` layout effect has deps `[ref, isActive, onEnd]`. All three are stable,
so the effect runs at most once per exit and a missed `onEnd` would strand the node.
**Workaround**
> Hide the overlay whenever the `placement` render prop is null, which is exactly the
condition "mounted but never measured". React Aria already treats `placement` as the
readiness signal, `isEntering` is `useEnterAnimation(tooltipRef, !!placement)`.
```tsx
(placement ? "" : "invisible")} />
```
> Use `visibility: hidden`, not `display: none`. `updatePosition` measures the overlay with
`getBoundingClientRect`, so removing it from layout gives a wrong position on the normal
open path, where the first commit also has `placement === null` for 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](https://stackblitz.com/edit/5avnje6b?file=src%2FTooltip.tsx)
### 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_
Contributor guide
Research direction
Start with the Stackblitz reproduction in src/Tooltip.tsx and reproduce the issue in Chrome by tabbing into and out of the Toolbar. Trace the Toolbar and Tooltip focus behavior described in the report; done means tabbing out produces no tooltip flash and does not leave a tooltip at the viewport origin.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- accessibility, frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100