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 @@ -8,9 +8,10 @@ import {
DialogTitle,
DialogTrigger,
} from '@fluentui/react-dialog';
import { OverlayDrawer, DrawerBody, DrawerHeader, DrawerHeaderTitle } from '@fluentui/react-drawer';
import { Button } from '@fluentui/react-button';
import { Combobox, Option } from '@fluentui/react-combobox';
import { Rocket24Regular } from '@fluentui/react-icons';
import { Rocket24Regular, Dismiss24Regular } from '@fluentui/react-icons';
import type { Meta } from '@storybook/react-webpack5';
import { getStoryVariant, DARK_MODE, HIGH_CONTRAST, RTL } from '../../utilities';

Expand Down Expand Up @@ -528,3 +529,149 @@ export const IntegrationComboboxInline = () => {
</Dialog>
);
};

export const DialogInsideDrawerDefault = () => (
<OverlayDrawer open position="start">
<DrawerHeader>
<DrawerHeaderTitle action={<Button appearance="subtle" aria-label="Close" icon={<Dismiss24Regular />} />}>
Drawer with Dialog
</DrawerHeaderTitle>
</DrawerHeader>
<DrawerBody>
<Dialog open>
<DialogTrigger>
<Button>Open dialog</Button>
</DialogTrigger>
<DialogSurface>
<DialogBody>
<DialogTitle>Dialog inside Drawer</DialogTitle>
<DialogContent>This dialog has the default transparent backdrop when nested inside a Drawer.</DialogContent>
<DialogActions>
<DialogTrigger>
<Button appearance="secondary">Close</Button>
</DialogTrigger>
<Button appearance="primary">Do Something</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</DrawerBody>
</OverlayDrawer>
);

DialogInsideDrawerDefault.storyName = 'dialog inside drawer default';

export const DialogInsideDrawerDefaultDarkMode = getStoryVariant(DialogInsideDrawerDefault, DARK_MODE);
export const DialogInsideDrawerDefaultHighContrast = getStoryVariant(DialogInsideDrawerDefault, HIGH_CONTRAST);
export const DialogInsideDrawerDefaultRTL = getStoryVariant(DialogInsideDrawerDefault, RTL);

export const DialogInsideDrawerDimmed = () => (
<OverlayDrawer open position="start">
<DrawerHeader>
<DrawerHeaderTitle action={<Button appearance="subtle" aria-label="Close" icon={<Dismiss24Regular />} />}>
Drawer with Dialog
</DrawerHeaderTitle>
</DrawerHeader>
<DrawerBody>
<Dialog open>
<DialogTrigger>
<Button>Open dialog</Button>
</DialogTrigger>
<DialogSurface backdrop={{ appearance: 'dimmed' }}>
<DialogBody>
<DialogTitle>Dialog inside Drawer</DialogTitle>
<DialogContent>This dialog has a dimmed backdrop even though it is nested inside a Drawer.</DialogContent>
<DialogActions>
<DialogTrigger>
<Button appearance="secondary">Close</Button>
</DialogTrigger>
<Button appearance="primary">Do Something</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</DrawerBody>
</OverlayDrawer>
);

DialogInsideDrawerDimmed.storyName = 'dialog inside drawer dimmed';

export const DialogInsideDrawerDimmedDarkMode = getStoryVariant(DialogInsideDrawerDimmed, DARK_MODE);
export const DialogInsideDrawerDimmedHighContrast = getStoryVariant(DialogInsideDrawerDimmed, HIGH_CONTRAST);
export const DialogInsideDrawerDimmedRTL = getStoryVariant(DialogInsideDrawerDimmed, RTL);

export const DialogInsideDrawerTransparent = () => (
<OverlayDrawer open position="start">
<DrawerHeader>
<DrawerHeaderTitle action={<Button appearance="subtle" aria-label="Close" icon={<Dismiss24Regular />} />}>
Drawer with Dialog
</DrawerHeaderTitle>
</DrawerHeader>
<DrawerBody>
<Dialog open>
<DialogTrigger>
<Button>Open dialog</Button>
</DialogTrigger>
<DialogSurface backdrop={{ appearance: 'transparent' }}>
<DialogBody>
<DialogTitle>Dialog inside Drawer</DialogTitle>
<DialogContent>This dialog explicitly sets transparent backdrop appearance.</DialogContent>
<DialogActions>
<DialogTrigger>
<Button appearance="secondary">Close</Button>
</DialogTrigger>
<Button appearance="primary">Do Something</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</DrawerBody>
</OverlayDrawer>
);

DialogInsideDrawerTransparent.storyName = 'dialog inside drawer transparent';

