Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix components not properly closing when using the transition prop #3448

Merged
merged 5 commits into from
Sep 3, 2024

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Sep 3, 2024

This PR fixes a bug where the components don't always properly close when using the transition prop on those components.

The issue here is that the internal useTransition(…) hook relies on a DOM node. Whenever the DOM node changes, we need to re-run the useTransition(…). This is why we store the DOM element in state instead of relying on a useRef(…).

Let's say you have a Popover component, then the structure looks like this:

<Popover>
  <PopoverButton>Show</PopoverButton>
  <PopoverPanel>Contents</PopoverPanel>
</Popover>

We store a DOM reference to the button and the panel in state, and the state lives in the Popover component. The reason we do that is so that the button can reference the panel and the panel can reference the button. This is needed for some aria-* attributes for example:

<PopoverButton aria-controls={panelElement.id}>

For the transitions, we set some state to make sure that the panel is visible or hidden, then we wait for transitions to finish by listening to transition related events on the DOM node directly.

If you now say, "hey panel, please re-render because you have to become visible/hidden" then the component re-renders, the panel DOM node (stored in the Popover component) eventually updates and then the useTransition(…) hooks receives the new value (either the DOM node or null when the leave transition is complete).

The problem here is the round trip that it first has to go to the root <Popover/> component, re-render everything and provide the new DOM node to the useTransition(…) hook.

The solution? Local state so that the panel can re-render on its own and doesn't require the round trip via the parent.

Fixes: #3438
Fixes: #3437
Fixes: tailwindlabs/tailwindui-issues#1625

Copy link

vercel bot commented Sep 3, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
headlessui-react ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 3, 2024 3:15pm
headlessui-vue ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 3, 2024 3:15pm

This now relies on local state for transitions so that when the local
state changes, the current component can re-render on its own already.
Relying on state from the parent via context, requires the full
component to re-render first which is not efficient enough for this
case.

We still need that more global state for cases where we want to
reference an element in another component (e.g.: reference the panel id
in a button's aria-controls attribute)
@RobinMalfait RobinMalfait merged commit 071aa0e into main Sep 3, 2024
8 checks passed
@RobinMalfait RobinMalfait deleted the fix/issue-3437 branch September 3, 2024 15:19
RobinMalfait added a commit that referenced this pull request Sep 4, 2024
…on` in rapid succession (#3452)

We recently landed a fix for `Popover`s not closing correctly when using
the `transition` prop (#3448). Once this fix was published, some users
still ran into issues using Firefox on Windows (see:
tailwindlabs/tailwindui-issues#1625).

One fun thing I discovered is that transitions somehow behave
differently based on where they are triggered from (?). What I mean by
this is that holding down the <kbd>space</kbd> key on the button does
properly open/close the `Popover`. But if you rapidly click the button,
the `Popover` will eventually get stuck.

> Note: when testing this, I made sure that the handling of the `space`
key (in a `keydown` handler) and the clicking of the mouse (handled in a
`click` handler) called the exact same code. It still happened.

The debugging continues…

One thing I noticed is that when the `Popover` gets stuck, it meant that
a transition didn't properly complete.

The current implementation of the internal `useTransition(…)` hook has
to wait for all the transitions to finish. This is done using a
`waitForTransition(…)` helper. This helper sets up some event listeners
(`transitionstart`, `transitionend`, …) and waits for them to fire.

This seems to be unreliable on Firefox for some unknown reason.

I knew the code for waiting for transitions wasn't ideal, so I wanted to
see if using the native `node.getAnimations()` simplifies this and makes
it work in general.

Lo and behold, it did! 🎉

This now has multiple benefits:

1. It works as expected on Firefox
2. The code is much much simpler
3. Uses native features

The `getAnimations(…)` function is supported in all modern browsers
(since 2020). At the time it was too early to rely on it, but right now
it should be safe to use.

Fixes: tailwindlabs/tailwindui-issues#1625
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants