Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
### Don't

- Don't use more than three buttons between `DialogActions`.
- Don't open a `Dialog` from a `Dialog`
- Don't open nested `Dialog`s without proper focus management. Use `DialogTrigger` for automatic focus restoration when dialogs are opened by user interaction, or use `useRestoreFocusSource()` and `useRestoreFocusTarget()` hooks when opening dialogs programmatically. See the [Nested Dialogs](/docs/components-dialog--nested-dialogs) example for details.
- Don't use a `Dialog` with no focusable elements
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Nested Dialogs
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest removing this example since it is an anti-pattern and does not align with our guidance or recommendations. Instead, we should include a 'Programmatically open' example that explains if you use a dialog without a DialogTrigger, you are responsible for managing focus return. We offer hooks to assist with this, but it remains the consumer’s responsibility.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have this in docs

Image

So maybe we can just tweak the description a bit to make it easier to understand.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example removed. I improved the description of the existing section. @dmytrokirpa let me know if it is now clearer


Nested dialogs (opening a dialog from within another dialog) require proper focus management to ensure accessibility and predictable user experience.

## Key Principle

When using nested dialogs, **always use `DialogTrigger` for focus restoration**. If you open a dialog programmatically without `DialogTrigger`, you become responsible for managing focus restoration using `useRestoreFocusSource()` and `useRestoreFocusTarget()` hooks.

## Recommended: Using DialogTrigger

Use `DialogTrigger` for opening nested dialogs. This provides automatic focus restoration when dialogs close:

```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 manual focus management needed

## Programmatic Control: Using Focus Restoration Hooks

If you must open dialogs programmatically (without user click), use `useRestoreFocusSource()` and `useRestoreFocusTarget()` hooks. Note that you are responsible for ensuring focus is correctly restored:

```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)}>
Comment on lines +46 to +57
Open Inner Dialog
</Button>
</DialogSurface>

<Dialog open={innerOpen} onOpenChange={(e, data) => setInnerOpen(data.open)}>
<DialogSurface {...innerSourceAttrs}>{/* Inner dialog content */}</DialogSurface>
</Dialog>
Comment on lines +60 to +64
</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. **Test with keyboard navigation** - Verify that Escape key and backdrop clicks work correctly, and Tab/Shift+Tab navigate through the dialog stack

## Accessibility

Proper focus management in nested dialogs is crucial for:

- **Keyboard users** - They can close dialogs with Escape and navigate 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.
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.
Comment on lines +38 to +39
</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'),
},
},
Comment on lines +61 to +70
};
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 and `useRestoreFocusSource` on the `DialogSurface`, or manually invoke `.focus()` on the target element.

The example below showcases both responsibilities:
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { TitleCustomAction } from './DialogTitleCustomAction.stories';
export { TitleNoAction } from './DialogTitleNoAction.stories';
export { Confirmation } from './DialogConfirmation.stories';
export { MotionCustom } from './DialogMotionCustom.stories';
export { NestedDialogsWithTrigger } from './DialogNestedDialogsWithTrigger.stories';

// Typing with Meta<typeof Dialog> generates a type error for the `subcomponents` property.
// https://github.com/storybookjs/storybook/issues/27535
Expand Down
Loading