Skip to content

Commit

Permalink
[Dialog, AlertDialog] Fix the nesting of different dialogs (#1167)
Browse files Browse the repository at this point in the history
  • Loading branch information
mnajdova authored Dec 19, 2024
1 parent 2bfbabb commit 178a7e3
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 34 deletions.
31 changes: 31 additions & 0 deletions packages/react/src/alert-dialog/popup/AlertDialogPopup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { expect } from 'chai';
import { act, waitFor, screen } from '@mui/internal-test-utils';
import { AlertDialog } from '@base-ui-components/react/alert-dialog';
import { Dialog } from '@base-ui-components/react/dialog';
import { createRenderer, describeConformance } from '#test-utils';

describe('<AlertDialog.Popup />', () => {
Expand Down Expand Up @@ -224,5 +225,35 @@ describe('<AlertDialog.Popup />', () => {
expect(parentDialog).to.have.attribute('data-has-nested-dialogs');
expect(nestedDialog).not.to.have.attribute('data-has-nested-dialogs');
});

it('adds the `nested` and `has-nested-dialogs` style hooks on an alert dialog if has a parent dialog', async () => {
await render(
<Dialog.Root open>
<Dialog.Portal>
<Dialog.Popup data-testid="parent-dialog" />
<AlertDialog.Root open>
<AlertDialog.Portal>
<AlertDialog.Popup data-testid="nested-dialog">
<AlertDialog.Root>
<AlertDialog.Portal>
<AlertDialog.Popup />
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</Dialog.Portal>
</Dialog.Root>,
);

const parentDialog = screen.getByTestId('parent-dialog');
const nestedDialog = screen.getByTestId('nested-dialog');

expect(parentDialog).not.to.have.attribute('data-nested');
expect(nestedDialog).to.have.attribute('data-nested');

expect(parentDialog).to.have.attribute('data-has-nested-dialogs');
expect(nestedDialog).not.to.have.attribute('data-has-nested-dialogs');
});
});
});
19 changes: 3 additions & 16 deletions packages/react/src/alert-dialog/root/AlertDialogRootContext.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
'use client';
import * as React from 'react';
import { type useDialogRoot } from '../../dialog/root/useDialogRoot';
import { DialogContext } from '../../dialog/utils/DialogContext';

export interface AlertDialogRootContext extends useDialogRoot.ReturnValue {
/**
* Determines if the dialog is nested within a parent dialog.
*/
nested: boolean;
}

export const AlertDialogRootContext = React.createContext<AlertDialogRootContext | undefined>(
undefined,
);

if (process.env.NODE_ENV !== 'production') {
AlertDialogRootContext.displayName = 'AlertDialogRootContext';
}
export { DialogContext as AlertDialogRootContext };

export function useAlertDialogRootContext() {
const context = React.useContext(AlertDialogRootContext);
const context = React.useContext(DialogContext);
if (context === undefined) {
throw new Error(
'Base UI: AlertDialogRootContext is missing. AlertDialog parts must be placed within <AlertDialog.Root>.',
Expand Down
31 changes: 31 additions & 0 deletions packages/react/src/dialog/popup/DialogPopup.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { expect } from 'chai';
import { Dialog } from '@base-ui-components/react/dialog';
import { AlertDialog } from '@base-ui-components/react/alert-dialog';
import { act, waitFor, screen } from '@mui/internal-test-utils';
import { describeConformance, createRenderer } from '#test-utils';

Expand Down Expand Up @@ -232,5 +233,35 @@ describe('<Dialog.Popup />', () => {
expect(parentDialog).to.have.attribute('data-has-nested-dialogs');
expect(nestedDialog).not.to.have.attribute('data-has-nested-dialogs');
});

it('adds the `nested` and `has-nested-dialogs` style hooks if a dialog has a parent alert dialog', async () => {
await render(
<AlertDialog.Root open>
<AlertDialog.Portal>
<AlertDialog.Popup data-testid="parent-dialog" />
<Dialog.Root open>
<Dialog.Portal>
<Dialog.Popup data-testid="nested-dialog">
<Dialog.Root>
<Dialog.Portal>
<Dialog.Popup />
</Dialog.Portal>
</Dialog.Root>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
</AlertDialog.Portal>
</AlertDialog.Root>,
);

const parentDialog = screen.getByTestId('parent-dialog');
const nestedDialog = screen.getByTestId('nested-dialog');

expect(parentDialog).not.to.have.attribute('data-nested');
expect(nestedDialog).to.have.attribute('data-nested');

expect(parentDialog).to.have.attribute('data-has-nested-dialogs');
expect(nestedDialog).not.to.have.attribute('data-has-nested-dialogs');
});
});
});
20 changes: 11 additions & 9 deletions packages/react/src/dialog/root/DialogRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import { DialogRootContext } from './DialogRootContext';
import { DialogRootContext, useOptionalDialogRootContext } from './DialogRootContext';
import { DialogContext } from '../utils/DialogContext';
import { type CommonParameters, useDialogRoot } from './useDialogRoot';
import { PortalContext } from '../../portal/PortalContext';

