Skip to content

FocusScope breaks when transitionend is never called #7973

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

Closed
MercerK opened this issue Mar 21, 2025 · 1 comment · Fixed by #8004
Closed

FocusScope breaks when transitionend is never called #7973

MercerK opened this issue Mar 21, 2025 · 1 comment · Fixed by #8004
Labels
bug Something isn't working

Comments

@MercerK
Copy link
Contributor

MercerK commented Mar 21, 2025

Provide a general summary of the issue here

I was working on creating a Dialog implementation from React Aria and noticed this weird bug that happens from time to time. Every so often, the focus management would break. After further investigation, it appears to be related to the runAfterTransition via focusSafety.

For example, when a user clicks on a button (such as a close button) that starts a transition, the container may suddenly become unmounted. If the transition doesn't finish, transitionend doesn't get called and the focusSafety gets stuck in a queue. When that happens, FocusScope no longer moves focus to within the scope and no longer restores it.

🤔 Expected Behavior?

FocusScope should always move focus when mounted, and restore focus when unmounted.

😯 Current Behavior

When an element is triggering a transition and is suddenly removed, FocusScope no longer moves focus when mounted and does not restore focus when unmounted.

💁 Possible Solution

One solution could be to loop through the elements in transitionsByElement, check whether they are still in the DOM, and when absent, remove them.

function removeRemovedElements() {
  for (const [element] of transitionsByElement) {
    if (element instanceof HTMLElement && !document.contains(element)) {
      transitionsByElement.delete(element);
    }
  }
}

export function runAfterTransition(fn: () => void): void {
  // Wait one frame to see if an animation starts, e.g. a transition on mount.
  requestAnimationFrame(() => {
    removeRemovedElements();
    // If no transitions are running, call the function immediately.
    // Otherwise, add it to a list of callbacks to run at the end of the animation.
    if (transitionsByElement.size === 0) {
      fn();
    } else {
      transitionCallbacks.add(fn);
    }
  });
}

🔦 Context

No response

🖥️ Steps to Reproduce

Screen.Recording.2025-03-21.at.1.16.12.PM.mov

Using keyboard, trigger open. Once opened, trigger Close to interrupt the transition.

https://codesandbox.io/p/sandbox/exciting-cookies-v9lwc5

Version

3.20.1

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

@snowystinger
Copy link
Member

That looks non-desirable.

Correct me if I'm wrong, but the flow is:

1. transition start
2. runAfterTransition, queues requestanimationframe
3. element removed, no transition end
4. raf runs
5. add callback when we should just call the fn

If so, then this solution looks fine to me.

Would you like to contribute the fix? https://react-spectrum.adobe.com/contribute.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
2 participants