Skip to content
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

[Dialog, Popover, Menu, Select, PreviewCard] Unify backdrop implementation #841

Merged
merged 19 commits into from
Dec 9, 2024
Merged
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
4 changes: 4 additions & 0 deletions docs/data/api/alert-dialog-backdrop.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"props": {
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"container": {
"type": { "name": "union", "description": "HTML element<br>&#124;&nbsp;func" },
"default": "false"
},
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
},
Expand Down
4 changes: 4 additions & 0 deletions docs/data/api/dialog-backdrop.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"props": {
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"container": {
"type": { "name": "union", "description": "HTML element<br>&#124;&nbsp;func" },
"default": "false"
},
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
},
Expand Down
24 changes: 24 additions & 0 deletions docs/data/api/menu-backdrop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"props": {
"className": { "type": { "name": "union", "description": "func<br>&#124;&nbsp;string" } },
"container": {
"type": { "name": "union", "description": "HTML element<br>&#124;&nbsp;func" },
"default": "false"
},
"keepMounted": { "type": { "name": "bool" }, "default": "false" },
"render": { "type": { "name": "union", "description": "element<br>&#124;&nbsp;func" } }
},
"name": "MenuBackdrop",
"imports": [
"import { Menu } from '@base-ui-components/react/menu';\nconst MenuBackdrop = Menu.Backdrop;"
],
"classes": [],
"spread": true,
"themeDefaultProps": true,
"muiName": "MenuBackdrop",
"forwardsRefTo": "HTMLDivElement",
"filename": "/packages/react/src/menu/backdrop/MenuBackdrop.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/components/react-menu/\">Menu</a></li></ul>",
"cssComponent": false
}
5 changes: 4 additions & 1 deletion docs/data/components/menu/menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
productId: base-ui
title: React Menu component
description: The Menu component provide end users with a list of options on temporary surfaces.
components: MenuItem, MenuPositioner, MenuPopup, MenuRoot, MenuTrigger, MenuSubmenuTrigger, MenuArrow, MenuRadioGroup, MenuRadioItem, MenuRadioItemIndicator, MenuCheckboxItem, MenuCheckboxItemIndicator, MenuGroup, MenuGroupLabel
components: MenuItem, MenuPositioner, MenuPopup, MenuRoot, MenuTrigger, MenuSubmenuTrigger, MenuArrow, MenuRadioGroup, MenuRadioItem, MenuRadioItemIndicator, MenuCheckboxItem, MenuCheckboxItemIndicator, MenuGroup, MenuGroupLabel, MenuBackdrop
githubLabel: 'component: menu'
waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/
---
Expand All @@ -25,6 +25,7 @@ Menus are implemented using a collection of related components:

- `<Menu.Root />` is a top-level component that facilitates communication between other components. It does not render to the DOM.
- `<Menu.Trigger />` is an optional component (a button by default) that, when clicked, shows the menu. When not used, menu can be shown programmatically using the `open` prop.
- `<Menu.Backdrop />` renders an optional backdrop element behind the menu popup.
- `<Menu.Positioner />` renders the element responsible for positioning the popup.
- `<Menu.Popup />` is the menu popup.
- `<Menu.Item />` is the menu item.
Expand All @@ -43,6 +44,8 @@ Menus are implemented using a collection of related components:
<Menu.Root>
<Menu.Trigger />

<Menu.Backdrop />