Expand All @@ -21,7 +22,7 @@ const DialogRoot = function DialogRoot(props: DialogRoot.Props) {
open,
} = props;

const parentDialogRootContext = React.useContext(DialogRootContext);
const parentDialogRootContext = useOptionalDialogRootContext();

const dialogRoot = useDialogRoot({
open,
Expand All @@ -35,15 +36,16 @@ const DialogRoot = function DialogRoot(props: DialogRoot.Props) {

const nested = Boolean(parentDialogRootContext);

const contextValue = React.useMemo(
() => ({ ...dialogRoot, nested, dismissible }),
[dialogRoot, nested, dismissible],
);
const dialogContextValue = React.useMemo(() => ({ ...dialogRoot, nested }), [dialogRoot, nested]);

const dialogRootContextValue = React.useMemo(() => ({ dismissible }), [dismissible]);

return (
<DialogRootContext.Provider value={contextValue}>
<PortalContext.Provider value={dialogRoot.mounted}>{children}</PortalContext.Provider>
</DialogRootContext.Provider>
<DialogContext.Provider value={dialogContextValue}>
<DialogRootContext.Provider value={dialogRootContextValue}>
<PortalContext.Provider value={dialogRoot.mounted}>{children}</PortalContext.Provider>
</DialogRootContext.Provider>
</DialogContext.Provider>
);
};

Expand Down
37 changes: 28 additions & 9 deletions packages/react/src/dialog/root/DialogRootContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
'use client';
import * as React from 'react';
import type { useDialogRoot } from './useDialogRoot';
import { DialogContext } from '../utils/DialogContext';

export interface DialogRootContext extends useDialogRoot.ReturnValue {
/**
* Determines if the dialog is nested within a parent dialog.
*/
nested: boolean;
export interface DialogRootContext {
/**
* Determines whether the dialog should close on outside clicks.
*/
Expand All @@ -15,13 +11,36 @@ export interface DialogRootContext extends useDialogRoot.ReturnValue {

export const DialogRootContext = React.createContext<DialogRootContext | undefined>(undefined);

if (process.env.NODE_ENV !== 'production') {
DialogRootContext.displayName = 'DialogRootContext';
}

export function useOptionalDialogRootContext() {
const dialogRootContext = React.useContext(DialogRootContext);
const dialogContext = React.useContext(DialogContext);

if (dialogContext === undefined && dialogRootContext === undefined) {
return undefined;
}

return {
...dialogRootContext,
...dialogContext,
};
}

export function useDialogRootContext() {
const context = React.useContext(DialogRootContext);
if (context === undefined) {
const dialogRootContext = React.useContext(DialogRootContext);
const dialogContext = React.useContext(DialogContext);

if (dialogContext === undefined) {
throw new Error(
'Base UI: DialogRootContext is missing. Dialog parts must be placed within <Dialog.Root>.',
);
}

return context;
return {
...dialogRootContext,
...dialogContext,
};
}
23 changes: 23 additions & 0 deletions packages/react/src/dialog/utils/DialogContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client';
import * as React from 'react';
import type { useDialogRoot } from '../root/useDialogRoot';

/**
* Common context for dialog & dialog alert components.
*/
export interface DialogContext extends useDialogRoot.ReturnValue {
/**
* Determines if the dialog is nested within a parent dialog.
*/
nested: boolean;
}

export const DialogContext = React.createContext<DialogContext | undefined>(undefined);

if (process.env.NODE_ENV !== 'production') {
DialogContext.displayName = 'DialogContext';
}

export function useDialogContext() {
return React.useContext(DialogContext);
}

0 comments on commit 178a7e3

Please sign in to comment.