[RAC] Stacked modals: closing overlay dismisses the one beneath it during its exit animation
- 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
With stacked modals (a `Modal`/`ModalOverlay` opened on top of another), closing the top modal while it has an exit animation can cause the modal **underneath** it to be dismissed by an outside-click that actually landed on the still-animating top modal.
### 🤔 Expected Behavior?
A modal that's still mounted and visually on top (mid exit-animation) should keep owning pointer interactions that land on it. Only a click genuinely outside all modals should dismiss the one beneath.
### 😯 Current Behavior
`useOverlay` only lets the **topmost** entry in its `visibleOverlays` stack react to outside interactions, but stack membership is keyed on `isOpen` — an overlay is spliced out the instant it starts closing:
https://github.com/adobe/react-spectrum/blob/54d1b2b3c8c2a84fd54cccadca2d9cb5d742b943/packages/react-aria/src/overlays/useOverlay.ts#L80-L91
react-aria-components, however, keeps the closing overlay **mounted** (backdrop + content, still on top, still hit-testable) until its exit animation finishes:
https://github.com/adobe/react-spectrum/blob/54d1b2b3c8c2a84fd54cccadca2d9cb5d742b943/packages/react-aria-components/src/Modal.tsx#L205-L206
So during the exit animation the top overlay is gone from the stack but still physically covers the one beneath. A pointer interaction landing on it is, for the overlay beneath, "outside", and since that overlay is now `visibleOverlays[last]`, the handler fires `onHide()` and closes it:
https://github.com/adobe/react-spectrum/blob/54d1b2b3c8c2a84fd54cccadca2d9cb5d742b943/packages/react-aria/src/overlays/useOverlay.ts#L94-L126
The "only the topmost visible overlay reacts to outside interactions" invariant is silently broken for any overlay mid exit-animation.
### 💁 Possible Solution
Keep an overlay in `visibleOverlays` until it actually unmounts, so it stays topmost as long as it's physically on top.
### 🔦 Context
We have a picker modal inside a dialog (pick an item, the picker closes, click something on the dialog, which lands on the exiting picker). Worked around it with a `shouldCloseOnInteractOutside` on the outer modal, ignoring interactions whose target is inside a modal.
### 🖥️ Steps to Reproduce
1. Stack dismissable `ModalOverlay` `A`, then `B` on top, both with `isExiting` exit animations.
2. Close `B`
3. While `B` is closing, click again
4. `A` will close because clicking on the exiting `B` is considered outside `A`
Code sandbox: https://codesandbox.io/p/sandbox/dvnjy5
https://github.com/user-attachments/assets/58b286f1-6ab7-40cf-9491-c0efb822a848
### Version
react-aria-components 1.19.0 (react-aria 3.50.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 packages/react-aria/src/overlays/useOverlay.ts, especially the visibleOverlays stack and outside-interaction handling, then compare the mounting behavior in packages/react-aria-components/src/Modal.tsx. Reproduce the stacked-modal case in the linked CodeSandbox; done means an exiting top modal remains the interaction owner until it unmounts, so the underlying modal does not dismiss.
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
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100