reactjs / react-modal

Accessible modal dialog component for React
http://reactcommunity.org/react-modal
MIT License
7.35k stars 810 forks source link

after-open CSS transitions fail in Safari 14 & mobile Safari #846

Open benjaminpadula opened 3 years ago

benjaminpadula commented 3 years ago

Summary:

CSS after-open transitions are failing to fire in Safari 14 & mobile Safari

Steps to reproduce:

  1. Open any modal with a defined CSS transition in Chrome or Safari <14 (after-open transition works)
  2. Open any modal with a defined CSS transition in Safari 14 or mobile Safari (after-open transition fails)

Expected behavior:

Transitions should always work when defined correctly no matter what browser is being used

Link to example of issue:

Existing (broken): https://codesandbox.io/s/heuristic-jennings-8c54r (CSS transition works in Chrome and old Safari browsers, but NOT in Safari 14+ or mobile Safari)

"Fixed": https://codesandbox.io/s/safari-14-bug-experimental-zlzdl ("fixed" using onAfterOpen with special consumer-defined show-open class)

Additional notes:

It struck me that perhaps the issue was that when the modal is mounted, it is immediately mounted with the open state and initially renders with the after-open class. It's actually kind of surprising, then that Chrome shows a transition, when technically nothing is really changing. Perhaps the old browser interpreter is inferring developer intent or doing something wrong. ¯\_(ツ)_\/¯

So I thought it was worth a try to ensure that the modal mounts in the closed state first, and then apply the open class. (See modified sandbox) And that seems to do the trick! But since it's likely a problem affecting everyone, I decided to try fixing it directly in react-modal so everyone could benefit from working transitions in Safari 14 & mobile Safari.

Moral: When using CSS transitions, ensure that the component is rendered first before applying the class that triggers the transition. Test in Safari 14 to ensure you got it right, as other browser seem to be more lenient.

wittenbrock commented 3 years ago

I also ran into this issue.

In Safari 14 and iOS 14, CSS transitions don't work correctly with React Modal's transition class ReactModal__Overlay--after-open. Instead of transitioning, the element instantly appears. Here is where React Modal's documentation explains how to apply transitions.

My Solution

In my project, I have a fullscreen modal that is off-screen. When the modal is opened, I want it to slide into view from the left. When the modal is closed, I want it to slide out of view.

To do that, I set the modal's default state to be off-screen by translating it completely to the left. Here the class .fullscreen-menu is the modal's parent container.

.fullscreen-menu .ReactModal__Overlay {
  transform: translateX(-100%);
}

Next, I create a helper function. Instead of applying the transition with the CSS class ReactModal__Overlay--after-open, I apply it with JavaScript. The secret sauce here seems to be to wrap the function in a setTimeout. Smarter people than me will know why this works; I can't explain it. It probably has something to do with the event loop.

const slideIn = () => {
  setTimeout(() => {
    // Get React Modal's overlay
    const overlay = document.querySelector(
      '.fullscreen-menu .ReactModal__Overlay'
    );

    // If the modal hasn't rendered correctly and is null, exit the function to prevent errors
    if (!overlay) return;

    // Apply the transition, and slide the modal into view from the left
    overlay.style.transform = 'translateX(0)';
  }, 0);
};

Now, when I instantiate my <Modal /> component, I call the function slideIn in React Modal's onAfterOpen prop.

<Modal
      isOpen={fullscreenMenuIsOpen}
      onRequestClose={closeFullscreenMenu}
      onAfterOpen={() => {
        slideIn();
      }}
      portalClassName="fullscreen-menu"
    >
    ...
</Modal>   

So, it appears that if you apply CSS transitions with JavaScript like this in conjunction with setTimeout, they will work as expected in iOS 14 and Safari 14.