Skip to content

Commit

Permalink
chore(dialog): update nested dialog css performance and fix disabled …
Browse files Browse the repository at this point in the history
…transitions
  • Loading branch information
mlaursen committed Sep 7, 2024
1 parent e27a8de commit 32456d7
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -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 <InfiniteDialog key={`${toggled}`} depth={0} closeAll={toggle} />;
}

interface InfiniteDialogProps {
depth: number;
closeAll(): void;
}

function InfiniteDialog(props: InfiniteDialogProps): ReactElement {
const { depth, closeAll } = props;
const { enable: show, disable: hide, toggled: visible } = useToggle();

return (
<>
<Button onClick={show}>Show</Button>
<Dialog aria-label="Dialog" visible={visible} onRequestClose={hide}>
<DialogHeader>
<DialogTitle>Dialog Depth {depth}</DialogTitle>
</DialogHeader>
<DialogContent>
<InfiniteDialog depth={depth + 1} closeAll={closeAll} />
</DialogContent>
<DialogFooter>
<Button theme="error" onClick={closeAll}>
Close All
</Button>
</DialogFooter>
</Dialog>
</>
);
}
Original file line number Diff line number Diff line change
@@ -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 <InfiniteDialog key={`${toggled}`} depth={0} closeAll={toggle} />;
}

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 (
<>
<Button onClick={show}>Show</Button>
<Dialog aria-label="Dialog" visible={visible} onRequestClose={hide}>
<DialogHeader>
<DialogTitle>Dialog Depth {depth}</DialogTitle>
</DialogHeader>
<DialogContent>
<InfiniteDialog depth={depth + 1} closeAll={closeAll} />
</DialogContent>
<DialogFooter>
<Button theme="error" onClick={closeAll}>
Close All
</Button>
</DialogFooter>
</Dialog>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
2 changes: 1 addition & 1 deletion apps/docs/src/components/ReturnToTop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
31 changes: 21 additions & 10 deletions packages/core/src/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,10 @@ export const Dialog = forwardRef<HTMLDivElement, DialogProps>(
exit,
onEnter = noop,
onEntering = noop,
onEntered,
onEntered = noop,
onExit = noop,
onExiting = noop,
onExited,
onExited = noop,
exitedHidden = true,
disableOverlay = false,
overlayProps,
Expand All @@ -273,13 +273,29 @@ export const Dialog = forwardRef<HTMLDivElement, DialogProps>(

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);
Expand All @@ -306,6 +322,7 @@ export const Dialog = forwardRef<HTMLDivElement, DialogProps>(
type,
fixed,
outline: !disableFocusOutline,
disableBoxShadow: isChildVisible,
className,
}),
appear: appear && !disableTransition && !ssr,
Expand All @@ -326,12 +343,6 @@ export const Dialog = forwardRef<HTMLDivElement, DialogProps>(
});
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 (
<NestedDialogProvider value={setIsChildVisible}>
{!disableOverlay && (
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/dialog/__tests__/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
});
});

Expand Down
20 changes: 15 additions & 5 deletions packages/core/src/dialog/__tests__/__snapshots__/Dialog.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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"`;
4 changes: 4 additions & 0 deletions packages/core/src/dialog/_dialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/dialog/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -50,6 +58,7 @@ export function dialog(options: DialogClassNameOptions = {}): string {
type = "centered",
fixed = false,
outline = type === "full-page",
disableBoxShadow,
className,
} = options;

Expand All @@ -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
);
Expand Down

0 comments on commit 32456d7

Please sign in to comment.