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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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, SubmenuTrigger, MenuArrow, MenuRadioGroup, MenuRadioItem, MenuRadioItemIndicator, MenuCheckboxItem, MenuCheckboxItemIndicator, MenuGroup, MenuGroupLabel
components: MenuItem, MenuPositioner, MenuPopup, MenuRoot, MenuTrigger, SubmenuTrigger, 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."
}
}
}
46 changes: 28 additions & 18 deletions packages/react/src/AlertDialog/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 { popupOpenStateMapping as baseMapping } from '../../utils/popupOpenStateMapping';
import { HTMLElementType } from '../../utils/proptypes';

const customStyleHookMapping: CustomStyleHookMapping<AlertDialogBackdrop.OwnerState> = {
...baseMapping,
Expand Down Expand Up @@ -37,32 +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, animated } = useAlertDialogRootContext();
const { render, className, keepMounted = false, container, ...other } = props;
const { open, hasParentDialog, mounted, transitionStatus } = useAlertDialogRootContext();

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

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

const { renderElement } = useComponentRenderer({
render: render ?? 'div',
className,
ownerState,
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 @@ -77,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 OwnerState {
Expand All @@ -98,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
3 changes: 2 additions & 1 deletion packages/react/src/AlertDialog/Popup/AlertDialogPopup.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import { FloatingFocusManager, FloatingPortal } from '@floating-ui/react';
import { FloatingFocusManager, FloatingOverlay, FloatingPortal } from '@floating-ui/react';
import { useDialogPopup } from '../../Dialog/Popup/useDialogPopup';
import { useAlertDialogRootContext } from '../Root/AlertDialogRootContext';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
Expand Down Expand Up @@ -115,6 +115,7 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup(

return (
<FloatingPortal root={container}>
{mounted && <FloatingOverlay />}
<FloatingFocusManager
context={floatingContext}
modal
Expand Down
45 changes: 26 additions & 19 deletions packages/react/src/Dialog/Backdrop/DialogBackdrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { FloatingPortal } from '@floating-ui/react';
import { useDialogBackdrop } from './useDialogBackdrop';
import { useDialogRootContext } from '../Root/DialogRootContext';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
import { type TransitionStatus } from '../../utils/useTransitionStatus';
import { type BaseUIComponentProps } from '../../utils/types';
import { type CustomStyleHookMapping } from '../../utils/getStyleHookProps';
import { popupOpenStateMapping as baseMapping } from '../../utils/popupOpenStateMapping';
import { HTMLElementType } from '../../utils/proptypes';

const customStyleHookMapping: CustomStyleHookMapping<DialogBackdrop.OwnerState> = {
...baseMapping,
Expand Down Expand Up @@ -38,39 +38,33 @@ const DialogBackdrop = React.forwardRef(function DialogBackdrop(
props: DialogBackdrop.Props,
forwardedRef: React.ForwardedRef<HTMLDivElement>,
) {
const { render, className, keepMounted = false, ...other } = props;
const { open, hasParentDialog, animated } = useDialogRootContext();

const { getRootProps, mounted, transitionStatus } = useDialogBackdrop({
animated,
open,
ref: forwardedRef,
});
const { render, className, keepMounted = false, container, ...other } = props;
const { open, hasParentDialog, mounted, transitionStatus } = useDialogRootContext();

const ownerState: DialogBackdrop.OwnerState = React.useMemo(
() => ({ open, transitionStatus }),
() => ({
open,
transitionStatus,
}),
[open, transitionStatus],
);

const { renderElement } = useComponentRenderer({
render: render ?? 'div',
className,
ownerState,
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;
}

return <FloatingPortal>{renderElement()}</FloatingPortal>;
return <FloatingPortal root={container}>{renderElement()}</FloatingPortal>;
});

namespace DialogBackdrop {
Expand All @@ -81,6 +75,11 @@ namespace DialogBackdrop {
* @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 OwnerState {
Expand All @@ -102,6 +101,14 @@ DialogBackdrop.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
Loading