-
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 3 commits
5ec400d
cb6dcd3
7fdda00
4089a0b
4383df2
32c849a
e3b330a
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,94 @@ | ||
| # Nested Dialogs | ||
|
paolo-aliprandi marked this conversation as resolved.
|
||
|
|
||
| When implementing nested dialogs (a dialog opened from within another dialog), proper focus management is critical to ensure users can navigate back through the dialog stack correctly. | ||
|
|
||
| ## Important: Nested Dialogs Should Be Closed Programmatically | ||
|
|
||
| Nested dialogs should **always be closed programmatically** through state management, not manually by clicking outside or pressing Escape. This ensures predictable focus behavior and a better user experience. | ||
|
paolo-aliprandi marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Two Approaches | ||
|
|
||
| ### Approach 1: Using DialogTrigger (Recommended for User-Triggered Opens) | ||
|
|
||
| If the nested dialog is opened by a button click (user interaction), use `DialogTrigger` for automatic focus restoration: | ||
|
|
||
| ```tsx | ||
| <Dialog> | ||
| <DialogTrigger disableButtonEnhancement> | ||
| <Button>Open Outer Dialog</Button> | ||
| </DialogTrigger> | ||
| <DialogSurface> | ||
| <DialogBody> | ||
| <DialogTitle>Outer Dialog</DialogTitle> | ||
| <Dialog> | ||
| <DialogTrigger disableButtonEnhancement> | ||
| <Button>Open Inner Dialog</Button> | ||
| </DialogTrigger> | ||
| <DialogSurface>{/* Inner dialog content */}</DialogSurface> | ||
| </Dialog> | ||
| </DialogBody> | ||
| </DialogSurface> | ||
| </Dialog> | ||
| ``` | ||
|
|
||
| **Benefits:** | ||
|
|
||
| - Focus is automatically restored when dialogs close | ||
| - Simpler to implement | ||
| - No need for manual focus management hooks | ||
|
|
||
| ### Approach 2: Using Focus Restoration Hooks (For Programmatic Control) | ||
|
|
||
| If the nested dialog is opened programmatically (not triggered by user interaction), use `useRestoreFocusSource()` and `useRestoreFocusTarget()` hooks: | ||
|
|
||
| ```tsx | ||
| const [outerOpen, setOuterOpen] = React.useState(false); | ||
| const [innerOpen, setInnerOpen] = React.useState(false); | ||
|
|
||
| const outerSourceAttrs = useRestoreFocusSource(); | ||
| const outerTargetAttrs = useRestoreFocusTarget(); | ||
| const innerSourceAttrs = useRestoreFocusSource(); | ||
| const innerTargetAttrs = useRestoreFocusTarget(); | ||
|
|
||
| return ( | ||
| <Dialog open={outerOpen} onOpenChange={(e, data) => setOuterOpen(data.open)}> | ||
| <Button {...outerTargetAttrs} onClick={() => setOuterOpen(true)}> | ||
| Open Outer Dialog | ||
| </Button> | ||
| <DialogSurface {...outerSourceAttrs}> | ||
| <Button {...innerTargetAttrs} onClick={() => setInnerOpen(true)}> | ||
|
paolo-aliprandi marked this conversation as resolved.
Outdated
|
||
| Open Inner Dialog | ||
| </Button> | ||
| </DialogSurface> | ||
|
|
||
| <Dialog open={innerOpen} onOpenChange={(e, data) => setInnerOpen(data.open)}> | ||
| <DialogSurface {...innerSourceAttrs}>{/* Inner dialog content */}</DialogSurface> | ||
| </Dialog> | ||
|
paolo-aliprandi marked this conversation as resolved.
Outdated
|
||
| </Dialog> | ||
| ); | ||
| ``` | ||
|
|
||
| **How it works:** | ||
|
|
||
| - `useRestoreFocusTarget()` attributes go on the button/element that opens the dialog | ||
| - `useRestoreFocusSource()` attributes go on the DialogSurface | ||
| - When the dialog closes, focus returns to the element with `useRestoreFocusTarget()` attributes | ||
| - Each dialog pair (source/target) manages its own focus restoration | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Use DialogTrigger by default** - It provides automatic focus restoration for user-triggered opens | ||
| 2. **Use focus hooks for programmatic dialogs** - When you open dialogs from code (not user clicks), use the focus restoration hooks | ||
| 3. **Always apply focus attributes** - Don't skip focus management; it's essential for accessibility | ||
| 4. **Close dialogs programmatically** - Don't rely on manual close (clicking outside, pressing Escape) for nested dialogs | ||
| 5. **Test with keyboard navigation** - Verify that Tab and Shift+Tab work correctly through the dialog stack | ||
|
|
||
| ## Accessibility | ||
|
|
||
| Proper focus management in nested dialogs is crucial for: | ||
|
|
||
| - **Keyboard users** - They can navigate back through the dialog stack using Tab | ||
| - **Screen reader users** - Focus announcements help users understand which dialog is active | ||
| - **Motor control users** - They depend on consistent focus behavior for reliable navigation | ||
|
|
||
| See [useRestoreFocusSource hook documentation](/docs/utilities-focus-management-userestorefocussource--docs) for more details on focus management utilities. | ||
|
paolo-aliprandi marked this conversation as resolved.
Outdated
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import * as React from 'react'; | ||
| import type { JSXElement } from '@fluentui/react-components'; | ||
| import { | ||
| Dialog, | ||
| DialogSurface, | ||
| DialogTitle, | ||
| DialogBody, | ||
| DialogContent, | ||
| DialogActions, | ||
| Button, | ||
| useRestoreFocusSource, | ||
| useRestoreFocusTarget, | ||
| } from '@fluentui/react-components'; | ||
| import story from './DialogNestedDialogs.md'; | ||
|
|
||
| export const NestedDialogs = (): JSXElement => { | ||
| const [outerOpen, setOuterOpen] = React.useState(false); | ||
| const [innerOpen, setInnerOpen] = React.useState(false); | ||
|
|
||
| // Focus restoration for outer dialog | ||
| const outerRestoreFocusSourceAttributes = useRestoreFocusSource(); | ||
| const outerRestoreFocusTargetAttributes = useRestoreFocusTarget(); | ||
|
|
||
| // Focus restoration for inner dialog | ||
| const innerRestoreFocusSourceAttributes = useRestoreFocusSource(); | ||
| const innerRestoreFocusTargetAttributes = useRestoreFocusTarget(); | ||
|
|
||
| return ( | ||
| <div> | ||
| {/* Outer Dialog */} | ||
| <Dialog open={outerOpen} onOpenChange={(event, data) => setOuterOpen(data.open)}> | ||
| <Button {...outerRestoreFocusTargetAttributes} appearance="primary" onClick={() => setOuterOpen(true)}> | ||
| Open Outer Dialog | ||
| </Button> | ||
|
|
||
| <DialogSurface {...outerRestoreFocusSourceAttributes}> | ||
| <DialogBody> | ||
| <DialogTitle>Outer Dialog</DialogTitle> | ||
| <DialogContent>This is the outer dialog. Click the button below to open a nested dialog.</DialogContent> | ||
| <DialogActions> | ||
| <Button {...innerRestoreFocusTargetAttributes} appearance="primary" onClick={() => setInnerOpen(true)}> | ||
| Open Inner Dialog | ||
| </Button> | ||
| <Button appearance="secondary" onClick={() => setOuterOpen(false)}> | ||
| Close Outer Dialog | ||
| </Button> | ||
| </DialogActions> | ||
| </DialogBody> | ||
| </DialogSurface> | ||
| </Dialog> | ||
|
|
||
| {/* Inner Dialog */} | ||
| <Dialog open={innerOpen} onOpenChange={(event, data) => setInnerOpen(data.open)}> | ||
| <DialogSurface {...innerRestoreFocusSourceAttributes}> | ||
| <DialogBody> | ||
| <DialogTitle>Inner Dialog</DialogTitle> | ||
| <DialogContent> | ||
| This is a nested dialog inside the outer dialog. Focus will be restored to the outer dialog when this one | ||
| closes. | ||
| </DialogContent> | ||
| <DialogActions> | ||
| <Button appearance="primary">Confirm</Button> | ||
| <Button appearance="secondary" onClick={() => setInnerOpen(false)}> | ||
| Close Inner Dialog | ||
| </Button> | ||
| </DialogActions> | ||
| </DialogBody> | ||
| </DialogSurface> | ||
| </Dialog> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| NestedDialogs.parameters = { | ||
| docs: { | ||
| description: { | ||
| story, | ||
| }, | ||
| }, | ||
| }; |
|
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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| 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'; | ||
|
|
||
| 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 outer | ||
| dialog when this one closes thanks to DialogTrigger. | ||
|
paolo-aliprandi marked this conversation as resolved.
Outdated
|
||
| </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: [ | ||
| 'Using DialogTrigger for nested dialogs provides automatic focus restoration.', | ||
| 'This is the simpler and recommended approach when the dialogs are opened by user interaction.', | ||
| 'Focus management is handled automatically without needing useRestoreFocusSource and useRestoreFocusTarget hooks.', | ||
| ].join('\n'), | ||
| }, | ||
| }, | ||
|
paolo-aliprandi marked this conversation as resolved.
|
||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.