diff --git a/.changeset/fruity-points-agree.md b/.changeset/fruity-points-agree.md new file mode 100644 index 000000000..48bf26856 --- /dev/null +++ b/.changeset/fruity-points-agree.md @@ -0,0 +1,5 @@ +--- +"@launchpad-ui/components": patch +--- + +fix a bug around autoclose behavior in modals when a toast is present diff --git a/packages/components/src/Modal.tsx b/packages/components/src/Modal.tsx index 012266631..d14acc037 100644 --- a/packages/components/src/Modal.tsx +++ b/packages/components/src/Modal.tsx @@ -94,6 +94,19 @@ const ModalOverlay = ({ isDismissable = true, ref, ...props }: ModalOverlayProps return ( { + // Don't close when the element being checked is HTML or BODY. + // This indicates a portal interaction (e.g., spawning a toast from a button). + // + // Why this is safe: + // - When users click the modal backdrop, the element is the overlay
, NOT HTML/BODY + // - HTML/BODY only appear here during React portal rendering edge cases + // - This prevents the modal from incorrectly closing when triggering toasts from modal buttons + // - Legitimate outside clicks (backdrop, other UI) still close the modal as expected + const isRootElement = element.tagName === 'HTML' || element.tagName === 'BODY'; + + return !isRootElement; + }} {...props} ref={ref} className={composeRenderProps(props.className, (className, renderProps) => diff --git a/packages/components/stories/Modal.stories.tsx b/packages/components/stories/Modal.stories.tsx index a2a5a9d75..12ee1d4fd 100644 --- a/packages/components/stories/Modal.stories.tsx +++ b/packages/components/stories/Modal.stories.tsx @@ -11,6 +11,7 @@ import { Heading } from '../src/Heading'; import { IconButton } from '../src/IconButton'; import { Modal, ModalOverlay } from '../src/Modal'; import { Text } from '../src/Text'; +import { ToastRegion, toastQueue } from '../src/Toast'; const meta: Meta = { component: Modal, @@ -110,3 +111,73 @@ export const Drawer: Story = { }, }, }; + +/** + * Bug reproduction: Dialog closes when clicking a button inside it while Toast is active. + * + * Steps to reproduce: + * 1. Click "Show Toast" to display a toast notification + * 2. Click "Open Dialog" to open the modal + * 3. Click "Click Me" button inside the dialog + * + * Expected: Button click should work normally, dialog stays open + * Actual: Dialog closes unexpectedly + */ +export const DialogWithActiveToast: Story = { + render: (args) => { + return ( + <> + +
+ + + + + + {({ close }) => ( + <> +
+ Dialog with Toast Active + + Try clicking the button below with toast active +
+
+

+ When a toast is visible, clicking the button below should NOT close the + dialog. +

+
+ {' '} +
+
+
+ + +
+ + )} +
+
+
+
+
+ + ); + }, +};