diff --git a/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/NestedDialogsExample.tsx b/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/NestedDialogsExample.tsx
new file mode 100644
index 0000000000..8e5b901f1e
--- /dev/null
+++ b/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/NestedDialogsExample.tsx
@@ -0,0 +1,43 @@
+"use client";
+import { Button } from "@react-md/core/button/Button";
+import { Dialog } from "@react-md/core/dialog/Dialog";
+import { DialogContent } from "@react-md/core/dialog/DialogContent";
+import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
+import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
+import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
+import { useToggle } from "@react-md/core/useToggle";
+import { type ReactElement } from "react";
+
+export default function NestedDialogsExample(): ReactElement {
+ const { toggle, toggled } = useToggle();
+ return ;
+}
+
+interface InfiniteDialogProps {
+ depth: number;
+ closeAll(): void;
+}
+
+function InfiniteDialog(props: InfiniteDialogProps): ReactElement {
+ const { depth, closeAll } = props;
+ const { enable: show, disable: hide, toggled: visible } = useToggle();
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/NestedDialogsVisibleExample.tsx b/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/NestedDialogsVisibleExample.tsx
new file mode 100644
index 0000000000..5056f69c6c
--- /dev/null
+++ b/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/NestedDialogsVisibleExample.tsx
@@ -0,0 +1,51 @@
+"use client";
+import { Button } from "@react-md/core/button/Button";
+import { Dialog } from "@react-md/core/dialog/Dialog";
+import { DialogContent } from "@react-md/core/dialog/DialogContent";
+import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
+import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
+import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
+import { useToggle } from "@react-md/core/useToggle";
+import { useEffect, type ReactElement } from "react";
+
+export default function NestedDialogsVisibleExample(): ReactElement {
+ const { toggle, toggled } = useToggle();
+ return ;
+}
+
+interface InfiniteDialogProps {
+ depth: number;
+ closeAll(): void;
+}
+
+function InfiniteDialog(props: InfiniteDialogProps): ReactElement {
+ const { depth, closeAll } = props;
+
+ const defaultVisible = depth > 0 && depth < 3;
+ // try setting `useToggle(defaultVisible)` to see the difference
+ const { enable: show, disable: hide, toggled: visible } = useToggle();
+ useEffect(() => {
+ if (defaultVisible) {
+ show();
+ }
+ }, [defaultVisible, show]);
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/page.mdx b/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/page.mdx
index d4d6ce1279..3db4e03d3b 100644
--- a/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/page.mdx
+++ b/apps/docs/src/app/(main)/(markdown)/(demos)/components/dialog/page.mdx
@@ -83,3 +83,28 @@ the visibility transitions and can be customized by providing the `timeout` and
```demo source="./CustomTransitionExample.tsx"
```
+
+# Nested Dialogs
+
+Dialogs can be nested without any additional setup since the dialogs are
+portalled behind the scenes and only the topmost overlay will be shown at a
+time.
+
+> !Info! If multiple overlays were shown at the same time, the overlay would
+> become darker as more dialogs are shown which can cause performance issues on
+> mobile devices.
+
+```demo source="./NestedDialogsExample.tsx"
+
+```
+
+## Nested Dialogs Default Visible
+
+If nested dialogs should be visible by on initial page load or mount, trigger
+the `show` behavior into a `useEffect` instead of setting the initial state to
+`true`. If multiple dialogs are rendered at once, the topmost dialog will be
+shown instead of the child dialogs due to how React renders.
+
+```demo source="./NestedDialogsVisibleExample.tsx"
+
+```
diff --git a/apps/docs/src/components/ReturnToTop.tsx b/apps/docs/src/components/ReturnToTop.tsx
index bdc32c5b67..f8718e218d 100644
--- a/apps/docs/src/components/ReturnToTop.tsx
+++ b/apps/docs/src/components/ReturnToTop.tsx
@@ -3,7 +3,7 @@ import { TooltippedButton } from "@react-md/core/button/TooltippedButton";
import {
DEFAULT_DIALOG_CLASSNAMES,
DEFAULT_DIALOG_TIMEOUT,
-} from "@react-md/core/dialog/Dialog";
+} from "@react-md/core/dialog/styles";
import { useCSSTransition } from "@react-md/core/transition/useCSSTransition";
import { useIntersectionObserver } from "@react-md/core/useIntersectionObserver";
import ArrowUpwardIcon from "@react-md/material-icons/ArrowUpwardIcon";
diff --git a/packages/core/src/dialog/Dialog.tsx b/packages/core/src/dialog/Dialog.tsx
index dee478b8e8..727296c771 100644
--- a/packages/core/src/dialog/Dialog.tsx
+++ b/packages/core/src/dialog/Dialog.tsx
@@ -252,10 +252,10 @@ export const Dialog = forwardRef(
exit,
onEnter = noop,
onEntering = noop,
- onEntered,
+ onEntered = noop,
onExit = noop,
onExiting = noop,
- onExited,
+ onExited = noop,
exitedHidden = true,
disableOverlay = false,
overlayProps,
@@ -273,13 +273,29 @@ export const Dialog = forwardRef(
const ssr = useSsr();
const setChildVisible = useNestedDialogContext();
+
+ // this makes it so that as more non-full page dialogs become visible, the
+ // overlay does not become darker as more and more overlays are stacked upon
+ // each other. only the top-most overlay will have and active background
+ // color.
+ const [isChildVisible, setIsChildVisible] = useState(false);
const { eventHandlers, transitionOptions } = useFocusContainer({
nodeRef: ref,
activate: visible,
- onEntered,
+ onEntered(appear) {
+ onEntered(appear);
+ // this needs to be called onEnter and onEntered just in case the
+ // transition is disabled
+ setChildVisible(type !== "full-page");
+ },
onEntering,
onExiting,
- onExited,
+ onExited() {
+ onExited();
+ // this needs to be called onExit and onExited just in case the
+ // transition is disabled
+ setChildVisible(false);
+ },
disableTransition,
onKeyDown(event) {
onKeyDown(event);
@@ -306,6 +322,7 @@ export const Dialog = forwardRef(
type,
fixed,
outline: !disableFocusOutline,
+ disableBoxShadow: isChildVisible,
className,
}),
appear: appear && !disableTransition && !ssr,
@@ -326,12 +343,6 @@ export const Dialog = forwardRef(
});
useScrollLock(!disableScrollLock && visible);
- // this makes it so that as more non-full page dialogs become visible, the
- // overlay does not become darker as more and more overlays are stacked upon
- // each other. only the top-most overlay will have and active background
- // color.
- const [isChildVisible, setIsChildVisible] = useState(false);
-
return (
{!disableOverlay && (
diff --git a/packages/core/src/dialog/__tests__/Dialog.tsx b/packages/core/src/dialog/__tests__/Dialog.tsx
index f0d3d3ed4c..119e270bfd 100644
--- a/packages/core/src/dialog/__tests__/Dialog.tsx
+++ b/packages/core/src/dialog/__tests__/Dialog.tsx
@@ -264,7 +264,7 @@ describe("Dialog", () => {
expect(document.body).not.toHaveStyle("overflow: hidden");
});
- it("should enable the noOpacity prop on parent dialog overlay elements so the screen does not get darker as more dialogs are visible", async () => {
+ it("should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible", async () => {
const user = userEvent.setup();
function InfiniteDialog({ depth }: { depth: number }): ReactElement {
const { toggled, enable, disable } = useToggle();
@@ -295,17 +295,34 @@ describe("Dialog", () => {
await user.click(screen.getByRole("button", { name: "Show" }));
const overlay = screen.getByTestId("overlay");
+ const dialog = screen.getByRole("dialog", { name: "Dialog" });
+ expect(overlay).toHaveClass("rmd-overlay--active");
+ expect(dialog).not.toHaveClass("rmd-dialog--no-box-shadow");
expect(overlay.className).toMatchSnapshot();
+ expect(dialog.className).toMatchSnapshot();
await user.click(screen.getByRole("button", { name: "Show 1" }));
const overlay1 = screen.getByTestId("overlay1");
+ const dialog1 = screen.getByRole("dialog", { name: "Dialog 1" });
+ expect(overlay).not.toHaveClass("rmd-overlay--active");
+ expect(overlay1).toHaveClass("rmd-overlay--active");
+ expect(dialog).toHaveClass("rmd-dialog--no-box-shadow");
+ expect(dialog1).not.toHaveClass("rmd-dialog--no-box-shadow");
expect(overlay.className).toMatchSnapshot();
expect(overlay1.className).toMatchSnapshot();
+ expect(dialog.className).toMatchSnapshot();
+ expect(dialog1.className).toMatchSnapshot();
await user.click(screen.getByRole("button", { name: "Show 2" }));
+ expect(overlay).not.toHaveClass("rmd-overlay--active");
+ expect(overlay1).not.toHaveClass("rmd-overlay--active");
+ expect(dialog).toHaveClass("rmd-dialog--no-box-shadow");
+ expect(dialog1).toHaveClass("rmd-dialog--no-box-shadow");
expect(overlay.className).toMatchSnapshot();
expect(overlay1.className).toMatchSnapshot();
+ expect(dialog.className).toMatchSnapshot();
+ expect(dialog1.className).toMatchSnapshot();
});
});
diff --git a/packages/core/src/dialog/__tests__/__snapshots__/Dialog.tsx.snap b/packages/core/src/dialog/__tests__/__snapshots__/Dialog.tsx.snap
index ae2b28ea65..4b16b35a99 100644
--- a/packages/core/src/dialog/__tests__/__snapshots__/Dialog.tsx.snap
+++ b/packages/core/src/dialog/__tests__/__snapshots__/Dialog.tsx.snap
@@ -71,14 +71,24 @@ exports[`Dialog should be able to render as a modal with the correct accessibili
/>
`;
-exports[`Dialog should enable the noOpacity prop on parent dialog overlay elements so the screen does not get darker as more dialogs are visible 1`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center rmd-overlay--active"`;
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 1`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center rmd-overlay--active"`;
-exports[`Dialog should enable the noOpacity prop on parent dialog overlay elements so the screen does not get darker as more dialogs are visible 2`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center rmd-overlay--active"`;
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 2`] = `"rmd-dialog rmd-dialog--outline rmd-dialog--centered"`;
-exports[`Dialog should enable the noOpacity prop on parent dialog overlay elements so the screen does not get darker as more dialogs are visible 3`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center rmd-overlay--active"`;
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 3`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center"`;
-exports[`Dialog should enable the noOpacity prop on parent dialog overlay elements so the screen does not get darker as more dialogs are visible 4`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center rmd-overlay--active"`;
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 4`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center rmd-overlay--active"`;
-exports[`Dialog should enable the noOpacity prop on parent dialog overlay elements so the screen does not get darker as more dialogs are visible 5`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center rmd-overlay--active"`;
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 5`] = `"rmd-dialog rmd-dialog--outline rmd-dialog--centered rmd-dialog--no-box-shadow"`;
+
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 6`] = `"rmd-dialog rmd-dialog--outline rmd-dialog--centered"`;
+
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 7`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center"`;
+
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 8`] = `"rmd-overlay rmd-overlay--visible rmd-overlay--clickable rmd-box rmd-box--gap rmd-box--wrap rmd-box--align-center rmd-box--justify-center"`;
+
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 9`] = `"rmd-dialog rmd-dialog--outline rmd-dialog--centered rmd-dialog--no-box-shadow"`;
+
+exports[`Dialog should handle nested dialogs by preventing the overlay and box-shadow from getting darker as more dialogs become visible 10`] = `"rmd-dialog rmd-dialog--outline rmd-dialog--centered rmd-dialog--no-box-shadow"`;
exports[`dialog class name utility should be callable with no arguments 1`] = `"rmd-dialog rmd-dialog--centered"`;
diff --git a/packages/core/src/dialog/_dialog.scss b/packages/core/src/dialog/_dialog.scss
index 0f06698d00..ba66207979 100644
--- a/packages/core/src/dialog/_dialog.scss
+++ b/packages/core/src/dialog/_dialog.scss
@@ -145,6 +145,10 @@ $variables: (
width: 100%;
}
+ &--no-box-shadow {
+ box-shadow: none;
+ }
+
@if not $disable-focus-outline {
// Note: Do not use the `interaction-outline` mixin +
// `interaction.set-var(interaction.$focus-color)` like normal focus
diff --git a/packages/core/src/dialog/styles.ts b/packages/core/src/dialog/styles.ts
index 3d82edcb33..6f3ddba705 100644
--- a/packages/core/src/dialog/styles.ts
+++ b/packages/core/src/dialog/styles.ts
@@ -42,6 +42,14 @@ export interface DialogClassNameOptions {
* @defaultValue `type === "full-page"`
*/
outline?: boolean;
+
+ /**
+ * This is mostly used for handling nested dialogs and removes any box shadow
+ * on a dialog that has a child visible.
+ *
+ * @defaultValue `false`
+ */
+ disableBoxShadow?: boolean;
}
/** @since 6.0.0 */
@@ -50,6 +58,7 @@ export function dialog(options: DialogClassNameOptions = {}): string {
type = "centered",
fixed = false,
outline = type === "full-page",
+ disableBoxShadow,
className,
} = options;
@@ -59,6 +68,7 @@ export function dialog(options: DialogClassNameOptions = {}): string {
outline,
centered: type === "centered",
"full-page": type === "full-page",
+ "no-box-shadow": type === "centered" && disableBoxShadow,
}),
className
);