From 7a73a793d26374b031ba648061a54d0d96f7c480 Mon Sep 17 00:00:00 2001 From: atomiks Date: Tue, 24 Dec 2024 13:52:49 +1100 Subject: [PATCH 1/4] [popups] Require Portal part Remove keepMounted on portal child parts Remove unused imports Remove keepMounted prop in tests --- .../generated/alert-dialog-backdrop.json | 5 - .../generated/alert-dialog-popup.json | 5 - .../generated/alert-dialog-portal.json | 17 + docs/reference/generated/dialog-backdrop.json | 5 - docs/reference/generated/dialog-popup.json | 5 - docs/reference/generated/dialog-portal.json | 17 + docs/reference/generated/menu-backdrop.json | 5 - .../{portal.json => menu-portal.json} | 4 +- docs/reference/generated/menu-positioner.json | 5 - .../reference/generated/popover-backdrop.json | 5 - docs/reference/generated/popover-portal.json | 17 + .../generated/popover-positioner.json | 5 - .../generated/preview-card-backdrop.json | 5 - .../generated/preview-card-portal.json | 17 + .../generated/preview-card-positioner.json | 5 - docs/reference/generated/select-backdrop.json | 5 - docs/reference/generated/select-portal.json | 2 +- docs/reference/generated/tooltip-portal.json | 17 + .../generated/tooltip-positioner.json | 5 - .../experiments/anchor-positioning.tsx | 1 + .../experiments/anchor-side-animations.tsx | 16 +- docs/src/app/(private)/experiments/dialog.tsx | 107 +++--- .../(private)/experiments/menu-anchor-el.tsx | 32 +- .../(private)/experiments/menu-anchor-ref.tsx | 32 +- .../app/(private)/experiments/menu-nested.tsx | 142 ++++--- .../app/(private)/experiments/menu-rtl.tsx | 142 ++++--- .../app/(private)/experiments/modality.tsx | 70 ++-- .../experiments/popup-transform-origin.tsx | 20 +- .../experiments/popups-in-popups.tsx | 234 ++++++------ docs/src/app/(private)/experiments/rtl.tsx | 346 ++++++++++-------- .../app/(private)/experiments/select-perf.tsx | 66 ++-- .../src/app/(private)/experiments/tooltip.tsx | 160 ++++---- .../dialog/demos/hero/css-modules/index.tsx | 2 +- .../backdrop/AlertDialogBackdrop.tsx | 17 +- .../close/AlertDialogClose.test.tsx | 4 +- .../AlertDialogDescription.test.tsx | 4 +- .../react/src/alert-dialog/index.parts.ts | 2 +- .../popup/AlertDialogPopup.test.tsx | 64 ++-- .../alert-dialog/popup/AlertDialogPopup.tsx | 19 +- .../alert-dialog/portal/AlertDialogPortal.tsx | 67 ++++ .../portal/AlertDialogPortalContext.ts | 11 + .../root/AlertDialogRoot.test.tsx | 32 +- .../src/alert-dialog/root/AlertDialogRoot.tsx | 3 +- .../title/AlertDialogTitle.test.tsx | 4 +- .../src/dialog/backdrop/DialogBackdrop.tsx | 17 +- .../src/dialog/close/DialogClose.test.tsx | 4 +- .../description/DialogDescription.test.tsx | 4 +- packages/react/src/dialog/index.parts.ts | 2 +- .../src/dialog/popup/DialogPopup.test.tsx | 60 +-- .../react/src/dialog/popup/DialogPopup.tsx | 19 +- .../react/src/dialog/portal/DialogPortal.tsx | 67 ++++ .../src/dialog/portal/DialogPortalContext.ts | 11 + .../react/src/dialog/root/DialogRoot.test.tsx | 81 ++-- packages/react/src/dialog/root/DialogRoot.tsx | 4 +- .../src/dialog/title/DialogTitle.test.tsx | 4 +- .../react/src/field/root/FieldRoot.test.tsx | 28 +- .../react/src/menu/arrow/MenuArrow.test.tsx | 8 +- .../react/src/menu/backdrop/MenuBackdrop.tsx | 20 +- .../MenuCheckboxItemIndicator.test.tsx | 54 +-- .../checkbox-item/MenuCheckboxItem.test.tsx | 130 ++++--- .../menu/group-label/MenuGroupLabel.test.tsx | 48 +-- packages/react/src/menu/index.parts.ts | 2 +- .../react/src/menu/item/MenuItem.test.tsx | 74 ++-- .../react/src/menu/popup/MenuPopup.test.tsx | 4 +- .../Portal.tsx => menu/portal/MenuPortal.tsx} | 37 +- .../src/menu/portal/MenuPortalContext.ts | 11 + .../menu/positioner/MenuPositioner.test.tsx | 136 ++++--- .../src/menu/positioner/MenuPositioner.tsx | 14 +- .../src/menu/positioner/useMenuPositioner.ts | 15 +- .../MenuRadioItemIndicator.test.tsx | 78 ++-- .../menu/radio-item/MenuRadioItem.test.tsx | 166 +++++---- .../react/src/menu/root/MenuRoot.test.tsx | 330 +++++++++-------- packages/react/src/menu/root/MenuRoot.tsx | 11 +- .../src/menu/trigger/MenuTrigger.test.tsx | 28 +- .../src/popover/arrow/PopoverArrow.test.tsx | 8 +- .../src/popover/backdrop/PopoverBackdrop.tsx | 18 +- .../src/popover/close/PopoverClose.test.tsx | 22 +- .../description/PopoverDescription.test.tsx | 20 +- packages/react/src/popover/index.parts.ts | 2 +- .../src/popover/popup/PopoverPopup.test.tsx | 86 +++-- .../src/popover/portal/PopoverPortal.tsx | 67 ++++ .../popover/portal/PopoverPortalContext.ts | 11 + .../positioner/PopoverPositioner.test.tsx | 14 +- .../popover/positioner/PopoverPositioner.tsx | 15 +- .../positioner/usePopoverPositioner.tsx | 15 +- .../src/popover/root/PopoverRoot.test.tsx | 150 +++++--- .../react/src/popover/root/PopoverRoot.tsx | 5 +- .../src/popover/title/PopoverTitle.test.tsx | 20 +- packages/react/src/portal/PortalContext.ts | 17 - .../arrow/PreviewCardArrow.test.tsx | 8 +- .../backdrop/PreviewCardBackdrop.tsx | 18 +- .../react/src/preview-card/index.parts.ts | 2 +- .../popup/PreviewCardPopup.test.tsx | 12 +- .../preview-card/portal/PreviewCardPortal.tsx | 67 ++++ .../portal/PreviewCardPortalContext.ts | 11 + .../positioner/PreviewCardPositioner.test.tsx | 6 +- .../positioner/PreviewCardPositioner.tsx | 15 +- .../positioner/usePreviewCardPositioner.ts | 15 +- .../root/PreviewCardRoot.test.tsx | 138 ++++--- .../src/preview-card/root/PreviewCardRoot.tsx | 3 +- .../src/select/backdrop/SelectBackdrop.tsx | 18 +- .../react/src/select/item/SelectItem.test.tsx | 92 +++-- packages/react/src/select/item/SelectItem.tsx | 28 +- .../src/select/popup/SelectPopup.test.tsx | 4 +- .../react/src/select/portal/SelectPortal.tsx | 21 +- .../src/select/portal/SelectPortalContext.ts | 11 + .../positioner/SelectPositioner.test.tsx | 6 +- .../select/positioner/useSelectPositioner.ts | 5 - .../react/src/select/root/SelectRoot.test.tsx | 140 ++++--- packages/react/src/select/root/SelectRoot.tsx | 5 +- .../src/select/root/SelectRootContext.ts | 1 + .../react/src/select/root/useSelectRoot.ts | 39 +- .../src/tooltip/arrow/TooltipArrow.test.tsx | 8 +- packages/react/src/tooltip/index.parts.ts | 2 +- .../src/tooltip/popup/TooltipPopup.test.tsx | 12 +- .../src/tooltip/portal/TooltipPortal.tsx | 67 ++++ .../tooltip/portal/TooltipPortalContext.ts | 11 + .../positioner/TooltipPositioner.test.tsx | 6 +- .../tooltip/positioner/TooltipPositioner.tsx | 15 +- .../positioner/useTooltipPositioner.ts | 15 +- .../tooltip/provider/TooltipProvider.test.tsx | 16 +- .../src/tooltip/root/TooltipRoot.test.tsx | 112 +++--- .../react/src/tooltip/root/TooltipRoot.tsx | 5 +- .../react/src/utils/useAnchorPositioning.ts | 2 +- 124 files changed, 2702 insertions(+), 1919 deletions(-) create mode 100644 docs/reference/generated/alert-dialog-portal.json create mode 100644 docs/reference/generated/dialog-portal.json rename docs/reference/generated/{portal.json => menu-portal.json} (82%) create mode 100644 docs/reference/generated/popover-portal.json create mode 100644 docs/reference/generated/preview-card-portal.json create mode 100644 docs/reference/generated/tooltip-portal.json create mode 100644 packages/react/src/alert-dialog/portal/AlertDialogPortal.tsx create mode 100644 packages/react/src/alert-dialog/portal/AlertDialogPortalContext.ts create mode 100644 packages/react/src/dialog/portal/DialogPortal.tsx create mode 100644 packages/react/src/dialog/portal/DialogPortalContext.ts rename packages/react/src/{portal/Portal.tsx => menu/portal/MenuPortal.tsx} (66%) create mode 100644 packages/react/src/menu/portal/MenuPortalContext.ts create mode 100644 packages/react/src/popover/portal/PopoverPortal.tsx create mode 100644 packages/react/src/popover/portal/PopoverPortalContext.ts delete mode 100644 packages/react/src/portal/PortalContext.ts create mode 100644 packages/react/src/preview-card/portal/PreviewCardPortal.tsx create mode 100644 packages/react/src/preview-card/portal/PreviewCardPortalContext.ts create mode 100644 packages/react/src/select/portal/SelectPortalContext.ts create mode 100644 packages/react/src/tooltip/portal/TooltipPortal.tsx create mode 100644 packages/react/src/tooltip/portal/TooltipPortalContext.ts diff --git a/docs/reference/generated/alert-dialog-backdrop.json b/docs/reference/generated/alert-dialog-backdrop.json index 0f6c03789b..51d2ad0a79 100644 --- a/docs/reference/generated/alert-dialog-backdrop.json +++ b/docs/reference/generated/alert-dialog-backdrop.json @@ -6,11 +6,6 @@ "type": "string | (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 element in the DOM while the alert dialog is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/alert-dialog-popup.json b/docs/reference/generated/alert-dialog-popup.json index 4abf067b82..b8a174b76f 100644 --- a/docs/reference/generated/alert-dialog-popup.json +++ b/docs/reference/generated/alert-dialog-popup.json @@ -14,11 +14,6 @@ "type": "string | (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 element in the DOM while the alert dialog is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/alert-dialog-portal.json b/docs/reference/generated/alert-dialog-portal.json new file mode 100644 index 0000000000..0422346f24 --- /dev/null +++ b/docs/reference/generated/alert-dialog-portal.json @@ -0,0 +1,17 @@ +{ + "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 ``.", + "props": { + "container": { + "type": "React.Ref | HTMLElement | null", + "description": "A parent element to render the portal element into." + }, + "keepMounted": { + "type": "boolean", + "default": "false", + "description": "Whether to keep the portal mounted in the DOM while the popup is hidden." + } + }, + "dataAttributes": {}, + "cssVariables": {} +} diff --git a/docs/reference/generated/dialog-backdrop.json b/docs/reference/generated/dialog-backdrop.json index 801ca1fab0..db2b4ba5b7 100644 --- a/docs/reference/generated/dialog-backdrop.json +++ b/docs/reference/generated/dialog-backdrop.json @@ -6,11 +6,6 @@ "type": "string | (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 HTML element in the DOM while the dialog is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/dialog-popup.json b/docs/reference/generated/dialog-popup.json index 1a0128d58f..186de3d19c 100644 --- a/docs/reference/generated/dialog-popup.json +++ b/docs/reference/generated/dialog-popup.json @@ -14,11 +14,6 @@ "type": "string | (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 HTML element in the DOM while the dialog is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/dialog-portal.json b/docs/reference/generated/dialog-portal.json new file mode 100644 index 0000000000..87565ef214 --- /dev/null +++ b/docs/reference/generated/dialog-portal.json @@ -0,0 +1,17 @@ +{ + "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 ``.", + "props": { + "container": { + "type": "React.Ref | HTMLElement | null", + "description": "A parent element to render the portal element into." + }, + "keepMounted": { + "type": "boolean", + "default": "false", + "description": "Whether to keep the portal mounted in the DOM while the popup is hidden." + } + }, + "dataAttributes": {}, + "cssVariables": {} +} diff --git a/docs/reference/generated/menu-backdrop.json b/docs/reference/generated/menu-backdrop.json index 58832ed4bc..de7938fded 100644 --- a/docs/reference/generated/menu-backdrop.json +++ b/docs/reference/generated/menu-backdrop.json @@ -6,11 +6,6 @@ "type": "string | (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 HTML element in the DOM while the menu is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/portal.json b/docs/reference/generated/menu-portal.json similarity index 82% rename from docs/reference/generated/portal.json rename to docs/reference/generated/menu-portal.json index b0e0ba31d5..5a7bcc3858 100644 --- a/docs/reference/generated/portal.json +++ b/docs/reference/generated/menu-portal.json @@ -1,10 +1,10 @@ { - "name": "Portal", + "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 ``.", "props": { "container": { "type": "React.Ref | HTMLElement | null", - "description": "A parent element to render the portal into." + "description": "A parent element to render the portal element into." }, "keepMounted": { "type": "boolean", diff --git a/docs/reference/generated/menu-positioner.json b/docs/reference/generated/menu-positioner.json index dc8fc9f0ac..105a5f811a 100644 --- a/docs/reference/generated/menu-positioner.json +++ b/docs/reference/generated/menu-positioner.json @@ -53,11 +53,6 @@ "type": "string | (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 HTML element in the DOM while the menu is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/popover-backdrop.json b/docs/reference/generated/popover-backdrop.json index d3e69417e7..4b81ad5446 100644 --- a/docs/reference/generated/popover-backdrop.json +++ b/docs/reference/generated/popover-backdrop.json @@ -6,11 +6,6 @@ "type": "string | (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 HTML element in the DOM while the popover is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/popover-portal.json b/docs/reference/generated/popover-portal.json new file mode 100644 index 0000000000..4a4554c421 --- /dev/null +++ b/docs/reference/generated/popover-portal.json @@ -0,0 +1,17 @@ +{ + "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 ``.", + "props": { + "container": { + "type": "React.Ref | HTMLElement | null", + "description": "A parent element to render the portal element into." + }, + "keepMounted": { + "type": "boolean", + "default": "false", + "description": "Whether to keep the portal mounted in the DOM while the popup is hidden." + } + }, + "dataAttributes": {}, + "cssVariables": {} +} diff --git a/docs/reference/generated/popover-positioner.json b/docs/reference/generated/popover-positioner.json index 9a155f18dd..5d7d915bcc 100644 --- a/docs/reference/generated/popover-positioner.json +++ b/docs/reference/generated/popover-positioner.json @@ -55,11 +55,6 @@ "type": "string | (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 HTML element in the DOM while the popover is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/preview-card-backdrop.json b/docs/reference/generated/preview-card-backdrop.json index 667211ad56..815bae8001 100644 --- a/docs/reference/generated/preview-card-backdrop.json +++ b/docs/reference/generated/preview-card-backdrop.json @@ -6,11 +6,6 @@ "type": "string | (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 HTML element in the DOM while the preview card is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/preview-card-portal.json b/docs/reference/generated/preview-card-portal.json new file mode 100644 index 0000000000..34c82e1b85 --- /dev/null +++ b/docs/reference/generated/preview-card-portal.json @@ -0,0 +1,17 @@ +{ + "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 ``.", + "props": { + "container": { + "type": "React.Ref | HTMLElement | null", + "description": "A parent element to render the portal element into." + }, + "keepMounted": { + "type": "boolean", + "default": "false", + "description": "Whether to keep the portal mounted in the DOM while the popup is hidden." + } + }, + "dataAttributes": {}, + "cssVariables": {} +} diff --git a/docs/reference/generated/preview-card-positioner.json b/docs/reference/generated/preview-card-positioner.json index 5fea11a83e..8ec325c32e 100644 --- a/docs/reference/generated/preview-card-positioner.json +++ b/docs/reference/generated/preview-card-positioner.json @@ -55,11 +55,6 @@ "type": "string | (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 HTML element in the DOM while the preview card is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/select-backdrop.json b/docs/reference/generated/select-backdrop.json index bd55c2bf52..2dab04eafd 100644 --- a/docs/reference/generated/select-backdrop.json +++ b/docs/reference/generated/select-backdrop.json @@ -6,11 +6,6 @@ "type": "string | (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 HTML element in the DOM while the select menu is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/reference/generated/select-portal.json b/docs/reference/generated/select-portal.json index d568bb6286..72a3729bc6 100644 --- a/docs/reference/generated/select-portal.json +++ b/docs/reference/generated/select-portal.json @@ -4,7 +4,7 @@ "props": { "container": { "type": "React.Ref | HTMLElement | null", - "description": "A parent element to render the portal into." + "description": "A parent element to render the portal element into." } }, "dataAttributes": {}, diff --git a/docs/reference/generated/tooltip-portal.json b/docs/reference/generated/tooltip-portal.json new file mode 100644 index 0000000000..41be076137 --- /dev/null +++ b/docs/reference/generated/tooltip-portal.json @@ -0,0 +1,17 @@ +{ + "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 ``.", + "props": { + "container": { + "type": "React.Ref | HTMLElement | null", + "description": "A parent element to render the portal element into." + }, + "keepMounted": { + "type": "boolean", + "default": "false", + "description": "Whether to keep the portal mounted in the DOM while the popup is hidden." + } + }, + "dataAttributes": {}, + "cssVariables": {} +} diff --git a/docs/reference/generated/tooltip-positioner.json b/docs/reference/generated/tooltip-positioner.json index 538059bb6a..5ca373df6f 100644 --- a/docs/reference/generated/tooltip-positioner.json +++ b/docs/reference/generated/tooltip-positioner.json @@ -55,11 +55,6 @@ "type": "string | (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 HTML element in the DOM while the tooltip is hidden." - }, "render": { "type": "React.ReactElement | (props, state) => React.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." diff --git a/docs/src/app/(private)/experiments/anchor-positioning.tsx b/docs/src/app/(private)/experiments/anchor-positioning.tsx index 8c3a4107a3..bad594ce66 100644 --- a/docs/src/app/(private)/experiments/anchor-positioning.tsx +++ b/docs/src/app/(private)/experiments/anchor-positioning.tsx @@ -49,6 +49,7 @@ export default function AnchorPositioning() { arrowPadding, trackAnchor, mounted: true, + keepMounted: true, }); const handleInitialScroll = React.useCallback((node: HTMLDivElement | null) => { diff --git a/docs/src/app/(private)/experiments/anchor-side-animations.tsx b/docs/src/app/(private)/experiments/anchor-side-animations.tsx index eecc703447..678d44e0c1 100644 --- a/docs/src/app/(private)/experiments/anchor-side-animations.tsx +++ b/docs/src/app/(private)/experiments/anchor-side-animations.tsx @@ -13,16 +13,20 @@ export default function AnchorSideAnimations() {

transition - - - + + + + + animation - - - + + + + + ); diff --git a/docs/src/app/(private)/experiments/dialog.tsx b/docs/src/app/(private)/experiments/dialog.tsx index aef9c97950..1573d4d1c7 100644 --- a/docs/src/app/(private)/experiments/dialog.tsx +++ b/docs/src/app/(private)/experiments/dialog.tsx @@ -46,15 +46,17 @@ function renderContent( Open nested - - {renderContent( - `Nested dialog ${NESTED_DIALOGS + 1 - includeNested}`, - includeNested - 1, - nestedClassName, - modal, - dismissible, - )} - + + + {renderContent( + `Nested dialog ${NESTED_DIALOGS + 1 - includeNested}`, + includeNested - 1, + nestedClassName, + modal, + dismissible, + )} + + ) : null} @@ -72,23 +74,20 @@ function CssTransitionDialogDemo({ keepMounted, modal, dismissible }: DemoProps) Open with CSS transition - - - - {renderContent( - 'Dialog with CSS transitions', - NESTED_DIALOGS, - classes.withTransitions, - modal, - dismissible, - )} - + + + + {renderContent( + 'Dialog with CSS transitions', + NESTED_DIALOGS, + classes.withTransitions, + modal, + dismissible, + )} + + ); @@ -102,23 +101,20 @@ function CssAnimationDialogDemo({ keepMounted, modal, dismissible }: DemoProps) Open with CSS animation - - - - {renderContent( - 'Dialog with CSS animations', - NESTED_DIALOGS, - classes.withAnimations, - modal, - dismissible, - )} - + + + + {renderContent( + 'Dialog with CSS animations', + NESTED_DIALOGS, + classes.withAnimations, + modal, + dismissible, + )} + + ); @@ -141,18 +137,19 @@ function ReactSpringDialogDemo({ keepMounted, modal, dismissible }: DemoProps) { /> - - {renderContent( - 'Dialog with ReactSpring transitions', - 3, - classes.withReactSpringTransition, - modal, - dismissible, - )} - + + + {renderContent( + 'Dialog with ReactSpring transitions', + 3, + classes.withReactSpringTransition, + modal, + dismissible, + )} + + diff --git a/docs/src/app/(private)/experiments/menu-anchor-el.tsx b/docs/src/app/(private)/experiments/menu-anchor-el.tsx index 01bf4b83fe..39479cb555 100644 --- a/docs/src/app/(private)/experiments/menu-anchor-el.tsx +++ b/docs/src/app/(private)/experiments/menu-anchor-el.tsx @@ -14,21 +14,23 @@ export default function Page() {

Element passed to anchor

Trigger - - - - One - - - Two - - - + + + + + One + + + Two + + + +
Ref passed to anchor Trigger - - - - One - - - Two - - - + + + + + One + + + Two + + + +
Text color - - - - Black - - - Dark grey - - - Accent - - - + + + + + Black + + + Dark grey + + + Accent + + + + Style - - - - Heading - - - + + + + Heading + + - Level 1 - - + + Level 1 + + + Level 2 + + + Level 3 + + + + + + + Paragraph + + + List + + - Level 2 - - - Level 3 - - - - - - Paragraph - - - List - - - - Ordered - - - Unordered - - - - - - + + + Ordered + + + Unordered + + + + + + + + diff --git a/docs/src/app/(private)/experiments/menu-rtl.tsx b/docs/src/app/(private)/experiments/menu-rtl.tsx index 1aee9f2a55..d1c2b1e2a3 100644 --- a/docs/src/app/(private)/experiments/menu-rtl.tsx +++ b/docs/src/app/(private)/experiments/menu-rtl.tsx @@ -16,44 +16,38 @@ export default function RtlPopover() { - - - - - - Notifications - - You are all caught up. Good job! - - - + + + + + + + Notifications + + You are all caught up. Good job! + + + + - - - - - - Notifications - - You are all caught up. Good job! - - - + + + + + + + Notifications + + You are all caught up. Good job! + + + +
@@ -62,52 +56,56 @@ export default function RtlPopover() { Song - - - - - - Add to Library - Add to Playlist - - Play Next - Play Last - - Favorite - Share - - + + + + + + + Add to Library + Add to Playlist + + Play Next + Play Last + + Favorite + Share + + + Song - - - - - - Add to Library - Add to Playlist - - Play Next - Play Last - - Favorite - Share - - + + + + + + + Add to Library + Add to Playlist + + Play Next + Play Last + + Favorite + Share + + +
diff --git a/docs/src/app/(private)/experiments/modality.tsx b/docs/src/app/(private)/experiments/modality.tsx index 2ecb578fcc..2a528b609c 100644 --- a/docs/src/app/(private)/experiments/modality.tsx +++ b/docs/src/app/(private)/experiments/modality.tsx @@ -41,22 +41,24 @@ function SelectDemo({ modal, withBackdrop }: Props) { {withBackdrop && } />} - }> - - - } /> - System font - - - } /> - Arial - - - } /> - Roboto - - - + + }> + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + + ); } @@ -68,11 +70,15 @@ function MenuDemo({ modal, withBackdrop }: Props) { {withBackdrop && } />} - }> - - console.log('Log out clicked')}>Log out - - + + }> + + console.log('Log out clicked')}> + Log out + + + + ); } @@ -84,16 +90,18 @@ function DialogDemo({ modal, withBackdrop }: Props) { {withBackdrop && } />} - - Subscribe - - Enter your email address to subscribe to our newsletter. - - - Subscribe - Cancel - - + + + Subscribe + + Enter your email address to subscribe to our newsletter. + + + Subscribe + Cancel + + + ); } diff --git a/docs/src/app/(private)/experiments/popup-transform-origin.tsx b/docs/src/app/(private)/experiments/popup-transform-origin.tsx index 98b804e5d2..97731bc862 100644 --- a/docs/src/app/(private)/experiments/popup-transform-origin.tsx +++ b/docs/src/app/(private)/experiments/popup-transform-origin.tsx @@ -9,9 +9,11 @@ function Popover({ side }: { side: Side }) { {side} - - - + + + + + ); } @@ -22,11 +24,13 @@ function PopoverWithArrow({ side }: { side: Side }) { {side} - - - - - + + + + + + + ); } diff --git a/docs/src/app/(private)/experiments/popups-in-popups.tsx b/docs/src/app/(private)/experiments/popups-in-popups.tsx index f8b7aad6d6..8568edcea1 100644 --- a/docs/src/app/(private)/experiments/popups-in-popups.tsx +++ b/docs/src/app/(private)/experiments/popups-in-popups.tsx @@ -66,24 +66,22 @@ function SelectDemo({ modal }: Props) { - - }> - - - } /> - System font - - - } /> - Arial - - - } /> - Roboto - - - - + }> + + + } /> + System font + + + } /> + Arial + + + } /> + Roboto + + + ); } @@ -98,107 +96,105 @@ function MenuDemo({ modal }: Props) { return ( }>Format - - } - > - - - Text color - } - > - - - Black - - - Dark grey - - - Accent - - - - - - - Style - } - > - - - Heading - } - > - - - Level 1 - - - Level 2 - - - Level 3 - - - - - - Paragraph - - - List - } - > - - - Ordered - - - Unordered - - - - - - - - - - Clear formatting - - - - + } + > + + + Text color + } + > + + + Black + + + Dark grey + + + Accent + + + + + + + Style + } + > + + + Heading + } + > + + + Level 1 + + + Level 2 + + + Level 3 + + + + + + Paragraph + + + List + } + > + + + Ordered + + + Unordered + + + + + + + + + + Clear formatting + + + ); } diff --git a/docs/src/app/(private)/experiments/rtl.tsx b/docs/src/app/(private)/experiments/rtl.tsx index e89c421e04..91f9c1c41e 100644 --- a/docs/src/app/(private)/experiments/rtl.tsx +++ b/docs/src/app/(private)/experiments/rtl.tsx @@ -20,181 +20,205 @@ export default function RtlNestedMenu() { Menu.Trigger - - - - - Text color - - - - - Black - - - Dark grey - - + + + + + Text color + + + - Accent - - - - + + + Black + + + Dark grey + + + Accent + + + + + - - - Style - - - - - - Heading - - - - - Level 1 - - - Level 2 - - - Level 3 - - - - - + + Style + + + - Paragraph - - - - List - - - - - Ordered - - - Unordered - - - - - - - + + + + Heading + + + + + + Level 1 + + + Level 2 + + + Level 3 + + + + + + + Paragraph + + + + List + + + + + + Ordered + + + Unordered + + + + + + + + + - - Clear formatting - - - + + Clear formatting + + + + PreviewCard.Trigger - - - Base UI Logo -

Base UI

-

- Unstyled React components and hooks (@base-ui-components/react), by - @MUI_hq. -

-
- - 1 Following - - - 1,000 Followers - -
- -
-
+ + + + Base UI Logo +

Base UI

+

+ Unstyled React components and hooks (@base-ui-components/react), by + @MUI_hq. +

+
+ + 1 Following + + + 1,000 Followers + +
+ +
+
+
Popover.Trigger - - - Popover Title - Popover Description - - - + + + + Popover Title + Popover Description + + + +
diff --git a/docs/src/app/(private)/experiments/select-perf.tsx b/docs/src/app/(private)/experiments/select-perf.tsx index 09690d790c..72918ac55e 100644 --- a/docs/src/app/(private)/experiments/select-perf.tsx +++ b/docs/src/app/(private)/experiments/select-perf.tsx @@ -30,38 +30,40 @@ function BaseSelectExample() { > - - - - {items.map((item) => ( - - {item} - - ))} - - - + + + + + {items.map((item) => ( + + {item} + + ))} + + + + ); } diff --git a/docs/src/app/(private)/experiments/tooltip.tsx b/docs/src/app/(private)/experiments/tooltip.tsx index 7cd52e4a60..e205e70991 100644 --- a/docs/src/app/(private)/experiments/tooltip.tsx +++ b/docs/src/app/(private)/experiments/tooltip.tsx @@ -142,24 +142,30 @@ export default function TooltipTransitionExperiment() { Anchor - - Tooltip - + + + Tooltip + + Anchor - - Tooltip - + + + Tooltip + +

CSS Animation

Anchor - - Tooltip - + + + Tooltip + +

CSS Transition Group

@@ -167,24 +173,30 @@ export default function TooltipTransitionExperiment() { Anchor - - Tooltip - + + + Tooltip + + Anchor - - Tooltip - + + + Tooltip + +

CSS Transition

Anchor - - Tooltip - + + + Tooltip + +

CSS Transition with `@starting-style`

@@ -196,11 +208,13 @@ export default function TooltipTransitionExperiment() {

Anchor - - - Tooltip - - + + + + Tooltip + + +
@@ -212,28 +226,36 @@ export default function TooltipTransitionExperiment() { Anchor - - - Tooltip - - + + + + Tooltip + + + Anchor - - - Tooltip - - + + + + Tooltip + + +

CSS Animation

Anchor - - Tooltip - + + + + Tooltip + + +

CSS Transition Group

@@ -241,30 +263,36 @@ export default function TooltipTransitionExperiment() { Anchor - - - Tooltip - - + + + + Tooltip + + + Anchor - - - Tooltip - - + + + + Tooltip + + +

CSS Transition

Anchor - - - Tooltip - - + + + + Tooltip + + +
@@ -282,20 +310,22 @@ function FramerMotion() { Anchor {isOpen && ( - - - } - > - Tooltip - - + + + + } + > + Tooltip + + + )} diff --git a/docs/src/app/(public)/(content)/react/components/dialog/demos/hero/css-modules/index.tsx b/docs/src/app/(public)/(content)/react/components/dialog/demos/hero/css-modules/index.tsx index e58cd5fced..64253fda19 100644 --- a/docs/src/app/(public)/(content)/react/components/dialog/demos/hero/css-modules/index.tsx +++ b/docs/src/app/(public)/(content)/react/components/dialog/demos/hero/css-modules/index.tsx @@ -6,7 +6,7 @@ export default function ExampleDialog() { return ( View notifications - + Notifications diff --git a/packages/react/src/alert-dialog/backdrop/AlertDialogBackdrop.tsx b/packages/react/src/alert-dialog/backdrop/AlertDialogBackdrop.tsx index edd8d28559..c7c0b7a762 100644 --- a/packages/react/src/alert-dialog/backdrop/AlertDialogBackdrop.tsx +++ b/packages/react/src/alert-dialog/backdrop/AlertDialogBackdrop.tsx @@ -24,7 +24,7 @@ const AlertDialogBackdrop = React.forwardRef(function AlertDialogBackdrop( props: AlertDialogBackdrop.Props, forwardedRef: React.ForwardedRef, ) { - const { render, className, keepMounted = false, ...other } = props; + const { render, className, ...other } = props; const { open, nested, mounted, transitionStatus } = useAlertDialogRootContext(); const state: AlertDialogBackdrop.State = React.useMemo( @@ -45,7 +45,7 @@ const AlertDialogBackdrop = React.forwardRef(function AlertDialogBackdrop( }); // no need to render nested backdrops - const shouldRender = (keepMounted || mounted) && !nested; + const shouldRender = !nested; if (!shouldRender) { return null; } @@ -54,13 +54,7 @@ const AlertDialogBackdrop = React.forwardRef(function AlertDialogBackdrop( }); namespace AlertDialogBackdrop { - export interface Props extends BaseUIComponentProps<'div', State> { - /** - * Whether to keep the element in the DOM while the alert dialog is hidden. - * @default false - */ - keepMounted?: boolean; - } + export interface Props extends BaseUIComponentProps<'div', State> {} export interface State { /** @@ -85,11 +79,6 @@ AlertDialogBackdrop.propTypes /* remove-proptypes */ = { * returns a class based on the component’s state. */ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - /** - * Whether to keep the element in the DOM while the alert dialog is hidden. - * @default false - */ - keepMounted: PropTypes.bool, /** * Allows you to replace the component’s HTML element * with a different tag, or compose it with another component. diff --git a/packages/react/src/alert-dialog/close/AlertDialogClose.test.tsx b/packages/react/src/alert-dialog/close/AlertDialogClose.test.tsx index c9498fad98..1158b16ccc 100644 --- a/packages/react/src/alert-dialog/close/AlertDialogClose.test.tsx +++ b/packages/react/src/alert-dialog/close/AlertDialogClose.test.tsx @@ -11,7 +11,9 @@ describe('', () => { return render( - {node} + + {node} + , ); }, diff --git a/packages/react/src/alert-dialog/description/AlertDialogDescription.test.tsx b/packages/react/src/alert-dialog/description/AlertDialogDescription.test.tsx index be892fdfce..5b40b3718a 100644 --- a/packages/react/src/alert-dialog/description/AlertDialogDescription.test.tsx +++ b/packages/react/src/alert-dialog/description/AlertDialogDescription.test.tsx @@ -11,7 +11,9 @@ describe('', () => { return render( - {node} + + {node} + , ); }, diff --git a/packages/react/src/alert-dialog/index.parts.ts b/packages/react/src/alert-dialog/index.parts.ts index 87eb08a1e1..5eb6454af9 100644 --- a/packages/react/src/alert-dialog/index.parts.ts +++ b/packages/react/src/alert-dialog/index.parts.ts @@ -2,7 +2,7 @@ export { AlertDialogBackdrop as Backdrop } from './backdrop/AlertDialogBackdrop' export { AlertDialogClose as Close } from './close/AlertDialogClose'; export { AlertDialogDescription as Description } from './description/AlertDialogDescription'; export { AlertDialogPopup as Popup } from './popup/AlertDialogPopup'; -export { Portal } from '../portal/Portal'; +export { AlertDialogPortal as Portal } from './portal/AlertDialogPortal'; export { AlertDialogRoot as Root } from './root/AlertDialogRoot'; export { AlertDialogTitle as Title } from './title/AlertDialogTitle'; export { AlertDialogTrigger as Trigger } from './trigger/AlertDialogTrigger'; diff --git a/packages/react/src/alert-dialog/popup/AlertDialogPopup.test.tsx b/packages/react/src/alert-dialog/popup/AlertDialogPopup.test.tsx index 241adfc903..42aa1f6acf 100644 --- a/packages/react/src/alert-dialog/popup/AlertDialogPopup.test.tsx +++ b/packages/react/src/alert-dialog/popup/AlertDialogPopup.test.tsx @@ -13,8 +13,10 @@ describe('', () => { render: (node) => { return render( - - {node} + + + {node} + , ); }, @@ -24,7 +26,9 @@ describe('', () => { const { getByTestId } = await render( - + + + , ); @@ -40,10 +44,12 @@ describe('', () => { Open - - - - + + + + + + , @@ -69,12 +75,14 @@ describe('', () => { Open - - - - - - + + + + + + + + @@ -106,12 +114,14 @@ describe('', () => { Open - - - - - - + + + + + + + + @@ -140,9 +150,11 @@ describe('', () => { Open - - Close - + + + Close + + , @@ -168,9 +180,11 @@ describe('', () => { Open - - Close - + + + Close + + diff --git a/packages/react/src/alert-dialog/popup/AlertDialogPopup.tsx b/packages/react/src/alert-dialog/popup/AlertDialogPopup.tsx index 785a99da62..989652fc9b 100644 --- a/packages/react/src/alert-dialog/popup/AlertDialogPopup.tsx +++ b/packages/react/src/alert-dialog/popup/AlertDialogPopup.tsx @@ -15,6 +15,7 @@ import { InteractionType } from '../../utils/useEnhancedClickHandler'; import { transitionStatusMapping } from '../../utils/styleHookMapping'; import { AlertDialogPopupDataAttributes } from './AlertDialogPopupDataAttributes'; import { InternalBackdrop } from '../../utils/InternalBackdrop'; +import { useAlertDialogPortalContext } from '../portal/AlertDialogPortalContext'; const customStyleHookMapping: CustomStyleHookMapping = { ...baseMapping, @@ -34,7 +35,7 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup( props: AlertDialogPopup.Props, forwardedRef: React.ForwardedRef, ) { - const { className, id, keepMounted = false, render, initialFocus, finalFocus, ...other } = props; + const { className, id, render, initialFocus, finalFocus, ...other } = props; const { descriptionElementId, @@ -54,6 +55,8 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup( modal, } = useAlertDialogRootContext(); + useAlertDialogPortalContext(); + const mergedRef = useForkRef(forwardedRef, popupRef); const { getRootProps, floatingContext, resolvedInitialFocus } = useDialogPopup({ @@ -98,10 +101,6 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup( customStyleHookMapping, }); - if (!keepMounted && !mounted) { - return null; - } - return ( {mounted && modal && } @@ -121,11 +120,6 @@ const AlertDialogPopup = React.forwardRef(function AlertDialogPopup( namespace AlertDialogPopup { export interface Props extends BaseUIComponentProps<'div', State> { - /** - * Whether to keep the element in the DOM while the alert dialog is hidden. - * @default false - */ - keepMounted?: boolean; /** * Determines the element to focus when the dialog is opened. * By default, the first focusable element is focused. @@ -188,11 +182,6 @@ AlertDialogPopup.propTypes /* remove-proptypes */ = { PropTypes.func, refType, ]), - /** - * Whether to keep the element in the DOM while the alert dialog is hidden. - * @default false - */ - keepMounted: PropTypes.bool, /** * Allows you to replace the component’s HTML element * with a different tag, or compose it with another component. diff --git a/packages/react/src/alert-dialog/portal/AlertDialogPortal.tsx b/packages/react/src/alert-dialog/portal/AlertDialogPortal.tsx new file mode 100644 index 0000000000..a9443cde41 --- /dev/null +++ b/packages/react/src/alert-dialog/portal/AlertDialogPortal.tsx @@ -0,0 +1,67 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { FloatingPortal } from '@floating-ui/react'; +import { useAlertDialogRootContext } from '../root/AlertDialogRootContext'; +import { AlertDialogPortalContext } from './AlertDialogPortalContext'; +import { HTMLElementType, refType } from '../../utils/proptypes'; + +/** + * A portal element that moves the popup to a different part of the DOM. + * By default, the portal element is appended to ``. + * + * Documentation: [Base UI Alert Dialog](https://base-ui.com/react/components/alert-dialog) + */ +function AlertDialogPortal(props: AlertDialogPortal.Props) { + const { children, keepMounted = false, container } = props; + + const { mounted } = useAlertDialogRootContext(); + + const shouldRender = mounted || keepMounted; + if (!shouldRender) { + return null; + } + + return ( + + {children} + + ); +} + +namespace AlertDialogPortal { + export interface Props { + children?: React.ReactNode; + /** + * 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; + } +} + +AlertDialogPortal.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * A parent element to render the portal element into. + */ + container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, refType]), + /** + * Whether to keep the portal mounted in the DOM while the popup is hidden. + * @default false + */ + keepMounted: PropTypes.bool, +} as any; + +export { AlertDialogPortal }; diff --git a/packages/react/src/alert-dialog/portal/AlertDialogPortalContext.ts b/packages/react/src/alert-dialog/portal/AlertDialogPortalContext.ts new file mode 100644 index 0000000000..e9169df78e --- /dev/null +++ b/packages/react/src/alert-dialog/portal/AlertDialogPortalContext.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export const AlertDialogPortalContext = React.createContext(undefined); + +export function useAlertDialogPortalContext() { + const value = React.useContext(AlertDialogPortalContext); + if (value === undefined) { + throw new Error('Base UI: is missing.'); + } + return value; +} diff --git a/packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx b/packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx index 38c643668a..d7e4994535 100644 --- a/packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx +++ b/packages/react/src/alert-dialog/root/AlertDialogRoot.test.tsx @@ -17,9 +17,11 @@ describe('', () => { const { user } = await render( Open - - Close - + + + Close + + , ); @@ -44,9 +46,11 @@ describe('', () => { const { user } = await render( Open - - Close - + + + Close + + , ); @@ -69,9 +73,11 @@ describe('', () => { const { user } = await render( Open - - Close - + + + Close + + , ); @@ -91,9 +97,11 @@ describe('', () => { Open Dialog - - Close Dialog - + + + Close Dialog + + diff --git a/packages/react/src/alert-dialog/root/AlertDialogRoot.tsx b/packages/react/src/alert-dialog/root/AlertDialogRoot.tsx index 443757bc47..c4e0093131 100644 --- a/packages/react/src/alert-dialog/root/AlertDialogRoot.tsx +++ b/packages/react/src/alert-dialog/root/AlertDialogRoot.tsx @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import type { DialogRoot } from '../../dialog/root/DialogRoot'; import { AlertDialogRootContext } from './AlertDialogRootContext'; import { useDialogRoot } from '../../dialog/root/useDialogRoot'; -import { PortalContext } from '../../portal/PortalContext'; /** * Groups all parts of the alert dialog. @@ -36,7 +35,7 @@ const AlertDialogRoot: React.FC = function AlertDialogRoo return ( - {children} + {children} ); }; diff --git a/packages/react/src/alert-dialog/title/AlertDialogTitle.test.tsx b/packages/react/src/alert-dialog/title/AlertDialogTitle.test.tsx index b296255c70..538d75c32c 100644 --- a/packages/react/src/alert-dialog/title/AlertDialogTitle.test.tsx +++ b/packages/react/src/alert-dialog/title/AlertDialogTitle.test.tsx @@ -11,7 +11,9 @@ describe('', () => { return render( - {node} + + {node} + , ); }, diff --git a/packages/react/src/dialog/backdrop/DialogBackdrop.tsx b/packages/react/src/dialog/backdrop/DialogBackdrop.tsx index b2dae7eba6..f5961a54cd 100644 --- a/packages/react/src/dialog/backdrop/DialogBackdrop.tsx +++ b/packages/react/src/dialog/backdrop/DialogBackdrop.tsx @@ -24,7 +24,7 @@ const DialogBackdrop = React.forwardRef(function DialogBackdrop( props: DialogBackdrop.Props, forwardedRef: React.ForwardedRef, ) { - const { render, className, keepMounted = false, ...other } = props; + const { render, className, ...other } = props; const { open, nested, mounted, transitionStatus } = useDialogRootContext(); const state: DialogBackdrop.State = React.useMemo( @@ -45,7 +45,7 @@ const DialogBackdrop = React.forwardRef(function DialogBackdrop( }); // no need to render nested backdrops - const shouldRender = (keepMounted || mounted) && !nested; + const shouldRender = mounted && !nested; if (!shouldRender) { return null; } @@ -54,13 +54,7 @@ const DialogBackdrop = React.forwardRef(function DialogBackdrop( }); namespace DialogBackdrop { - export interface Props extends BaseUIComponentProps<'div', State> { - /** - * Whether to keep the HTML element in the DOM while the dialog is hidden. - * @default false - */ - keepMounted?: boolean; - } + export interface Props extends BaseUIComponentProps<'div', State> {} export interface State { /** @@ -85,11 +79,6 @@ DialogBackdrop.propTypes /* remove-proptypes */ = { * returns a class based on the component’s state. */ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - /** - * Whether to keep the HTML element in the DOM while the dialog is hidden. - * @default false - */ - keepMounted: PropTypes.bool, /** * Allows you to replace the component’s HTML element * with a different tag, or compose it with another component. diff --git a/packages/react/src/dialog/close/DialogClose.test.tsx b/packages/react/src/dialog/close/DialogClose.test.tsx index fb0cd7412a..f9a05b20c0 100644 --- a/packages/react/src/dialog/close/DialogClose.test.tsx +++ b/packages/react/src/dialog/close/DialogClose.test.tsx @@ -10,7 +10,9 @@ describe('', () => { render: (node) => { return render( - {node} + + {node} + , ); }, diff --git a/packages/react/src/dialog/description/DialogDescription.test.tsx b/packages/react/src/dialog/description/DialogDescription.test.tsx index 546794e6b1..2eac5f6b93 100644 --- a/packages/react/src/dialog/description/DialogDescription.test.tsx +++ b/packages/react/src/dialog/description/DialogDescription.test.tsx @@ -10,7 +10,9 @@ describe('', () => { render: (node) => { return render( - {node} + + {node} + , ); }, diff --git a/packages/react/src/dialog/index.parts.ts b/packages/react/src/dialog/index.parts.ts index d6df8f438f..c6b2a98d6f 100644 --- a/packages/react/src/dialog/index.parts.ts +++ b/packages/react/src/dialog/index.parts.ts @@ -2,7 +2,7 @@ export { DialogBackdrop as Backdrop } from './backdrop/DialogBackdrop'; export { DialogClose as Close } from './close/DialogClose'; export { DialogDescription as Description } from './description/DialogDescription'; export { DialogPopup as Popup } from './popup/DialogPopup'; -export { Portal } from '../portal/Portal'; +export { DialogPortal as Portal } from './portal/DialogPortal'; export { DialogRoot as Root } from './root/DialogRoot'; export { DialogTitle as Title } from './title/DialogTitle'; export { DialogTrigger as Trigger } from './trigger/DialogTrigger'; diff --git a/packages/react/src/dialog/popup/DialogPopup.test.tsx b/packages/react/src/dialog/popup/DialogPopup.test.tsx index 4c6ae9a404..8378715c91 100644 --- a/packages/react/src/dialog/popup/DialogPopup.test.tsx +++ b/packages/react/src/dialog/popup/DialogPopup.test.tsx @@ -13,7 +13,7 @@ describe('', () => { render: (node) => { return render( - {node} + {node} , ); }, @@ -28,7 +28,9 @@ describe('', () => { it(`should ${!expectedIsMounted ? 'not ' : ''}keep the dialog mounted when keepMounted=${keepMounted}`, async () => { const { queryByRole } = await render( - + + + , ); @@ -50,10 +52,12 @@ describe('', () => { Open - - - - + + + + + + , @@ -78,12 +82,14 @@ describe('', () => { Open - - - - - - + + + + + + + + @@ -114,12 +120,14 @@ describe('', () => { Open - - - - - - + + + + + + + + @@ -148,9 +156,11 @@ describe('', () => { Open - - Close - + + + Close + + , @@ -176,9 +186,11 @@ describe('', () => { Open - - Close - + + + Close + + diff --git a/packages/react/src/dialog/popup/DialogPopup.tsx b/packages/react/src/dialog/popup/DialogPopup.tsx index be21ee3681..2d80874ffa 100644 --- a/packages/react/src/dialog/popup/DialogPopup.tsx +++ b/packages/react/src/dialog/popup/DialogPopup.tsx @@ -16,6 +16,7 @@ import { transitionStatusMapping } from '../../utils/styleHookMapping'; import { DialogPopupCssVars } from './DialogPopupCssVars'; import { DialogPopupDataAttributes } from './DialogPopupDataAttributes'; import { InternalBackdrop } from '../../utils/InternalBackdrop'; +import { useDialogPortalContext } from '../portal/DialogPortalContext'; const customStyleHookMapping: CustomStyleHookMapping = { ...baseMapping, @@ -35,7 +36,7 @@ const DialogPopup = React.forwardRef(function DialogPopup( props: DialogPopup.Props, forwardedRef: React.ForwardedRef, ) { - const { className, finalFocus, id, initialFocus, keepMounted = false, render, ...other } = props; + const { className, finalFocus, id, initialFocus, render, ...other } = props; const { descriptionElementId, @@ -56,6 +57,8 @@ const DialogPopup = React.forwardRef(function DialogPopup( transitionStatus, } = useDialogRootContext(); + useDialogPortalContext(); + const mergedRef = useForkRef(forwardedRef, popupRef); const { getRootProps, floatingContext, resolvedInitialFocus } = useDialogPopup({ @@ -94,10 +97,6 @@ const DialogPopup = React.forwardRef(function DialogPopup( customStyleHookMapping, }); - if (!keepMounted && !mounted) { - return null; - } - return ( {mounted && modal && } @@ -118,11 +117,6 @@ const DialogPopup = React.forwardRef(function DialogPopup( namespace DialogPopup { export interface Props extends BaseUIComponentProps<'div', State> { - /** - * Whether to keep the HTML element in the DOM while the dialog is hidden. - * @default false - */ - keepMounted?: boolean; /** * Determines the element to focus when the dialog is opened. * By default, the first focusable element is focused. @@ -185,11 +179,6 @@ DialogPopup.propTypes /* remove-proptypes */ = { PropTypes.func, refType, ]), - /** - * Whether to keep the HTML element in the DOM while the dialog is hidden. - * @default false - */ - keepMounted: PropTypes.bool, /** * Allows you to replace the component’s HTML element * with a different tag, or compose it with another component. diff --git a/packages/react/src/dialog/portal/DialogPortal.tsx b/packages/react/src/dialog/portal/DialogPortal.tsx new file mode 100644 index 0000000000..1f0a85ab52 --- /dev/null +++ b/packages/react/src/dialog/portal/DialogPortal.tsx @@ -0,0 +1,67 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { FloatingPortal } from '@floating-ui/react'; +import { useDialogRootContext } from '../root/DialogRootContext'; +import { DialogPortalContext } from './DialogPortalContext'; +import { HTMLElementType, refType } from '../../utils/proptypes'; + +/** + * A portal element that moves the popup to a different part of the DOM. + * By default, the portal element is appended to ``. + * + * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog) + */ +function DialogPortal(props: DialogPortal.Props) { + const { children, keepMounted = false, container } = props; + + const { mounted } = useDialogRootContext(); + + const shouldRender = mounted || keepMounted; + if (!shouldRender) { + return null; + } + + return ( + + {children} + + ); +} + +namespace DialogPortal { + export interface Props { + children?: React.ReactNode; + /** + * 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; + } +} + +DialogPortal.propTypes /* remove-proptypes */ = { + // ┌────────────────────────────── Warning ──────────────────────────────┐ + // │ These PropTypes are generated from the TypeScript type definitions. │ + // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ + // └─────────────────────────────────────────────────────────────────────┘ + /** + * @ignore + */ + children: PropTypes.node, + /** + * A parent element to render the portal element into. + */ + container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, refType]), + /** + * Whether to keep the portal mounted in the DOM while the popup is hidden. + * @default false + */ + keepMounted: PropTypes.bool, +} as any; + +export { DialogPortal }; diff --git a/packages/react/src/dialog/portal/DialogPortalContext.ts b/packages/react/src/dialog/portal/DialogPortalContext.ts new file mode 100644 index 0000000000..60a523f117 --- /dev/null +++ b/packages/react/src/dialog/portal/DialogPortalContext.ts @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export const DialogPortalContext = React.createContext(undefined); + +export function useDialogPortalContext() { + const value = React.useContext(DialogPortalContext); + if (value === undefined) { + throw new Error('Base UI: is missing.'); + } + return value; +} diff --git a/packages/react/src/dialog/root/DialogRoot.test.tsx b/packages/react/src/dialog/root/DialogRoot.test.tsx index 6faa878510..b9a4f2b9f1 100644 --- a/packages/react/src/dialog/root/DialogRoot.test.tsx +++ b/packages/react/src/dialog/root/DialogRoot.test.tsx @@ -21,7 +21,9 @@ describe('', () => { const { queryByRole, getByRole } = await render( - + + + , ); @@ -40,7 +42,9 @@ describe('', () => { it('should open and close the dialog with the `open` prop', async () => { const { queryByRole, setProps } = await render( - + + + , ); @@ -67,7 +71,9 @@ describe('', () => {
- + + +
); @@ -122,12 +128,13 @@ describe('', () => {