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 transition bug on Firefox, triggered by clicking the PopoverButton in rapid succession #3452

Merged
merged 5 commits into from
Sep 4, 2024

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Sep 4, 2024

We recently landed a fix for Popovers 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 space 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

Copy link

vercel bot commented Sep 4, 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 4, 2024 0:11am
headlessui-vue ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 4, 2024 0:11am

Up until now, we've tried to do this ourselves by listening to the
correct events. The new `node.getAnimations()` API is a much simpler API
to use.

The only requirement is that we call `node.getAnimations()` in a
`requestAnimationFrame` so that the browser can flush all the changes.
We couldn't do this before, because we needed to setup the event
listeners to prevent race conditions.

Now there are no race conditions, in fact, if all transitions already
complete before we can call the `waitForTransition`, then the
`node.getAnimations()` list will be empty and we can call the `done()`
function.

The `Element.prototype.getAnimations` has been available in browsers
since mid 2020, but at the time it was too new to use. Now seems like a
safe time to use this.

See: https://developer.mozilla.org/en-US/docs/Web/API/Element/getAnimations#browser_compatibility
The order slightly changed here, but it looks more correct now as well.
The `child-2-1` and `child-2-2` have the same duration/delay, so the
order that they are rendered in seems to be the correct order.

That said, these DOM nodes are on the same level (siblings), so for
these callbacks we are testing here the order doesn't really matter. The
order that _does_ matter is that the `after*` events of a parent fire
_after_ the `after*` events of the children, which it still does.
@RobinMalfait RobinMalfait force-pushed the fix/improve-transition-management branch from fe1b1a6 to aa9e189 Compare September 4, 2024 11:57
@RobinMalfait RobinMalfait changed the title Improve waiting for transitions Fix bug when waiting for transitions when clicking the PopoverButton in rapid succession on Firefox Sep 4, 2024
@RobinMalfait RobinMalfait force-pushed the fix/improve-transition-management branch from 4bdddbe to 03da4d5 Compare September 4, 2024 12:05
@RobinMalfait RobinMalfait changed the title Fix bug when waiting for transitions when clicking the PopoverButton in rapid succession on Firefox Fix transition bug on Firefox, triggered by clicking the PopoverButton in rapid succession Sep 4, 2024
'child-2-1: afterEnter',
'child-2-2: afterEnter',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why the order of these changed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's based on the order that the setTimeout is being called. I can get the old behavior by applying this diff:

diff --git a/packages/@headlessui-react/jest.setup.js b/packages/@headlessui-react/jest.setup.js
index 771a30cb..9e656463 100644
--- a/packages/@headlessui-react/jest.setup.js
+++ b/packages/@headlessui-react/jest.setup.js
@@ -15,9 +15,11 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true
         this.duration = duration
       }
 
-      finished = new Promise((resolve) => {
-        setTimeout(resolve, this.duration)
-      })
+      get finished() {
+        return new Promise((resolve) => {
+          setTimeout(resolve, this.duration)
+        })
+      }
     }
   }

But the exact reason why I am still unsure about. The order is also only different on the same level (siblings) which is the less important part I think. The more important part is the relation between child and parent and that that order is correct.

That said, note that the order here (and in the old code) was determined by the setTimeout (not by the real events because JSDOM doesn't have proper support for them all).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could revert the change to the test, and apply the diff above. But it feels incorrect since that would rely on us accessing the finished property, instead of relying on the creation of the CSSTransition object.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that's fine — was just curious and hoping it wasn't a side effect of some "problem"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, if you look at the browser example of this:

The output is in the exact same order (notice the order of the siblings "overlay" and "panel")

Main version:

Events:
2024-09-04T14:04:49.428Z - [Root] Before enter
2024-09-04T14:04:49.430Z - [Overlay] Before enter
2024-09-04T14:04:49.430Z - [Panel] Before enter
2024-09-04T14:04:49.765Z - [Overlay] After enter
2024-09-04T14:04:49.794Z - [Panel] After enter
2024-09-04T14:04:49.794Z - [Root] After enter
2024-09-04T14:04:50.873Z - [Overlay] Before leave
2024-09-04T14:04:50.873Z - [Panel] Before leave
2024-09-04T14:04:50.873Z - [Root] Before leave
2024-09-04T14:04:51.116Z - [Overlay] After leave
2024-09-04T14:04:51.124Z - [Panel] After leave
2024-09-04T14:04:51.124Z - [Root] After leave

PR version:

Events:
2024-09-04T14:04:17.440Z - [Root] Before enter
2024-09-04T14:04:17.441Z - [Overlay] Before enter
2024-09-04T14:04:17.441Z - [Panel] Before enter
2024-09-04T14:04:17.781Z - [Overlay] After enter
2024-09-04T14:04:17.820Z - [Panel] After enter
2024-09-04T14:04:17.820Z - [Root] After enter
2024-09-04T14:04:19.504Z - [Overlay] Before leave
2024-09-04T14:04:19.504Z - [Panel] Before leave
2024-09-04T14:04:19.504Z - [Root] Before leave
2024-09-04T14:04:19.765Z - [Overlay] After leave
2024-09-04T14:04:19.767Z - [Panel] After leave
2024-09-04T14:04:19.767Z - [Root] After leave

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweet 👍

@RobinMalfait RobinMalfait merged commit 971ff6b into main Sep 4, 2024
8 checks passed
@RobinMalfait RobinMalfait deleted the fix/improve-transition-management branch September 4, 2024 14:09
@razzeee
Copy link

razzeee commented Sep 9, 2024

We now have jest tests failing with


    TypeError: node.getAnimations is not a function

      at waitForTransition (node_modules/@headlessui/react/dist/headlessui.dev.cjs:3752:26)
      at node_modules/@headlessui/react/dist/headlessui.dev.cjs:3739:13
      at invokeTheCallbackFunction (node_modules/jsdom/lib/jsdom/living/generated/Function.js:19:26)
      at runAnimationFrameCallbacks (node_modules/jsdom/lib/jsdom/browser/Window.js:603:13)
      at Timeout.<anonymous> (node_modules/jsdom/lib/jsdom/browser/Window.js:581:11)

@corneliusroemer
Copy link

Indeed, also having a regression, see #3469

@jrmuizel
Copy link

jrmuizel commented Oct 3, 2024

@RobinMalfait we got a bug report on linktr.ee that I believe might be fixed by this. https://bugzilla.mozilla.org/show_bug.cgi?id=1916176

I'd like to better understand why Firefox isn't firing the transitionend events that you expect. Do you have a standalone test case that reproduces this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bug on headlessui Popover
5 participants