export const DialogInsideDrawerTransparentDarkMode = getStoryVariant(DialogInsideDrawerTransparent, DARK_MODE);
export const DialogInsideDrawerTransparentHighContrast = getStoryVariant(DialogInsideDrawerTransparent, HIGH_CONTRAST);
export const DialogInsideDrawerTransparentRTL = getStoryVariant(DialogInsideDrawerTransparent, RTL);

export const NestedDialogDimmed = () => (
<Dialog open>
<DialogTrigger>
<Button>Open nested dialog</Button>
</DialogTrigger>
<DialogSurface as="div">
<DialogBody>
<DialogTitle>Outer Dialog</DialogTitle>
<DialogContent>This is the outer dialog.</DialogContent>
<DialogActions>
<Dialog open>
<DialogTrigger>
<Button appearance="primary">Open inner dialog</Button>
</DialogTrigger>
<DialogSurface as="div" backdrop={{ appearance: 'dimmed' }}>
<DialogBody>
<DialogTitle>Inner Dialog with dimmed backdrop</DialogTitle>
<DialogContent>
This inner dialog explicitly sets dimmed backdrop to override the default transparent behavior for
nested dialogs.
</DialogContent>
<DialogActions>
<DialogTrigger>
<Button appearance="primary">Close</Button>
</DialogTrigger>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
);

NestedDialogDimmed.storyName = 'nested dialog dimmed';

export const NestedDialogDimmedDarkMode = getStoryVariant(NestedDialogDimmed, DARK_MODE);
export const NestedDialogDimmedHighContrast = getStoryVariant(NestedDialogDimmed, HIGH_CONTRAST);
export const NestedDialogDimmedRTL = getStoryVariant(NestedDialogDimmed, RTL);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add appearance to backdrop slot",
"packageName": "@fluentui/react-dialog",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ARIAButtonType } from '@fluentui/react-aria';
import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import { ContextSelector } from '@fluentui/react-context-selector';
import type { ExtractSlotProps } from '@fluentui/react-utilities';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import type { JSXElement } from '@fluentui/react-utilities';
import type { PortalProps } from '@fluentui/react-portal';
Expand Down Expand Up @@ -54,6 +55,11 @@ export type DialogActionsSlots = {
// @public
export type DialogActionsState = ComponentState<DialogActionsSlots> & Pick<Required<DialogActionsProps>, 'position' | 'fluid'>;

// @public
export type DialogBackdropSlotProps = ExtractSlotProps<Slot<'div'> & {
appearance?: 'dimmed' | 'transparent';
}>;

// @public
export const DialogBody: ForwardRefComponent<DialogBodyProps>;

Expand Down Expand Up @@ -180,7 +186,7 @@ export const DialogSurfaceProvider: React_2.Provider<boolean | undefined>;

// @public (undocumented)
export type DialogSurfaceSlots = {
backdrop?: Slot<'div'>;
backdrop?: Slot<DialogBackdropSlotProps>;
root: Slot<'div'>;
backdropMotion: Slot<PresenceMotionSlotProps>;
};
Expand All @@ -190,6 +196,7 @@ export type DialogSurfaceState = ComponentState<DialogSurfaceSlots> & Pick<Dialo
open?: boolean;
unmountOnClose?: boolean;
transitionStatus?: 'entering' | 'entered' | 'idle' | 'exiting' | 'exited' | 'unmounted';
backdropAppearance?: DialogBackdropSlotProps['appearance'];
};

// @public
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type {
DialogBackdropSlotProps,
DialogSurfaceContextValues,
DialogSurfaceElement,
DialogSurfaceProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const useDialog_unstable = (props: DialogProps): DialogState => {
trapFocus: modalType !== 'non-modal',
legacyTrapFocus: !inertTrapFocus,
});

const isNestedDialog = useHasParentContext(DialogContext);

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
import type { PresenceMotionSlotProps } from '@fluentui/react-motion';
import type { PortalProps } from '@fluentui/react-portal';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import type { ComponentProps, ComponentState, Slot, ExtractSlotProps } from '@fluentui/react-utilities';

import { DialogContextValue, DialogSurfaceContextValue } from '../../contexts';

/**
* Custom slot props for the backdrop slot.
*/
export type DialogBackdropSlotProps = ExtractSlotProps<
Slot<'div'> & {
/**
* Controls the backdrop appearance.
* - 'dimmed': Shows a dimmed backdrop (default for standalone dialogs)
* - 'transparent': Shows a transparent backdrop (default for nested dialogs)
*/
appearance?: 'dimmed' | 'transparent';
}
>;

