adobe / adobe/react-spectrum

[RAC] Stacked modals: closing overlay dismisses the one beneath it during its exit animation

Open
#10,291 4 comments 1 reaction 0 assignees View on GitHub
needs investigation
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.