diff --git a/docs/reference/generated/alert-dialog-portal.json b/docs/reference/generated/alert-dialog-portal.json index 31014bc670..fbdb9ede3e 100644 --- a/docs/reference/generated/alert-dialog-portal.json +++ b/docs/reference/generated/alert-dialog-portal.json @@ -1,21 +1,27 @@ { "name": "AlertDialogPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: AlertDialog.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state.", + "detailedType": "| string\n| ((state: AlertDialog.Portal.State) => string)" }, "keepMounted": { "type": "boolean", "default": "false", "description": "Whether to keep the portal mounted in the DOM while the popup is hidden.", "detailedType": "boolean | undefined" + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: AlertDialog.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: AlertDialog.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/combobox-portal.json b/docs/reference/generated/combobox-portal.json index 1514ab86c1..431f648701 100644 --- a/docs/reference/generated/combobox-portal.json +++ b/docs/reference/generated/combobox-portal.json @@ -1,21 +1,26 @@ { "name": "ComboboxPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { - "type": "HTMLElement | RefObject | null", + "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", - "detailedType": "| HTMLElement\n| React.RefObject\n| null\n| undefined" + "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: Combobox.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." }, "keepMounted": { "type": "boolean", "default": "false", "description": "Whether to keep the portal mounted in the DOM while the popup is hidden.", "detailedType": "boolean | undefined" + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: Combobox.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: Combobox.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/dialog-portal.json b/docs/reference/generated/dialog-portal.json index 4be121c6d1..b58bd81919 100644 --- a/docs/reference/generated/dialog-portal.json +++ b/docs/reference/generated/dialog-portal.json @@ -1,21 +1,26 @@ { "name": "DialogPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: Dialog.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." }, "keepMounted": { "type": "boolean", "default": "false", "description": "Whether to keep the portal mounted in the DOM while the popup is hidden.", "detailedType": "boolean | undefined" + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: Dialog.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: Dialog.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/menu-portal.json b/docs/reference/generated/menu-portal.json index ca33cd284b..376eaf5c63 100644 --- a/docs/reference/generated/menu-portal.json +++ b/docs/reference/generated/menu-portal.json @@ -1,21 +1,26 @@ { "name": "MenuPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: Menu.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." }, "keepMounted": { "type": "boolean", "default": "false", "description": "Whether to keep the portal mounted in the DOM while the popup is hidden.", "detailedType": "boolean | undefined" + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: Menu.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: Menu.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/navigation-menu-portal.json b/docs/reference/generated/navigation-menu-portal.json index 29b172d752..f2f7d7aa69 100644 --- a/docs/reference/generated/navigation-menu-portal.json +++ b/docs/reference/generated/navigation-menu-portal.json @@ -1,21 +1,27 @@ { "name": "NavigationMenuPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { - "type": "HTMLElement | RefObject | null", + "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", - "detailedType": "| HTMLElement\n| React.RefObject\n| null\n| undefined" + "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: NavigationMenu.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state.", + "detailedType": "| string\n| ((state: NavigationMenu.Portal.State) => string)" }, "keepMounted": { "type": "boolean", "default": "false", "description": "Whether to keep the portal mounted in the DOM while the popup is hidden.", "detailedType": "boolean | undefined" + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: NavigationMenu.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: NavigationMenu.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/popover-portal.json b/docs/reference/generated/popover-portal.json index b69dc39f37..6bdb4f5fd7 100644 --- a/docs/reference/generated/popover-portal.json +++ b/docs/reference/generated/popover-portal.json @@ -1,21 +1,26 @@ { "name": "PopoverPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: Popover.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." }, "keepMounted": { "type": "boolean", "default": "false", "description": "Whether to keep the portal mounted in the DOM while the popup is hidden.", "detailedType": "boolean | undefined" + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: Popover.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: Popover.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/preview-card-portal.json b/docs/reference/generated/preview-card-portal.json index b2a55326c4..5605d1bcab 100644 --- a/docs/reference/generated/preview-card-portal.json +++ b/docs/reference/generated/preview-card-portal.json @@ -1,21 +1,27 @@ { "name": "PreviewCardPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: PreviewCard.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state.", + "detailedType": "| string\n| ((state: PreviewCard.Portal.State) => string)" }, "keepMounted": { "type": "boolean", "default": "false", "description": "Whether to keep the portal mounted in the DOM while the popup is hidden.", "detailedType": "boolean | undefined" + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: PreviewCard.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: PreviewCard.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/select-portal.json b/docs/reference/generated/select-portal.json index 7b1e69db10..5bfb92091f 100644 --- a/docs/reference/generated/select-portal.json +++ b/docs/reference/generated/select-portal.json @@ -1,15 +1,20 @@ { "name": "SelectPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: Select.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: Select.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: Select.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/toast-portal.json b/docs/reference/generated/toast-portal.json index e2b2aa546c..96a6afedc9 100644 --- a/docs/reference/generated/toast-portal.json +++ b/docs/reference/generated/toast-portal.json @@ -1,15 +1,20 @@ { "name": "ToastPortal", - "description": "A portal element that moves the viewport to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the viewport to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { - "type": "HTMLElement | RefObject | null", + "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", - "detailedType": "| HTMLElement\n| React.RefObject\n| null\n| undefined" + "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: any) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: any) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((props: HTMLProps, state: any) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/reference/generated/tooltip-portal.json b/docs/reference/generated/tooltip-portal.json index 2ddeb24196..11f2e15907 100644 --- a/docs/reference/generated/tooltip-portal.json +++ b/docs/reference/generated/tooltip-portal.json @@ -1,21 +1,26 @@ { "name": "TooltipPortal", - "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.", + "description": "A portal element that moves the popup to a different part of the DOM.\nBy default, the portal element is appended to ``.\nRenders a `
` element.", "props": { "container": { "type": "HTMLElement | ShadowRoot | RefObject | null", "description": "A parent element to render the portal element into.", "detailedType": "| HTMLElement\n| ShadowRoot\n| React.RefObject\n| null\n| undefined" }, - "children": { - "type": "ReactNode", - "detailedType": "React.ReactNode" + "className": { + "type": "string | ((state: Tooltip.Portal.State) => string)", + "description": "CSS class applied to the element, or a function that\nreturns a class based on the component’s state." }, "keepMounted": { "type": "boolean", "default": "false", "description": "Whether to keep the portal mounted in the DOM while the popup is hidden.", "detailedType": "boolean | undefined" + }, + "render": { + "type": "ReactElement | ((props: HTMLProps, state: Tooltip.Portal.State) => ReactElement)", + "description": "Allows you to replace the component’s HTML element\nwith a different tag, or compose it with another component.\n\nAccepts a `ReactElement` or a function that returns the element to render.", + "detailedType": "| ReactElement\n| ((\n props: HTMLProps,\n state: Tooltip.Portal.State,\n ) => ReactElement)" } }, "dataAttributes": {}, diff --git a/docs/src/components/Demo/DemoLoader.tsx b/docs/src/components/Demo/DemoLoader.tsx index 8d4420e904..7632162584 100644 --- a/docs/src/components/Demo/DemoLoader.tsx +++ b/docs/src/components/Demo/DemoLoader.tsx @@ -254,6 +254,21 @@ function resolveExtensionlessFile(filePath: string, preferTs: boolean): string { ? ['.tsx', '.ts', '.jsx', '.js', '.json'] : ['.jsx', '.js', '.tsx', '.ts', '.json']; + if (existsSync(filePath)) { + const stats = statSync(filePath); + if (stats.isFile()) { + return filePath; + } + if (stats.isDirectory()) { + for (const extension of extensions) { + const indexPath = join(filePath, `index${extension}`); + if (existsSync(indexPath)) { + return indexPath; + } + } + } + } + for (const extension of extensions) { const fullPath = `${filePath}${extension}`; if (existsSync(fullPath)) { diff --git a/packages/react/src/alert-dialog/portal/AlertDialogPortal.test.tsx b/packages/react/src/alert-dialog/portal/AlertDialogPortal.test.tsx new file mode 100644 index 0000000000..e7bfe1d892 --- /dev/null +++ b/packages/react/src/alert-dialog/portal/AlertDialogPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { AlertDialog } from '@base-ui-components/react/alert-dialog'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/alert-dialog/portal/AlertDialogPortal.tsx b/packages/react/src/alert-dialog/portal/AlertDialogPortal.tsx index 26b5962e7f..1e0953232c 100644 --- a/packages/react/src/alert-dialog/portal/AlertDialogPortal.tsx +++ b/packages/react/src/alert-dialog/portal/AlertDialogPortal.tsx @@ -1,17 +1,21 @@ 'use client'; import * as React from 'react'; -import { FloatingPortal, FloatingPortalProps } from '../../floating-ui-react'; +import { FloatingPortal } from '../../floating-ui-react'; import { useDialogRootContext } from '../../dialog/root/DialogRootContext'; import { AlertDialogPortalContext } from './AlertDialogPortalContext'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Alert Dialog](https://base-ui.com/react/components/alert-dialog) */ -export function AlertDialogPortal(props: AlertDialogPortal.Props) { - const { children, keepMounted = false, container } = props; +export const AlertDialogPortal = React.forwardRef(function AlertDialogPortal( + props: AlertDialogPortal.Props, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted = false, ...portalProps } = props; const { store } = useDialogRootContext(); const mounted = store.useState('mounted'); @@ -23,13 +27,16 @@ export function AlertDialogPortal(props: AlertDialogPortal.Props) { return ( - {children} + ); +}); + +export namespace AlertDialogPortal { + export interface State {} } -export interface AlertDialogPortalProps { - children?: React.ReactNode; +export interface AlertDialogPortalProps extends FloatingPortal.Props { /** * Whether to keep the portal mounted in the DOM while the popup is hidden. * @default false @@ -38,7 +45,7 @@ export interface AlertDialogPortalProps { /** * A parent element to render the portal element into. */ - container?: FloatingPortalProps['root']; + container?: FloatingPortal.Props['container']; } export namespace AlertDialogPortal { diff --git a/packages/react/src/combobox/portal/ComboboxPortal.test.tsx b/packages/react/src/combobox/portal/ComboboxPortal.test.tsx new file mode 100644 index 0000000000..6f990deca2 --- /dev/null +++ b/packages/react/src/combobox/portal/ComboboxPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Combobox } from '@base-ui-components/react/combobox'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/combobox/portal/ComboboxPortal.tsx b/packages/react/src/combobox/portal/ComboboxPortal.tsx index c343cb20c8..aaaeac2d41 100644 --- a/packages/react/src/combobox/portal/ComboboxPortal.tsx +++ b/packages/react/src/combobox/portal/ComboboxPortal.tsx @@ -9,9 +9,13 @@ import { selectors } from '../store'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. */ -export function ComboboxPortal(props: ComboboxPortal.Props) { - const { children, keepMounted = false, container } = props; +export const ComboboxPortal = React.forwardRef(function ComboboxPortal( + props: ComboboxPortal.Props, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted = false, ...portalProps } = props; const store = useComboboxRootContext(); @@ -25,22 +29,21 @@ export function ComboboxPortal(props: ComboboxPortal.Props) { return ( - {children} + ); +}); + +export namespace ComboboxPortal { + export interface State {} } -export interface ComboboxPortalProps { - children?: React.ReactNode; +export interface ComboboxPortalProps extends FloatingPortal.Props { /** * Whether to keep the portal mounted in the DOM while the popup is hidden. * @default false */ keepMounted?: boolean; - /** - * A parent element to render the portal element into. - */ - container?: HTMLElement | null | React.RefObject; } export namespace ComboboxPortal { diff --git a/packages/react/src/dialog/portal/DialogPortal.test.tsx b/packages/react/src/dialog/portal/DialogPortal.test.tsx new file mode 100644 index 0000000000..2525c58c82 --- /dev/null +++ b/packages/react/src/dialog/portal/DialogPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Dialog } from '@base-ui-components/react/dialog'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/dialog/portal/DialogPortal.tsx b/packages/react/src/dialog/portal/DialogPortal.tsx index bd80ecb978..f739f35c78 100644 --- a/packages/react/src/dialog/portal/DialogPortal.tsx +++ b/packages/react/src/dialog/portal/DialogPortal.tsx @@ -1,17 +1,21 @@ 'use client'; import * as React from 'react'; -import { FloatingPortal, FloatingPortalProps } from '../../floating-ui-react'; +import { FloatingPortal } from '../../floating-ui-react'; import { useDialogRootContext } from '../root/DialogRootContext'; import { DialogPortalContext } from './DialogPortalContext'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog) */ -export function DialogPortal(props: DialogPortal.Props) { - const { children, keepMounted = false, container } = props; +export const DialogPortal = React.forwardRef(function DialogPortal( + props: DialogPortal.Props, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted = false, ...portalProps } = props; const { store } = useDialogRootContext(); const mounted = store.useState('mounted'); @@ -23,13 +27,16 @@ export function DialogPortal(props: DialogPortal.Props) { return ( - {children} + ); +}); + +export namespace DialogPortal { + export interface State {} } -export interface DialogPortalProps { - children?: React.ReactNode; +export interface DialogPortalProps extends FloatingPortal.Props { /** * Whether to keep the portal mounted in the DOM while the popup is hidden. * @default false @@ -38,7 +45,7 @@ export interface DialogPortalProps { /** * A parent element to render the portal element into. */ - container?: FloatingPortalProps['root']; + container?: FloatingPortal.Props['container']; } export namespace DialogPortal { diff --git a/packages/react/src/floating-ui-react/components/FloatingFocusManager.test.tsx b/packages/react/src/floating-ui-react/components/FloatingFocusManager.test.tsx index ca19462d41..e0ef1ebd34 100644 --- a/packages/react/src/floating-ui-react/components/FloatingFocusManager.test.tsx +++ b/packages/react/src/floating-ui-react/components/FloatingFocusManager.test.tsx @@ -584,7 +584,7 @@ describe.skipIf(!isJSDOM)('FloatingFocusManager', () => { <> {React.cloneElement(children, getReferenceProps({ ref: refs.setReference }))} {open && ( - +
{render()} diff --git a/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx b/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx index 2630cfcf48..7ec4157b4d 100644 --- a/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx +++ b/packages/react/src/floating-ui-react/components/FloatingFocusManager.tsx @@ -879,7 +879,7 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS if (modal) { const els = getTabbableElements(); enqueueFocus(els[els.length - 1]); - } else if (portalContext?.preserveTabOrder && portalContext.portalNode) { + } else if (portalContext?.portalNode) { preventReturnFocusRef.current = false; if (isOutsideEvent(event, portalContext.portalNode)) { const nextTabbable = getNextTabbable(domReference); @@ -899,7 +899,7 @@ export function FloatingFocusManager(props: FloatingFocusManagerProps): React.JS onFocus={(event) => { if (modal) { enqueueFocus(getTabbableElements()[0]); - } else if (portalContext?.preserveTabOrder && portalContext.portalNode) { + } else if (portalContext?.portalNode) { if (closeOnFocusOut) { preventReturnFocusRef.current = true; } diff --git a/packages/react/src/floating-ui-react/components/FloatingPortal.test.tsx b/packages/react/src/floating-ui-react/components/FloatingPortal.test.tsx index a4ee6c681a..272b8b10ab 100644 --- a/packages/react/src/floating-ui-react/components/FloatingPortal.test.tsx +++ b/packages/react/src/floating-ui-react/components/FloatingPortal.test.tsx @@ -1,13 +1,15 @@ -import { fireEvent, flushMicrotasks, render, screen } from '@mui/internal-test-utils'; import * as React from 'react'; - +import { fireEvent, flushMicrotasks, render, screen } from '@mui/internal-test-utils'; import { isJSDOM } from '@base-ui-components/utils/detectBrowser'; import { FloatingPortal, useFloating } from '../index'; +import { FloatingPortalLite } from '../../utils/FloatingPortalLite'; +import type { UseFloatingPortalNodeProps } from './FloatingPortal'; -function App(props: { - root?: HTMLElement | null | React.RefObject; - id?: string; -}) { +interface AppProps { + container?: UseFloatingPortalNodeProps['container']; +} + +function App(props: AppProps) { const [open, setOpen] = React.useState(false); const { refs } = useFloating({ open, @@ -25,35 +27,11 @@ function App(props: { } describe.skipIf(!isJSDOM)('FloatingPortal', () => { - test('creates a custom id node', async () => { - render(); - await flushMicrotasks(); - expect(document.querySelector('#custom-id')).toBeInTheDocument(); - }); - - test('uses a custom id node as the root', async () => { + test('allows custom containers', async () => { const customRoot = document.createElement('div'); customRoot.id = 'custom-root'; document.body.appendChild(customRoot); - render(); - fireEvent.click(screen.getByTestId('reference')); - await flushMicrotasks(); - expect(screen.getByTestId('floating').parentElement?.parentElement).toBe(customRoot); - customRoot.remove(); - }); - - test('creates a custom id node as the root', async () => { - render(); - fireEvent.click(screen.getByTestId('reference')); - await flushMicrotasks(); - expect(screen.getByTestId('floating').parentElement?.parentElement?.id).toBe('custom-id'); - }); - - test('allows custom roots', async () => { - const customRoot = document.createElement('div'); - customRoot.id = 'custom-root'; - document.body.appendChild(customRoot); - render(); + render(); fireEvent.click(screen.getByTestId('reference')); await flushMicrotasks(); @@ -64,11 +42,11 @@ describe.skipIf(!isJSDOM)('FloatingPortal', () => { customRoot.remove(); }); - test('allows refs as roots', async () => { + test('allows refs as containers', async () => { const el = document.createElement('div'); document.body.appendChild(el); const ref = { current: el }; - render(); + render(); fireEvent.click(screen.getByTestId('reference')); await flushMicrotasks(); const parent = screen.getByTestId('floating').parentElement; @@ -77,19 +55,19 @@ describe.skipIf(!isJSDOM)('FloatingPortal', () => { document.body.removeChild(el); }); - test('allows roots to be initially null', async () => { + test('allows containers to be initially null', async () => { function RootApp() { - const [root, setRoot] = React.useState(null); - const [renderRoot, setRenderRoot] = React.useState(false); + const [container, setContainer] = React.useState(null); + const [renderContainer, setRenderContainer] = React.useState(false); React.useEffect(() => { - setRenderRoot(true); + setRenderContainer(true); }, []); return ( - {renderRoot &&
} - ; + {renderContainer &&
} + ); } @@ -103,4 +81,72 @@ describe.skipIf(!isJSDOM)('FloatingPortal', () => { const root = screen.getByTestId('root'); expect(root).toBe(subRoot?.parentElement); }); + + test('reattaches the portal when the container changes', async () => { + const customRoot = document.createElement('div'); + document.body.appendChild(customRoot); + + try { + function RootSwitcher() { + const [container, setContainer] = + React.useState(undefined); + + return ( + + + {open && ( - +
{children} @@ -978,8 +978,8 @@ describe.skipIf(!isJSDOM)('useDismiss', () => { return ( - - + + diff --git a/packages/react/src/floating-ui-react/types.ts b/packages/react/src/floating-ui-react/types.ts index ab04304b58..e0a348dc60 100644 --- a/packages/react/src/floating-ui-react/types.ts +++ b/packages/react/src/floating-ui-react/types.ts @@ -11,7 +11,7 @@ import type { ExtendedUserProps } from './hooks/useInteractions'; export * from '.'; export type { FloatingDelayGroupProps } from './components/FloatingDelayGroup'; export type { FloatingFocusManagerProps } from './components/FloatingFocusManager'; -export type { FloatingPortalProps, UseFloatingPortalNodeProps } from './components/FloatingPortal'; +export type { UseFloatingPortalNodeProps } from './components/FloatingPortal'; export type { UseClientPointProps } from './hooks/useClientPoint'; export type { UseDismissProps } from './hooks/useDismiss'; export type { UseFocusProps } from './hooks/useFocus'; diff --git a/packages/react/src/menu/portal/MenuPortal.test.tsx b/packages/react/src/menu/portal/MenuPortal.test.tsx new file mode 100644 index 0000000000..e542052f0b --- /dev/null +++ b/packages/react/src/menu/portal/MenuPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Menu } from '@base-ui-components/react/menu'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/menu/portal/MenuPortal.tsx b/packages/react/src/menu/portal/MenuPortal.tsx index 8a3dd4f34a..0a29f7f04e 100644 --- a/packages/react/src/menu/portal/MenuPortal.tsx +++ b/packages/react/src/menu/portal/MenuPortal.tsx @@ -1,17 +1,21 @@ 'use client'; import * as React from 'react'; -import { FloatingPortal, FloatingPortalProps } from '../../floating-ui-react'; +import { FloatingPortal } from '../../floating-ui-react'; import { useMenuRootContext } from '../root/MenuRootContext'; import { MenuPortalContext } from './MenuPortalContext'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu) */ -export function MenuPortal(props: MenuPortal.Props) { - const { children, keepMounted = false, container } = props; +export const MenuPortal = React.forwardRef(function MenuPortal( + props: MenuPortal.Props, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted = false, ...portalProps } = props; const { mounted } = useMenuRootContext(); @@ -22,22 +26,21 @@ export function MenuPortal(props: MenuPortal.Props) { return ( - {children} + ); +}); + +export namespace MenuPortal { + export interface State {} } -export interface MenuPortalProps { - children?: React.ReactNode; +export interface MenuPortalProps extends FloatingPortal.Props { /** * Whether to keep the portal mounted in the DOM while the popup is hidden. * @default false */ keepMounted?: boolean; - /** - * A parent element to render the portal element into. - */ - container?: FloatingPortalProps['root']; } export namespace MenuPortal { diff --git a/packages/react/src/navigation-menu/portal/NavigationMenuPortal.test.tsx b/packages/react/src/navigation-menu/portal/NavigationMenuPortal.test.tsx new file mode 100644 index 0000000000..ae097bac89 --- /dev/null +++ b/packages/react/src/navigation-menu/portal/NavigationMenuPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { NavigationMenu } from '@base-ui-components/react/navigation-menu'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/navigation-menu/portal/NavigationMenuPortal.tsx b/packages/react/src/navigation-menu/portal/NavigationMenuPortal.tsx index 6b57e7af3f..77af635f4a 100644 --- a/packages/react/src/navigation-menu/portal/NavigationMenuPortal.tsx +++ b/packages/react/src/navigation-menu/portal/NavigationMenuPortal.tsx @@ -7,11 +7,15 @@ import { NavigationMenuPortalContext } from './NavigationMenuPortalContext'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Navigation Menu](https://base-ui.com/react/components/navigation-menu) */ -export function NavigationMenuPortal(props: NavigationMenuPortal.Props) { - const { children, keepMounted = false, container } = props; +export const NavigationMenuPortal = React.forwardRef(function NavigationMenuPortal( + props: NavigationMenuPortal.Props, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted = false, ...portalProps } = props; const { mounted } = useNavigationMenuRootContext(); @@ -22,13 +26,17 @@ export function NavigationMenuPortal(props: NavigationMenuPortal.Props) { return ( - {children} + ); +}); + +export namespace NavigationMenuPortal { + export interface State {} } -export interface NavigationMenuPortalProps { - children?: React.ReactNode; +export interface NavigationMenuPortalProps + extends FloatingPortal.Props { /** * Whether to keep the portal mounted in the DOM while the popup is hidden. * @default false @@ -37,7 +45,7 @@ export interface NavigationMenuPortalProps { /** * A parent element to render the portal element into. */ - container?: HTMLElement | null | React.RefObject; + container?: FloatingPortal.Props['container']; } export namespace NavigationMenuPortal { diff --git a/packages/react/src/popover/portal/PopoverPortal.test.tsx b/packages/react/src/popover/portal/PopoverPortal.test.tsx new file mode 100644 index 0000000000..472ed2a6be --- /dev/null +++ b/packages/react/src/popover/portal/PopoverPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Popover } from '@base-ui-components/react/popover'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/popover/portal/PopoverPortal.tsx b/packages/react/src/popover/portal/PopoverPortal.tsx index 22f0fd8ab5..0b352184ba 100644 --- a/packages/react/src/popover/portal/PopoverPortal.tsx +++ b/packages/react/src/popover/portal/PopoverPortal.tsx @@ -1,17 +1,21 @@ 'use client'; import * as React from 'react'; -import { FloatingPortal, FloatingPortalProps } from '../../floating-ui-react'; +import { FloatingPortal } from '../../floating-ui-react'; import { usePopoverRootContext } from '../root/PopoverRootContext'; import { PopoverPortalContext } from './PopoverPortalContext'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Popover](https://base-ui.com/react/components/popover) */ -export function PopoverPortal(props: PopoverPortal.Props) { - const { children, keepMounted = false, container } = props; +export const PopoverPortal = React.forwardRef(function PopoverPortal( + props: PopoverPortal.Props, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted = false, ...portalProps } = props; const { store } = usePopoverRootContext(); const mounted = store.useState('mounted'); @@ -23,24 +27,21 @@ export function PopoverPortal(props: PopoverPortal.Props) { return ( - - {children} - + ); +}); + +export namespace PopoverPortal { + export interface State {} } -export interface PopoverPortalProps { - children?: React.ReactNode; +export interface PopoverPortalProps extends FloatingPortal.Props { /** * Whether to keep the portal mounted in the DOM while the popup is hidden. * @default false */ keepMounted?: boolean; - /** - * A parent element to render the portal element into. - */ - container?: FloatingPortalProps['root']; } export namespace PopoverPortal { diff --git a/packages/react/src/preview-card/portal/PreviewCardPortal.test.tsx b/packages/react/src/preview-card/portal/PreviewCardPortal.test.tsx new file mode 100644 index 0000000000..9a83dbd2c9 --- /dev/null +++ b/packages/react/src/preview-card/portal/PreviewCardPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { PreviewCard } from '@base-ui-components/react/preview-card'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/preview-card/portal/PreviewCardPortal.tsx b/packages/react/src/preview-card/portal/PreviewCardPortal.tsx index e28a050fca..87267414c3 100644 --- a/packages/react/src/preview-card/portal/PreviewCardPortal.tsx +++ b/packages/react/src/preview-card/portal/PreviewCardPortal.tsx @@ -3,16 +3,19 @@ import * as React from 'react'; import { usePreviewCardRootContext } from '../root/PreviewCardContext'; import { PreviewCardPortalContext } from './PreviewCardPortalContext'; import { FloatingPortalLite } from '../../utils/FloatingPortalLite'; -import type { FloatingPortalProps } from '../../floating-ui-react'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Preview Card](https://base-ui.com/react/components/preview-card) */ -export function PreviewCardPortal(props: PreviewCardPortal.Props) { - const { children, keepMounted = false, container } = props; +export const PreviewCardPortal = React.forwardRef(function PreviewCardPortal( + props: PreviewCardPortal.Props, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted = false, ...portalProps } = props; const { mounted } = usePreviewCardRootContext(); @@ -23,22 +26,21 @@ export function PreviewCardPortal(props: PreviewCardPortal.Props) { return ( - {children} + ); +}); + +export namespace PreviewCardPortal { + export interface State {} } -export interface PreviewCardPortalProps { - children?: React.ReactNode; +export interface PreviewCardPortalProps extends FloatingPortalLite.Props { /** * Whether to keep the portal mounted in the DOM while the popup is hidden. * @default false */ keepMounted?: boolean; - /** - * A parent element to render the portal element into. - */ - container?: FloatingPortalProps['root']; } export namespace PreviewCardPortal { diff --git a/packages/react/src/select/portal/SelectPortal.test.tsx b/packages/react/src/select/portal/SelectPortal.test.tsx new file mode 100644 index 0000000000..0bbd5a10ec --- /dev/null +++ b/packages/react/src/select/portal/SelectPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Select } from '@base-ui-components/react/select'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/select/portal/SelectPortal.tsx b/packages/react/src/select/portal/SelectPortal.tsx index e8ce69f124..68575c7afb 100644 --- a/packages/react/src/select/portal/SelectPortal.tsx +++ b/packages/react/src/select/portal/SelectPortal.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; import { useStore } from '@base-ui-components/utils/store'; -import { FloatingPortal, FloatingPortalProps } from '../../floating-ui-react'; +import { FloatingPortal } from '../../floating-ui-react'; import { SelectPortalContext } from './SelectPortalContext'; import { useSelectRootContext } from '../root/SelectRootContext'; import { selectors } from '../store'; @@ -9,12 +9,14 @@ import { selectors } from '../store'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Select](https://base-ui.com/react/components/select) */ -export function SelectPortal(props: SelectPortal.Props) { - const { children, container } = props; - +export const SelectPortal = React.forwardRef(function SelectPortal( + portalProps: SelectPortal.Props, + forwardedRef: React.ForwardedRef, +) { const { store } = useSelectRootContext(); const mounted = useStore(store, selectors.mounted); const forceMount = useStore(store, selectors.forceMount); @@ -26,19 +28,17 @@ export function SelectPortal(props: SelectPortal.Props) { return ( - {children} + ); -} +}); -export interface SelectPortalProps { - children?: React.ReactNode; - /** - * A parent element to render the portal element into. - */ - container?: FloatingPortalProps['root']; +export namespace SelectPortal { + export interface State {} } +export interface SelectPortalProps extends FloatingPortal.Props {} + export namespace SelectPortal { export type Props = SelectPortalProps; } diff --git a/packages/react/src/toast/portal/ToastPortal.test.tsx b/packages/react/src/toast/portal/ToastPortal.test.tsx new file mode 100644 index 0000000000..b21f3444fc --- /dev/null +++ b/packages/react/src/toast/portal/ToastPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Toast } from '@base-ui-components/react/toast'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render(node); + }, + })); +}); diff --git a/packages/react/src/toast/portal/ToastPortal.tsx b/packages/react/src/toast/portal/ToastPortal.tsx index 7fb269aab7..a2db5e46fa 100644 --- a/packages/react/src/toast/portal/ToastPortal.tsx +++ b/packages/react/src/toast/portal/ToastPortal.tsx @@ -1,26 +1,21 @@ 'use client'; -import * as React from 'react'; import { FloatingPortalLite } from '../../utils/FloatingPortalLite'; /** * A portal element that moves the viewport to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Toast](https://base-ui.com/react/components/toast) */ -export function ToastPortal(props: ToastPortal.Props) { - const { children, container } = props; - return {children}; -} +export const ToastPortal = FloatingPortalLite; -export interface ToastPortalProps { - children?: React.ReactNode; - /** - * A parent element to render the portal element into. - */ - container?: HTMLElement | null | React.RefObject; +export namespace ToastPortal { + export interface State {} } +export interface ToastPortalProps extends FloatingPortalLite.Props {} + export namespace ToastPortal { export type Props = ToastPortalProps; } diff --git a/packages/react/src/tooltip/portal/TooltipPortal.test.tsx b/packages/react/src/tooltip/portal/TooltipPortal.test.tsx new file mode 100644 index 0000000000..0f599bbee4 --- /dev/null +++ b/packages/react/src/tooltip/portal/TooltipPortal.test.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Tooltip } from '@base-ui-components/react/tooltip'; +import { createRenderer, describeConformance } from '#test-utils'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + })); +}); diff --git a/packages/react/src/tooltip/portal/TooltipPortal.tsx b/packages/react/src/tooltip/portal/TooltipPortal.tsx index 19478e00d2..dea4b01352 100644 --- a/packages/react/src/tooltip/portal/TooltipPortal.tsx +++ b/packages/react/src/tooltip/portal/TooltipPortal.tsx @@ -3,16 +3,19 @@ import * as React from 'react'; import { useTooltipRootContext } from '../root/TooltipRootContext'; import { TooltipPortalContext } from './TooltipPortalContext'; import { FloatingPortalLite } from '../../utils/FloatingPortalLite'; -import type { FloatingPortalProps } from '../../floating-ui-react'; /** * A portal element that moves the popup to a different part of the DOM. * By default, the portal element is appended to ``. + * Renders a `
` element. * * Documentation: [Base UI Tooltip](https://base-ui.com/react/components/tooltip) */ -export function TooltipPortal(props: TooltipPortal.Props) { - const { children, keepMounted = false, container } = props; +export const TooltipPortal = React.forwardRef(function TooltipPortal( + props: TooltipPortal.Props, + forwardedRef: React.ForwardedRef, +) { + const { keepMounted = false, ...portalProps } = props; const { mounted } = useTooltipRootContext(); @@ -23,22 +26,21 @@ export function TooltipPortal(props: TooltipPortal.Props) { return ( - {children} + ); +}); + +export namespace TooltipPortal { + export interface State {} } -export interface TooltipPortalProps { - children?: React.ReactNode; +export interface TooltipPortalProps extends FloatingPortalLite.Props { /** * Whether to keep the portal mounted in the DOM while the popup is hidden. * @default false */ keepMounted?: boolean; - /** - * A parent element to render the portal element into. - */ - container?: FloatingPortalProps['root']; } export namespace TooltipPortal { diff --git a/packages/react/src/utils/FloatingPortalLite.tsx b/packages/react/src/utils/FloatingPortalLite.tsx index c86d904d2a..c49358943e 100644 --- a/packages/react/src/utils/FloatingPortalLite.tsx +++ b/packages/react/src/utils/FloatingPortalLite.tsx @@ -1,22 +1,40 @@ 'use client'; +import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { FloatingPortalProps, useFloatingPortalNode } from '../floating-ui-react'; +import { useFloatingPortalNode, type FloatingPortal } from '../floating-ui-react'; /** * `FloatingPortal` includes tabbable logic handling for focus management. * For components that don't need tabbable logic, use `FloatingPortalLite`. * @internal */ -export function FloatingPortalLite(props: FloatingPortalLite.Props) { - const node = useFloatingPortalNode({ root: props.root }); - return node && ReactDOM.createPortal(props.children, node); -} +export const FloatingPortalLite = React.forwardRef(function FloatingPortalLite( + componentProps: FloatingPortalLite.Props, + forwardedRef: React.ForwardedRef, +) { + const { children, container, className, render, ...elementProps } = componentProps; -export interface FloatingPortalLiteProps { - children?: React.ReactNode; - root?: FloatingPortalProps['root']; -} + const { portalNode, portalSubtree } = useFloatingPortalNode({ + container, + ref: forwardedRef, + componentProps, + elementProps, + }); + + if (!portalSubtree && !portalNode) { + return null; + } + + return ( + + {portalSubtree} + {portalNode && ReactDOM.createPortal(children, portalNode)} + + ); +}); + +export interface FloatingPortalLiteProps extends FloatingPortal.Props {} export namespace FloatingPortalLite { - export type Props = FloatingPortalLiteProps; + export type Props = FloatingPortalLiteProps; }