<Menu.Positioner>
<Menu.Popup>
<Menu.Group>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"className": {
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"container": { "description": "The container element to which the backdrop is appended to." },
"keepMounted": {
"description": "If <code>true</code>, the backdrop element is kept in the DOM when closed."
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"className": {
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"container": { "description": "The container element to which the backdrop is appended to." },
"keepMounted": {
"description": "If <code>true</code>, the backdrop element is kept in the DOM when closed."
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"componentDescription": "Renders a backdrop for the menu.",
"propDescriptions": {
"className": {
"description": "Class names applied to the element or a function that returns them based on the component&#39;s state."
},
"container": { "description": "The container element to which the backdrop is appended to." },
"keepMounted": {
"description": "If <code>true</code>, the backdrop remains mounted when the menu popup is closed."
},
"render": { "description": "A function to customize rendering of the component." }
},
"classDescriptions": {}
}
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 @@ -6,6 +6,11 @@
"type": "string | (state) => string",
"description": "Class names applied to the element or a function that returns them based on the component's state."
},
"container": {
"type": "React.Ref | HTMLElement | null",
"default": "false",
"description": "The container element to which the backdrop is appended to."
},
"keepMounted": {
"type": "boolean",
"default": "false",
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 @@ -6,6 +6,11 @@
"type": "string | (state) => string",
"description": "Class names applied to the element or a function that returns them based on the component's state."
},
"container": {
"type": "React.Ref | HTMLElement | null",
"default": "false",
"description": "The container element to which the backdrop is appended to."
},
"keepMounted": {
"type": "boolean",
"default": "false",
Expand Down
24 changes: 24 additions & 0 deletions docs/reference/generated/menu-backdrop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "MenuBackdrop",
"description": "Renders a backdrop for the menu.",
"props": {
"className": {
"type": "string | (state) => string",
"description": "Class names applied to the element or a function that returns them based on the component's state."
},
"container": {
"type": "React.Ref | HTMLElement | null",
"default": "false",
"description": "The container element to which the backdrop is appended to."
},
"keepMounted": {
"type": "boolean",
"default": "false",
"description": "If `true`, the backdrop remains mounted when the menu popup is closed."
},
"render": {
"type": "React.ReactElement | (props, state) => React.ReactElement",
"description": "A function to customize rendering of the component."
}
}
}
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
},
"dependencies": {
"@babel/runtime": "^7.26.0",
"@floating-ui/react": "^0.26.28",
"@floating-ui/react": "^0.27.0",
"@floating-ui/react-dom": "^2.1.2",
"@floating-ui/utils": "^0.2.8",
"clsx": "^2.1.1",
Expand Down
45 changes: 28 additions & 17 deletions packages/react/src/alert-dialog/backdrop/AlertDialogBackdrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import { FloatingPortal } from '@floating-ui/react';
import { useAlertDialogRootContext } from '../root/AlertDialogRootContext';
import { useDialogBackdrop } from '../../dialog/backdrop/useDialogBackdrop';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
import type { TransitionStatus } from '../../utils/useTransitionStatus';
import type { BaseUIComponentProps } from '../../utils/types';
import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
import { popupStateMapping as baseMapping } from '../../utils/popupStateMapping';
import { HTMLElementType } from '../../utils/proptypes';

const customStyleHookMapping: CustomStyleHookMapping<AlertDialogBackdrop.State> = {
...baseMapping,
Expand Down Expand Up @@ -37,31 +37,29 @@ const AlertDialogBackdrop = React.forwardRef(function AlertDialogBackdrop(
props: AlertDialogBackdrop.Props,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const { render, className, keepMounted = false, ...other } = props;
const { open, hasParentDialog } = useAlertDialogRootContext();
const { render, className, keepMounted = false, container, ...other } = props;
const { open, hasParentDialog, mounted, transitionStatus } = useAlertDialogRootContext();

const { getRootProps, mounted, transitionStatus } = useDialogBackdrop({
open,
ref: forwardedRef,
});

const state: AlertDialogBackdrop.State = { open, transitionStatus };
const state: AlertDialogBackdrop.State = React.useMemo(
() => ({
open,
transitionStatus,
}),
[open, transitionStatus],
);

const { renderElement } = useComponentRenderer({
render: render ?? 'div',
className,
state,
propGetter: getRootProps,
extraProps: other,
ref: forwardedRef,
extraProps: { role: 'presentation', hidden: !mounted, ...other },
customStyleHookMapping,
});

if (!mounted && !keepMounted) {
return null;
}

if (hasParentDialog) {
// no need to render nested backdrops
// no need to render nested backdrops
const shouldRender = (keepMounted || mounted) && !hasParentDialog;
if (!shouldRender) {
return null;
}

Expand All @@ -76,6 +74,11 @@ namespace AlertDialogBackdrop {
* @default false
*/
keepMounted?: boolean;
/**
* The container element to which the backdrop is appended to.
* @default false
*/
container?: HTMLElement | null | React.MutableRefObject<HTMLElement | null>;
}

export interface State {
Expand All @@ -97,6 +100,14 @@ AlertDialogBackdrop.propTypes /* remove-proptypes */ = {
* Class names applied to the element or a function that returns them based on the component's state.
*/
className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
/**
* The container element to which the backdrop is appended to.
* @default false
*/
container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
HTMLElementType,
PropTypes.func,
]),
/**
* If `true`, the backdrop element is kept in the DOM when closed.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(
disabled={!mounted}
initialFocus={resolvedInitialFocus}
returnFocus={finalFocus}
outsideElementsInert
>
{renderElement()}
</FloatingFocusManager>
Expand Down
55 changes: 53 additions & 2 deletions packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
// This file is required by the API doc generator
import * as React from 'react';
import { expect } from 'chai';
import { describeSkipIf, screen, waitFor } from '@mui/internal-test-utils';
import { AlertDialog } from '@base-ui-components/react/alert-dialog';
import { createRenderer } from '#test-utils';

const isJSDOM = /jsdom/.test(window.navigator.userAgent);

describe('<AlertDialog.Root />', () => {
it('no-op', () => {});
const { render } = createRenderer();

describeSkipIf(isJSDOM)('modality', () => {
it('makes other interactive elements on the page inert when a modal dialog is open and restores them after the dialog is closed', async () => {
const { user } = await render(
<div>
<input data-testid="input" />
<textarea data-testid="textarea" />

<AlertDialog.Root>
<AlertDialog.Trigger>Open Dialog</AlertDialog.Trigger>
<AlertDialog.Popup>
<AlertDialog.Close>Close Dialog</AlertDialog.Close>
</AlertDialog.Popup>
</AlertDialog.Root>

<button type="button">Another Button</button>
</div>,
);

const outsideElements = [
screen.getByTestId('input'),
screen.getByTestId('textarea'),
screen.getByRole('button', { name: 'Another Button' }),
];

const trigger = screen.getByRole('button', { name: 'Open Dialog' });
await user.click(trigger);

await waitFor(() => {
outsideElements.forEach((element) => {
// The `inert` attribute can be applied to the element itself or to an ancestor
expect(element.closest('[inert]')).not.to.equal(null);
});
});

const close = screen.getByRole('button', { name: 'Close Dialog' });
await user.click(close);

await waitFor(() => {
outsideElements.forEach((element) => {
expect(element.closest('[inert]')).to.equal(null);
});
});
});
});
});
Loading
Loading