-
Notifications
You must be signed in to change notification settings - Fork 2.9k
docs(react-dialog): add comprehensive nested dialogs documentation #36187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
5ec400d
cb6dcd3
7fdda00
4089a0b
4383df2
32c849a
e3b330a
cd15770
d7e1803
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Nested Dialogs | ||
|
|
||
| Nested dialogs (opening a dialog from within another dialog) are **an anti-pattern and should be avoided** whenever possible. Even when implementing proper focus management, nested dialogs create complex focus restoration logic and confuse users about their context. | ||
|
|
||
| ## Why Nested Dialogs Are Problematic | ||
|
|
||
| 1. **Focus Management Complexity** - Multiple dialog layers require careful keyboard and focus handling, increasing the risk of navigation bugs | ||
| 2. **User Confusion** - Users lose sense of their context when multiple overlays stack on top of each other | ||
| 3. **Accessibility Challenges** - Screen reader users and keyboard-only users struggle to manage multiple modal overlays | ||
| 4. **Anti-pattern by Design** - Modal dialogs are intentionally disruptive; stacking them compounds this problem | ||
|
|
||
| ## What to Do Instead | ||
|
|
||
| **Redesign your workflow** to eliminate the need for nested dialogs: | ||
|
|
||
| - **Single multi-step flow** - Use tabs, accordions, or numbered steps within a single dialog | ||
| - **Sequential dialogs** - Close the first dialog before opening the next one | ||
| - **Different UI patterns** - Consider panels, sidebars, popovers, or non-modal overlays | ||
| - **Inline content** - Expand/collapse sections within the dialog instead of opening new dialogs | ||
|
|
||
| ## If You Must Use Nested Dialogs (Rare) | ||
|
|
||
| Should your design truly require nested dialogs: | ||
|
|
||
| 1. Use `DialogTrigger` for user-triggered opens (it automatically restores focus) | ||
| 2. Use `useRestoreFocusTarget()` on elements that programmatically open dialogs | ||
| 3. Test thoroughly with keyboard navigation (Escape, Tab, Shift+Tab) | ||
| 4. Verify focus restoration works correctly for screen readers | ||
|
|
||
| For details on focus management utilities, see the [focus management documentation](https://react.fluentui.dev/?path=/docs/utilities-focus-management--docs). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We used to have a story for Nested dialogs usage before also. It was removed since, even though we clearly stated that is a bad pattern, people kept using it all the time. Our conclusion was: if this is a bad pattern we should most likely not be explaining how to "do it properly", it's an anti-pattern and we discourage it, find another solution. I would keep it as a section and an explanation, but I would avoid providing an example entirely, just to discourage even more the usage of it.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! I removed the anti-pattern example |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| import * as React from 'react'; | ||
| import type { JSXElement } from '@fluentui/react-components'; | ||
| import { | ||
| Dialog, | ||
| DialogTrigger, | ||
| DialogSurface, | ||
| DialogTitle, | ||
| DialogBody, | ||
| DialogContent, | ||
| DialogActions, | ||
| Button, | ||
| } from '@fluentui/react-components'; | ||
| import story from './DialogNestedDialogs.md'; | ||
|
|
||
| export const NestedDialogsWithTrigger = (): JSXElement => { | ||
| return ( | ||
| <Dialog> | ||
| <DialogTrigger disableButtonEnhancement> | ||
| <Button appearance="primary">Open Outer Dialog</Button> | ||
| </DialogTrigger> | ||
|
|
||
| <DialogSurface> | ||
| <DialogBody> | ||
| <DialogTitle>Outer Dialog</DialogTitle> | ||
| <DialogContent> | ||
| This is the outer dialog. Click the button below to open a nested dialog. When using DialogTrigger, focus is | ||
| automatically restored. | ||
| </DialogContent> | ||
| <DialogActions> | ||
| <Dialog> | ||
| <DialogTrigger disableButtonEnhancement> | ||
| <Button appearance="primary">Open Inner Dialog</Button> | ||
| </DialogTrigger> | ||
|
|
||
| <DialogSurface> | ||
| <DialogBody> | ||
| <DialogTitle>Inner Dialog</DialogTitle> | ||
| <DialogContent> | ||
| This is a nested dialog inside the outer dialog. Focus will automatically be restored to the Open | ||
| Inner Dialog button when this one closes thanks to DialogTrigger. | ||
| </DialogContent> | ||
| <DialogActions> | ||
| <Button appearance="primary">Confirm</Button> | ||
| <DialogTrigger disableButtonEnhancement> | ||
| <Button appearance="secondary">Close Inner Dialog</Button> | ||
| </DialogTrigger> | ||
| </DialogActions> | ||
| </DialogBody> | ||
| </DialogSurface> | ||
| </Dialog> | ||
|
|
||
| <DialogTrigger disableButtonEnhancement> | ||
| <Button appearance="secondary">Close Outer Dialog</Button> | ||
| </DialogTrigger> | ||
| </DialogActions> | ||
| </DialogBody> | ||
| </DialogSurface> | ||
| </Dialog> | ||
| ); | ||
| }; | ||
|
|
||
| NestedDialogsWithTrigger.parameters = { | ||
| docs: { | ||
| description: { | ||
| story, | ||
| }, | ||
| }, | ||
|
paolo-aliprandi marked this conversation as resolved.
|
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,11 @@ | ||
| When using a `Dialog` without a `DialogTrigger` (or when using a `DialogTrigger` outside of a `Dialog`), it becomes your responsibility to control some of the dialog's behavior. | ||
| When using a `Dialog` without a `DialogTrigger`, you become responsible for managing the dialog's behavior. This applies to: | ||
|
|
||
| 1. You must make sure that the `open` state is set accordingly to the dialog's visibility (mostly this means to properly react to the events provided by `onOpenChange` callback on `Dialog` component). | ||
| 2. You must make sure that focus is properly restored once the dialog is closed (this can be achieved by using the `useRestoreFocusTarget` hook, or by manually invoking `.focus()` on the target element). | ||
| - Opening dialogs programmatically (via state, API calls, side effects) | ||
| - Opening nested dialogs where the inner dialog is not wrapped in a `DialogTrigger` | ||
|
|
||
| The example bellow showcases both explicit responsibilities: | ||
| **Your responsibilities:** | ||
|
|
||
| 1. **Control the open state** - React to the `onOpenChange` callback and ensure the `open` state reflects the dialog's visibility | ||
| 2. **Restore focus** - When the dialog closes, you must restore focus to the element that triggered the open. Use `useRestoreFocusTarget` on the trigger element, or manually invoke `.focus()` on the target element. `DialogSurface` already applies the restore-focus source attributes internally when used inside `Dialog`. | ||
|
|
||
| The example below showcases both responsibilities: |
Uh oh!
There was an error while loading. Please reload this page.