diff --git a/packages/react-components/react-dialog/stories/src/Dialog/DialogBestPractices.md b/packages/react-components/react-dialog/stories/src/Dialog/DialogBestPractices.md index 3717da47e72eff..ac1e4529c23073 100644 --- a/packages/react-components/react-dialog/stories/src/Dialog/DialogBestPractices.md +++ b/packages/react-components/react-dialog/stories/src/Dialog/DialogBestPractices.md @@ -4,12 +4,12 @@ - Dialog boxes consist of a header (`DialogTitle`), content (`DialogContent`), and footer (`DialogActions`), which should all be included inside a body (`DialogBody`). - Validate that people’s entries are acceptable before closing the dialog. Show an inline validation error near the field they must correct. -- Modal dialogs should be used very sparingly—only when it’s critical that people make a choice or provide information before they can proceed. Thee dialogs are generally used for irreversible or potentially destructive tasks. They’re typically paired with an backdrop without a light dismiss. +- Modal dialogs should be used very sparingly—only when it’s critical that people make a choice or provide information before they can proceed. These dialogs are generally used for irreversible or potentially destructive tasks. They’re typically paired with a backdrop without a light dismiss. - Add a `aria-describedby` attribute on `DialogSurface` pointing to the dialog content on short confirmation like dialogs. - Add a `aria-label` or `aria-labelledby` attribute on `DialogSurface` if there is no `DialogTitle` ### 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. They are an anti-pattern and should be avoided. Nested dialogs create complex focus restoration logic and confuse users. If your design requires stacking dialogs, consider using a multi-step wizard within a single dialog, sequential dialogs, or a different UI component (panels, sidebars, popovers). - Don't use a `Dialog` with no focusable elements diff --git a/packages/react-components/react-dialog/stories/src/Dialog/DialogNestedDialogs.md b/packages/react-components/react-dialog/stories/src/Dialog/DialogNestedDialogs.md new file mode 100644 index 00000000000000..10fe92fae2ea4e --- /dev/null +++ b/packages/react-components/react-dialog/stories/src/Dialog/DialogNestedDialogs.md @@ -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). diff --git a/packages/react-components/react-dialog/stories/src/Dialog/DialogNestedDialogsWithTrigger.stories.tsx b/packages/react-components/react-dialog/stories/src/Dialog/DialogNestedDialogsWithTrigger.stories.tsx new file mode 100644 index 00000000000000..621ac827239db7 --- /dev/null +++ b/packages/react-components/react-dialog/stories/src/Dialog/DialogNestedDialogsWithTrigger.stories.tsx @@ -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 ( + + + + + + + + Outer Dialog + + This is the outer dialog. Click the button below to open a nested dialog. When using DialogTrigger, focus is + automatically restored. + + + + + + + + + + Inner Dialog + + 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. + + + + + + + + + + + + + + + + + + + ); +}; + +NestedDialogsWithTrigger.parameters = { + docs: { + description: { + story, + }, + }, +}; diff --git a/packages/react-components/react-dialog/stories/src/Dialog/DialogTriggerOutsideDialog.md b/packages/react-components/react-dialog/stories/src/Dialog/DialogTriggerOutsideDialog.md index d0ff162cbbe8a0..50aec58904ec40 100644 --- a/packages/react-components/react-dialog/stories/src/Dialog/DialogTriggerOutsideDialog.md +++ b/packages/react-components/react-dialog/stories/src/Dialog/DialogTriggerOutsideDialog.md @@ -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: