Skip to content

Commit 669bd06

Browse files
fix(components): addresses the modal closing when toasts appear bug (#1829)
1 parent 577b63b commit 669bd06

File tree

3 files changed

+89
-0
lines changed

3 files changed

+89
-0
lines changed

.changeset/fruity-points-agree.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@launchpad-ui/components": patch
3+
---
4+
5+
fix a bug around autoclose behavior in modals when a toast is present

packages/components/src/Modal.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ const ModalOverlay = ({ isDismissable = true, ref, ...props }: ModalOverlayProps
9494
return (
9595
<AriaModalOverlay
9696
isDismissable={isDismissable}
97+
shouldCloseOnInteractOutside={(element) => {
98+
// Don't close when the element being checked is HTML or BODY.
99+
// This indicates a portal interaction (e.g., spawning a toast from a button).
100+
//
101+
// Why this is safe:
102+
// - When users click the modal backdrop, the element is the overlay <div>, NOT HTML/BODY
103+
// - HTML/BODY only appear here during React portal rendering edge cases
104+
// - This prevents the modal from incorrectly closing when triggering toasts from modal buttons
105+
// - Legitimate outside clicks (backdrop, other UI) still close the modal as expected
106+
const isRootElement = element.tagName === 'HTML' || element.tagName === 'BODY';
107+
108+
return !isRootElement;
109+
}}
97110
{...props}
98111
ref={ref}
99112
className={composeRenderProps(props.className, (className, renderProps) =>

packages/components/stories/Modal.stories.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Heading } from '../src/Heading';
1111
import { IconButton } from '../src/IconButton';
1212
import { Modal, ModalOverlay } from '../src/Modal';
1313
import { Text } from '../src/Text';
14+
import { ToastRegion, toastQueue } from '../src/Toast';
1415

1516
const meta: Meta<typeof Modal> = {
1617
component: Modal,
@@ -110,3 +111,73 @@ export const Drawer: Story = {
110111
},
111112
},
112113
};
114+
115+
/**
116+
* Bug reproduction: Dialog closes when clicking a button inside it while Toast is active.
117+
*
118+
* Steps to reproduce:
119+
* 1. Click "Show Toast" to display a toast notification
120+
* 2. Click "Open Dialog" to open the modal
121+
* 3. Click "Click Me" button inside the dialog
122+
*
123+
* Expected: Button click should work normally, dialog stays open
124+
* Actual: Dialog closes unexpectedly
125+
*/
126+
export const DialogWithActiveToast: Story = {
127+
render: (args) => {
128+
return (
129+
<>
130+
<ToastRegion />
131+
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem' }}>
132+
<DialogTrigger>
133+
<Button variant="primary">Open Dialog</Button>
134+
<ModalOverlay>
135+
<Modal {...args}>
136+
<Dialog>
137+
{({ close }) => (
138+
<>
139+
<div slot="header">
140+
<Heading slot="title">Dialog with Toast Active</Heading>
141+
<IconButton
142+
aria-label="close"
143+
icon="cancel"
144+
size="small"
145+
variant="minimal"
146+
onPress={close}
147+
/>
148+
<Text slot="subtitle">Try clicking the button below with toast active</Text>
149+
</div>
150+
<div slot="body">
151+
<p>
152+
When a toast is visible, clicking the button below should NOT close the
153+
dialog.
154+
</p>
155+
<div style={{ marginTop: '1rem' }}>
156+
<Button
157+
onPress={() => {
158+
toastQueue.add({
159+
title: 'Toast is active',
160+
description: 'Now try clicking the button inside the dialog',
161+
status: 'info',
162+
});
163+
}}
164+
>
165+
Show Toast
166+
</Button>{' '}
167+
</div>
168+
</div>
169+
<div slot="footer">
170+
<Button slot="close">Cancel</Button>
171+
<Button variant="primary">Confirm</Button>
172+
</div>
173+
</>
174+
)}
175+
</Dialog>
176+
</Modal>
177+
</ModalOverlay>
178+
</DialogTrigger>
179+
</div>
180+
</>
181+
);
182+
},
183+
};

0 commit comments

Comments
 (0)