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
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add support to keep Dialog mounter on close",
"packageName": "@fluentui/react-dialog",
"email": "marcosvmmoura@gmail.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export type DialogContextValue = {
dialogTitleId?: string;
isNestedDialog: boolean;
dialogRef: React_2.Ref<DialogSurfaceElement | null>;
unmountOnClose?: boolean;
modalType: DialogModalType;
requestOpenChange: (data: DialogOpenChangeData) => void;
} & Partial<ReturnType<typeof useModalAttributes>>;
Expand Down Expand Up @@ -137,6 +138,7 @@ export type DialogProps = ComponentProps<Partial<DialogSlots>> & {
onOpenChange?: DialogOpenChangeEventHandler;
children: [JSXElement, JSXElement] | JSXElement;
inertTrapFocus?: boolean;
unmountOnClose?: boolean;
};

// @public (undocumented)
Expand Down Expand Up @@ -186,6 +188,7 @@ export type DialogSurfaceSlots = {
// @public
export type DialogSurfaceState = ComponentState<DialogSurfaceSlots> & Pick<DialogContextValue, 'isNestedDialog'> & Pick<PortalProps, 'mountNode'> & {
open?: boolean;
unmountOnClose?: boolean;
transitionStatus?: 'entering' | 'entered' | 'idle' | 'exiting' | 'exited' | 'unmounted';
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,42 @@ describe('Dialog', () => {
cy.get(dialogTriggerCloseSelector).realClick();
cy.get(dialogTriggerOpenSelector).should('be.focused');
});

it('should remain mounted after close when unmountOnClose is false', () => {
mount(
<Dialog unmountOnClose={false}>
<DialogTrigger disableButtonEnhancement>
<Button id={dialogTriggerOpenId}>Open dialog</Button>
</DialogTrigger>
<DialogSurface>
<DialogBody>
<DialogTitle>Dialog title</DialogTitle>
<DialogContent>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam exercitationem cumque repellendus eaque
est dolor eius expedita nulla ullam? Tenetur reprehenderit aut voluptatum impedit voluptates in natus iure
cumque eaque?
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button id={dialogTriggerCloseId} appearance="secondary">
Close
</Button>
</DialogTrigger>
<Button appearance="primary">Do Something</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>,
);

cy.get(dialogTriggerOpenSelector).realClick();
cy.get(dialogSurfaceSelector).should('exist');

cy.get(dialogTriggerCloseSelector).realClick();
// dialog surface should remain mounted when unmountOnClose is false
cy.get(dialogSurfaceSelector).should('exist');
});

it('should allow change of focus on open', () => {
const CustomFocusedElementOnOpen = () => {
const buttonRef = React.useRef<HTMLButtonElement>(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ export type DialogProps = ComponentProps<Partial<DialogSlots>> & {
* @default false
*/
inertTrapFocus?: boolean;
/**
* Decides whether the dialog should be removed from the DOM tree when it is closed.
* This can be useful when dealing with components that may contain state that should not
* be reset when the dialog is closed.
*
* @default true
*/
unmountOnClose?: boolean;
};

export type DialogState = ComponentState<InternalDialogSlots> &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { DialogOpenChangeData, DialogProps, DialogState } from './Dialog.ty
* @param props - props from this instance of Dialog
*/
export const useDialog_unstable = (props: DialogProps): DialogState => {
const { children, modalType = 'modal', onOpenChange, inertTrapFocus = false } = props;
const { children, modalType = 'modal', onOpenChange, inertTrapFocus = false, unmountOnClose = true } = props;

const [trigger, content] = childrenToTriggerAndContent(children);

Expand Down Expand Up @@ -58,15 +58,16 @@ export const useDialog_unstable = (props: DialogProps): DialogState => {
requestOpenChange,
dialogTitleId: useId('dialog-title-'),
isNestedDialog,
unmountOnClose,
dialogRef: focusRef,
modalAttributes,
triggerAttributes,
surfaceMotion: presenceMotionSlot(props.surfaceMotion, {
elementType: DialogSurfaceMotion,
defaultProps: {
appear: true,
visible: open,
unmountOnExit: true,
appear: unmountOnClose,
unmountOnExit: unmountOnClose,
},
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function useDialogContextValues_unstable(state: DialogState): DialogConte
requestOpenChange,
modalAttributes,
triggerAttributes,
unmountOnClose,
} = state;

/**
Expand All @@ -27,6 +28,7 @@ export function useDialogContextValues_unstable(state: DialogState): DialogConte
inertTrapFocus,
modalAttributes,
triggerAttributes,
unmountOnClose,
requestOpenChange,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type DialogSurfaceState = ComponentState<DialogSurfaceSlots> &
Pick<DialogContextValue, 'isNestedDialog'> &
Pick<PortalProps, 'mountNode'> & {
open?: boolean;
unmountOnClose?: boolean;

/**
* Transition status for animation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const useDialogSurface_unstable = (
const requestOpenChange = useDialogContext_unstable(ctx => ctx.requestOpenChange);
const dialogTitleID = useDialogContext_unstable(ctx => ctx.dialogTitleId);
const open = useDialogContext_unstable(ctx => ctx.open);
const unmountOnClose = useDialogContext_unstable(ctx => ctx.unmountOnClose);

const handledBackdropClick = useEventCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (isResolvedShorthand(props.backdrop)) {
Expand Down Expand Up @@ -75,23 +76,27 @@ export const useDialogSurface_unstable = (
},
elementType: 'div',
});

if (backdrop) {
backdrop.onClick = handledBackdropClick;
}

const { disableBodyScroll, enableBodyScroll } = useDisableBodyScroll();

useIsomorphicLayoutEffect(() => {
if (!open) {
enableBodyScroll();
return;
}

if (isNestedDialog || modalType === 'non-modal') {
return;
}

disableBodyScroll();

return () => {
enableBodyScroll();
};
}, [enableBodyScroll, isNestedDialog, disableBodyScroll, modalType]);
return () => enableBodyScroll();
}, [open, modalType, isNestedDialog, disableBodyScroll, enableBodyScroll]);

return {
components: {
Expand All @@ -102,13 +107,15 @@ export const useDialogSurface_unstable = (
open,
backdrop,
isNestedDialog,
unmountOnClose,
mountNode: props.mountNode,
root: slot.always(
getIntrinsicElementProps('div', {
tabIndex: -1, // https://github.com/microsoft/fluentui/issues/25150
'aria-modal': modalType !== 'non-modal',
role: modalType === 'alert' ? 'alertdialog' : 'dialog',
'aria-modal': modalType !== 'non-modal',
'aria-labelledby': props['aria-label'] ? undefined : dialogTitleID,
'aria-hidden': !unmountOnClose && !open ? true : undefined,
...props,
...modalAttributes,
onKeyDown: handleKeyDown,
Expand All @@ -122,7 +129,7 @@ export const useDialogSurface_unstable = (
backdropMotion: presenceMotionSlot(props.backdropMotion, {
elementType: DialogBackdropMotion,
defaultProps: {
appear: true,
appear: unmountOnClose,
visible: open,
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ const useStyles = makeStyles({
nestedDialogBackdrop: {
backgroundColor: tokens.colorTransparentBackground,
},

dialogHidden: {
pointerEvents: 'none',
},
});

/**
Expand All @@ -76,20 +80,27 @@ const useStyles = makeStyles({
export const useDialogSurfaceStyles_unstable = (state: DialogSurfaceState): DialogSurfaceState => {
'use no memo';

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

const rootBaseStyle = useRootBaseStyle();

const backdropBaseStyle = useBackdropBaseStyle();
const styles = useStyles();

root.className = mergeClasses(dialogSurfaceClassNames.root, rootBaseStyle, root.className);
const mountedAndClosed = !unmountOnClose && !open;

root.className = mergeClasses(
dialogSurfaceClassNames.root,
rootBaseStyle,
mountedAndClosed && styles.dialogHidden,
root.className,
);

if (backdrop) {
backdrop.className = mergeClasses(
dialogSurfaceClassNames.backdrop,
backdropBaseStyle,
isNestedDialog && styles.nestedDialogBackdrop,
mountedAndClosed && styles.dialogHidden,
backdrop.className,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type DialogContextValue = {
dialogTitleId?: string;
isNestedDialog: boolean;
dialogRef: React.Ref<DialogSurfaceElement | null>;
unmountOnClose?: boolean;
modalType: DialogModalType;
/**
* Requests dialog main component to update it's internal open state
Expand All @@ -23,6 +24,7 @@ const defaultContextValue: DialogContextValue = {
inertTrapFocus: false,
modalType: 'modal',
isNestedDialog: false,
unmountOnClose: true,
dialogRef: { current: null },
requestOpenChange() {
/* noop */
Expand Down
Loading
Loading