export type DialogSurfaceSlots = {
/**
* Dimmed background of dialog.
* The default backdrop is rendered as a `<div>` with styling.
* This slot expects a `<div>` element which will replace the default backdrop.
* The backdrop should have `aria-hidden="true"`.
*
* Accepts an `appearance` prop to control backdrop visibility:
* - `'dimmed'`: Always shows a dimmed backdrop, regardless of nesting.
* - `'transparent'`: Always shows a transparent backdrop.
*
* @example
* ```tsx
* <DialogSurface backdrop={{ appearance: 'dimmed' }} />
* ```
*/
backdrop?: Slot<'div'>;
backdrop?: Slot<DialogBackdropSlotProps>;
root: Slot<'div'>;
/**
* For more information refer to the [Motion docs page](https://react.fluentui.dev/?path=/docs/motion-motion-slot--docs).
Expand Down Expand Up @@ -44,12 +66,12 @@ export type DialogSurfaceState = ComponentState<DialogSurfaceSlots> &
Pick<PortalProps, 'mountNode'> & {
open?: boolean;
unmountOnClose?: boolean;

/**
* Transition status for animation.
* In test environment, this is always `undefined`.
*
* @deprecated Will be always `undefined`.
*/
transitionStatus?: 'entering' | 'entered' | 'idle' | 'exiting' | 'exited' | 'unmounted';
backdropAppearance?: DialogBackdropSlotProps['appearance'];
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { DialogSurface } from './DialogSurface';
export type {
DialogBackdropSlotProps,
DialogSurfaceContextValues,
DialogSurfaceElement,
DialogSurfaceProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,12 @@ export const useDialogSurface_unstable = (
elementType: 'div',
});

const backdropAppearance = backdrop?.appearance;

if (backdrop) {
backdrop.onClick = handledBackdropClick;
// remove backdrop.appearance so it is not passed to the DOM
delete backdrop.appearance;
}

const { disableBodyScroll, enableBodyScroll } = useDisableBodyScroll();
Expand Down Expand Up @@ -109,6 +113,7 @@ export const useDialogSurface_unstable = (
open,
backdrop,
isNestedDialog,
backdropAppearance,
unmountOnClose,
mountNode: props.mountNode,
root: slot.always(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import { makeResetStyles, makeStyles, mergeClasses } from '@griffel/react';
import type { SlotClassNames } from '@fluentui/react-utilities';
import { tokens } from '@fluentui/react-theme';
import { createFocusOutlineStyle } from '@fluentui/react-tabster';
import {
Expand All @@ -12,6 +11,7 @@ import {
SURFACE_PADDING,
} from '../../contexts';
import type { DialogSurfaceSlots, DialogSurfaceState } from './DialogSurface.types';
import type { SlotClassNames } from '@fluentui/react-utilities';

export const dialogSurfaceClassNames: SlotClassNames<Omit<DialogSurfaceSlots, 'backdropMotion'>> = {
root: 'fui-DialogSurface',
Expand Down Expand Up @@ -82,11 +82,12 @@ const useStyles = makeStyles({
export const useDialogSurfaceStyles_unstable = (state: DialogSurfaceState): DialogSurfaceState => {
'use no memo';

const { isNestedDialog, root, backdrop, open, unmountOnClose } = state;
const { root, backdrop, open, unmountOnClose, isNestedDialog, backdropAppearance } = state;

const rootBaseStyle = useRootBaseStyle();
const backdropBaseStyle = useBackdropBaseStyle();
const styles = useStyles();
const isBackdropTransparent = backdropAppearance ? backdropAppearance === 'transparent' : isNestedDialog;

const mountedAndClosed = !unmountOnClose && !open;

Expand All @@ -101,10 +102,14 @@ export const useDialogSurfaceStyles_unstable = (state: DialogSurfaceState): Dial
backdrop.className = mergeClasses(
dialogSurfaceClassNames.backdrop,
backdropBaseStyle,
isNestedDialog && styles.nestedDialogBackdrop,
mountedAndClosed && styles.dialogHidden,
isBackdropTransparent && styles.nestedDialogBackdrop,
backdrop.className,
);

if (backdrop?.appearance) {
delete backdrop.appearance;
}
}

return state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export {
renderDialogSurface_unstable,
} from './DialogSurface';
export type {
DialogBackdropSlotProps,
DialogSurfaceProps,
DialogSurfaceSlots,
DialogSurfaceState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
The `backdrop` slot on `DialogSurface` accepts an `appearance` prop that allows you to explicitly control the backdrop appearance of the dialog.

By default, DialogSurface automatically determines the backdrop appearance based on context: standalone dialogs show a dimmed backdrop, while nested dialogs (inside another Dialog) show a transparent backdrop to avoid stacking multiple dimmed layers.

Use `backdrop={{ appearance: "dimmed" }}` when rendering a Dialog inside components that internally use Dialog (like `OverlayDrawer`) but the dialog should visually behave as standalone with a dimmed backdrop.

- **`'dimmed'`**: Always shows a dimmed backdrop, regardless of nesting.
- **`'transparent'`**: Always shows a transparent backdrop.

```tsx
<DialogSurface backdrop={{ appearance: 'dimmed' }} />
```
Loading