Skip to content

[dialog][alert dialog] Add renderMode prop to Backdrop part #2037

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions docs/reference/generated/alert-dialog-backdrop.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
"name": "AlertDialogBackdrop",
"description": "An overlay displayed beneath the popup.\nRenders a `<div>` element.",
"props": {
"renderMode": {
"type": "'root' | 'leaf' | 'always'",
"default": "'root'",
"description": "How to render the backdrop when dialogs are nested in the React tree."
},
"className": {
"type": "string | ((state: AlertDialog.Backdrop.State) => string)",
"description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state."
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/generated/dialog-backdrop.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
"name": "DialogBackdrop",
"description": "An overlay displayed beneath the popup.\nRenders a `<div>` element.",
"props": {
"renderMode": {
"type": "'root' | 'leaf' | 'always'",
"default": "'root'",
"description": "How to render the backdrop when dialogs are nested in the React tree."
},
"className": {
"type": "string | ((state: Dialog.Backdrop.State) => string)",
"description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state."
Expand Down
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 { AlertDialog } from '@base-ui-components/react/alert-dialog';
import { createRenderer, describeConformance } from '#test-utils';
import { flushMicrotasks, screen } from '@mui/internal-test-utils';

describe('<AlertDialog.Backdrop />', () => {
const { render } = createRenderer();
Expand All @@ -22,4 +23,320 @@ describe('<AlertDialog.Backdrop />', () => {

expect(getByTestId('backdrop')).to.have.attribute('role', 'presentation');
});

describe('prop: renderMode', () => {
it('defaults to "root" renderMode when not specified', async () => {
function App() {
const [nestedOpen, setNestedOpen] = React.useState(true);

return (
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="root-backdrop" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Root dialog
<AlertDialog.Root open={nestedOpen} onOpenChange={setNestedOpen}>
<AlertDialog.Backdrop data-testid="nested-backdrop" />
<AlertDialog.Portal>
<AlertDialog.Popup>Nested dialog</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

await render(<App />);

expect(screen.getByTestId('root-backdrop')).not.to.equal(null);
expect(screen.queryByTestId('nested-backdrop')).to.equal(null);
});

describe('root', () => {
it('renders by default when not nested', async () => {
await render(
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="backdrop" />
<AlertDialog.Portal>
<AlertDialog.Popup>Content</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>,
);

expect(screen.getByTestId('backdrop')).not.to.equal(null);
});

it('does not render when nested', async () => {
function App() {
const [nestedOpen, setNestedOpen] = React.useState(true);

return (
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="root-backdrop" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Root dialog
<AlertDialog.Root open={nestedOpen} onOpenChange={setNestedOpen}>
<AlertDialog.Backdrop data-testid="nested-backdrop" renderMode="root" />
<AlertDialog.Portal>
<AlertDialog.Popup>Nested dialog</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

await render(<App />);

expect(screen.getByTestId('root-backdrop')).not.to.equal(null);
expect(screen.queryByTestId('nested-backdrop')).to.equal(null);
});

it('renders only at the root level with multiple nesting levels', async () => {
function App() {
const [level2Open, setLevel2Open] = React.useState(true);
const [level3Open, setLevel3Open] = React.useState(true);

return (
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="level-1-backdrop" renderMode="root" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 1 dialog
<AlertDialog.Root open={level2Open} onOpenChange={setLevel2Open}>
<AlertDialog.Backdrop data-testid="level-2-backdrop" renderMode="root" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 2 dialog
<AlertDialog.Root open={level3Open} onOpenChange={setLevel3Open}>
<AlertDialog.Backdrop data-testid="level-3-backdrop" renderMode="root" />
<AlertDialog.Portal>
<AlertDialog.Popup>Level 3 dialog</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

await render(<App />);

expect(screen.getByTestId('level-1-backdrop')).not.to.equal(null);
expect(screen.queryByTestId('level-2-backdrop')).to.equal(null);
expect(screen.queryByTestId('level-3-backdrop')).to.equal(null);
});
});

describe('leaf', () => {
it('renders at root level when not nested', async () => {
await render(
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>Content</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>,
);

expect(screen.getByTestId('backdrop')).not.to.equal(null);
});

it('renders only at the leaf level when nested', async () => {
function App() {
const [level2Open, setLevel2Open] = React.useState(true);
const [level3Open, setLevel3Open] = React.useState(true);

return (
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="level-1-backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 1 dialog
<AlertDialog.Root open={level2Open} onOpenChange={setLevel2Open}>
<AlertDialog.Backdrop data-testid="level-2-backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 2 dialog
<AlertDialog.Root open={level3Open} onOpenChange={setLevel3Open}>
<AlertDialog.Backdrop data-testid="level-3-backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>Level 3 dialog</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

await render(<App />);

expect(screen.queryByTestId('level-1-backdrop')).to.equal(null);
expect(screen.queryByTestId('level-2-backdrop')).to.equal(null);
expect(screen.getByTestId('level-3-backdrop')).not.to.equal(null);
});

it('works with simple two-level nesting', async () => {
function App() {
return (
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="level-1-backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 1 dialog
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="level-2-backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>Level 2 dialog</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

await render(<App />);

// With leaf rendering, only the innermost (level 2) should render
expect(screen.queryByTestId('level-1-backdrop')).to.equal(null);
expect(screen.getByTestId('level-2-backdrop')).not.to.equal(null);
});

it('updates rendering when nested dialogs open/close', async () => {
function App() {
const [level2Open, setLevel2Open] = React.useState(false);

return (
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="level-1-backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 1 dialog
<button onClick={() => setLevel2Open(true)}>Open level 2</button>
<AlertDialog.Root open={level2Open} onOpenChange={setLevel2Open}>
<AlertDialog.Backdrop data-testid="level-2-backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>Level 2 dialog</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

const { user } = await render(<App />);

// Initially, level 1 should render as it's the leaf
expect(screen.queryByTestId('level-1-backdrop')).not.to.equal(null);
expect(screen.queryByTestId('level-2-backdrop')).to.have.property('hidden', true);

// Open level 2 dialog
const openButton = screen.getByRole('button', { name: 'Open level 2' });
await user.click(openButton);
await flushMicrotasks();

// Now level 2 should render as it's the leaf, level 1 should not
expect(screen.queryByTestId('level-1-backdrop')).to.equal(null);
expect(screen.queryByTestId('level-2-backdrop')).not.to.have.property('hidden', true);
});
});

describe('always', () => {
it('always renders regardless of nesting', async () => {
function App() {
const [level2Open, setLevel2Open] = React.useState(true);
const [level3Open, setLevel3Open] = React.useState(true);

return (
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="level-1-backdrop" renderMode="always" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 1 dialog
<AlertDialog.Root open={level2Open} onOpenChange={setLevel2Open}>
<AlertDialog.Backdrop data-testid="level-2-backdrop" renderMode="always" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 2 dialog
<AlertDialog.Root open={level3Open} onOpenChange={setLevel3Open}>
<AlertDialog.Backdrop
data-testid="level-3-backdrop"
renderMode="always"
/>
<AlertDialog.Portal>
<AlertDialog.Popup>Level 3 dialog</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

await render(<App />);

expect(screen.getByTestId('level-1-backdrop')).not.to.equal(null);
expect(screen.getByTestId('level-2-backdrop')).not.to.equal(null);
expect(screen.getByTestId('level-3-backdrop')).not.to.equal(null);
});
});

describe('mixed modes', () => {
it('works with different render modes in nested dialogs', async () => {
function App() {
const [level2Open, setLevel2Open] = React.useState(true);
const [level3Open, setLevel3Open] = React.useState(true);

return (
<AlertDialog.Root open>
<AlertDialog.Backdrop data-testid="level-1-backdrop" renderMode="root" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 1 dialog
<AlertDialog.Root open={level2Open} onOpenChange={setLevel2Open}>
<AlertDialog.Backdrop data-testid="level-2-backdrop" renderMode="always" />
<AlertDialog.Portal>
<AlertDialog.Popup>
Level 2 dialog
<AlertDialog.Root open={level3Open} onOpenChange={setLevel3Open}>
<AlertDialog.Backdrop data-testid="level-3-backdrop" renderMode="leaf" />
<AlertDialog.Portal>
<AlertDialog.Popup>Level 3 dialog</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
</AlertDialog.Popup>
</AlertDialog.Portal>
</AlertDialog.Root>
);
}

await render(<App />);

// root mode: only renders at root level
expect(screen.getByTestId('level-1-backdrop')).not.to.equal(null);
// always mode: always renders
expect(screen.getByTestId('level-2-backdrop')).not.to.equal(null);
// leaf mode: only renders at leaf level
expect(screen.getByTestId('level-3-backdrop')).not.to.equal(null);
});
});
});
});
Loading