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
-
- Unstyled React components and hooks (@base-ui-components/react), by
- @MUI_hq.
-
-
-
- 1 Following
-
-
- 1,000 Followers
-
-
-
-
-
+
+
+
+
+ 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
-
-
- Close
-
+
+
+
+ Close
+
+
,
@@ -69,12 +75,14 @@ describe(' ', () => {
Open
-
-
-
-
- Close
-
+
+
+
+
+
+ Close
+
+
@@ -106,12 +114,14 @@ describe(' ', () => {
Open
-
-
-
-
- Close
-
+
+
+
+
+
+ Close
+
+
@@ -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
+
+
Another Button
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
-
-
- Close
-
+
+
+
+ Close
+
+
,
@@ -78,12 +82,14 @@ describe(' ', () => {
Open
-
-
-
-
- Close
-
+
+
+
+
+
+ Close
+
+
@@ -114,12 +120,14 @@ describe(' ', () => {
Open
-
-
-
-
- Close
-
+
+
+
+
+
+ Close
+
+
@@ -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(' ', () => {
setOpen(false)}>Close
-
+
+
+
);
@@ -122,12 +128,13 @@ describe(' ', () => {
setOpen(false)}>Close
-
+
+
+
);
@@ -153,9 +160,11 @@ describe(' ', () => {
const { user } = await render(
Open
-
- Close
-
+
+
+ Close
+
+
,
);
@@ -180,9 +189,11 @@ describe(' ', () => {
const { user } = await render(
Open
-
- Close
-
+
+
+ Close
+
+
,
);
@@ -205,9 +216,11 @@ describe(' ', () => {
const { user } = await render(
Open
-
- Close
-
+
+
+ Close
+
+
,
);
@@ -223,9 +236,11 @@ describe(' ', () => {
const { user } = await render(
Open
-
- Close
-
+
+
+ Close
+
+
,
);
@@ -245,9 +260,11 @@ describe(' ', () => {
Open Dialog
-
- Close Dialog
-
+
+
+ Close Dialog
+
+
Another Button
@@ -288,9 +305,11 @@ describe(' ', () => {
Open Dialog
-
- Close Dialog
-
+
+
+ Close Dialog
+
+
Another Button
@@ -333,7 +352,9 @@ describe(' ', () => {
dismissible={dismissible}
modal={false}
>
-
+
+
+
,
);
@@ -377,7 +398,9 @@ describe(' ', () => {
{/* eslint-disable-next-line react/no-danger */}
-
+
+
+
,
);
diff --git a/packages/react/src/dialog/root/DialogRoot.tsx b/packages/react/src/dialog/root/DialogRoot.tsx
index 236a9d091e..faa1303c22 100644
--- a/packages/react/src/dialog/root/DialogRoot.tsx
+++ b/packages/react/src/dialog/root/DialogRoot.tsx
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { DialogRootContext, useOptionalDialogRootContext } from './DialogRootContext';
import { DialogContext } from '../utils/DialogContext';
import { type CommonParameters, useDialogRoot } from './useDialogRoot';
-import { PortalContext } from '../../portal/PortalContext';
/**
* Groups all parts of the dialog.
@@ -37,13 +36,12 @@ const DialogRoot = function DialogRoot(props: DialogRoot.Props) {
const nested = Boolean(parentDialogRootContext);
const dialogContextValue = React.useMemo(() => ({ ...dialogRoot, nested }), [dialogRoot, nested]);
-
const dialogRootContextValue = React.useMemo(() => ({ dismissible }), [dismissible]);
return (
- {children}
+ {children}
);
diff --git a/packages/react/src/dialog/title/DialogTitle.test.tsx b/packages/react/src/dialog/title/DialogTitle.test.tsx
index 4a5ab7c8a8..6112802d63 100644
--- a/packages/react/src/dialog/title/DialogTitle.test.tsx
+++ b/packages/react/src/dialog/title/DialogTitle.test.tsx
@@ -10,7 +10,9 @@ describe(' ', () => {
render: (node) => {
return render(
- {node}
+
+ {node}
+
,
);
},
diff --git a/packages/react/src/field/root/FieldRoot.test.tsx b/packages/react/src/field/root/FieldRoot.test.tsx
index 56e34ef281..56c6205bdf 100644
--- a/packages/react/src/field/root/FieldRoot.test.tsx
+++ b/packages/react/src/field/root/FieldRoot.test.tsx
@@ -484,12 +484,14 @@ describe(' ', () => {
-
-
- Select
- Option 1
-
-
+
+
+
+ Select
+ Option 1
+
+
+
,
);
@@ -683,12 +685,14 @@ describe(' ', () => {
-
-
- Select
- Option 1
-
-
+
+
+
+ Select
+ Option 1
+
+
+
,
);
diff --git a/packages/react/src/menu/arrow/MenuArrow.test.tsx b/packages/react/src/menu/arrow/MenuArrow.test.tsx
index 852f633909..fe60ccc0de 100644
--- a/packages/react/src/menu/arrow/MenuArrow.test.tsx
+++ b/packages/react/src/menu/arrow/MenuArrow.test.tsx
@@ -10,9 +10,11 @@ describe(' ', () => {
render(node) {
return render(
-
- {node}
-
+
+
+ {node}
+
+
,
);
},
diff --git a/packages/react/src/menu/backdrop/MenuBackdrop.tsx b/packages/react/src/menu/backdrop/MenuBackdrop.tsx
index 7955ac1df8..0f6671d963 100644
--- a/packages/react/src/menu/backdrop/MenuBackdrop.tsx
+++ b/packages/react/src/menu/backdrop/MenuBackdrop.tsx
@@ -24,7 +24,7 @@ const MenuBackdrop = React.forwardRef(function MenuBackdrop(
props: MenuBackdrop.Props,
forwardedRef: React.ForwardedRef,
) {
- const { className, render, keepMounted = false, ...other } = props;
+ const { className, render, ...other } = props;
const { open, mounted, transitionStatus } = useMenuRootContext();
const state: MenuBackdrop.State = React.useMemo(
@@ -44,11 +44,6 @@ const MenuBackdrop = React.forwardRef(function MenuBackdrop(
customStyleHookMapping,
});
- const shouldRender = keepMounted || mounted;
- if (!shouldRender) {
- return null;
- }
-
return renderElement();
});
@@ -61,13 +56,7 @@ namespace MenuBackdrop {
transitionStatus: TransitionStatus;
}
- export interface Props extends BaseUIComponentProps<'div', State> {
- /**
- * Whether to keep the HTML element in the DOM while the menu is hidden.
- * @default false
- */
- keepMounted?: boolean;
- }
+ export interface Props extends BaseUIComponentProps<'div', State> {}
}
MenuBackdrop.propTypes /* remove-proptypes */ = {
@@ -84,11 +73,6 @@ MenuBackdrop.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 menu 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/menu/checkbox-item-indicator/MenuCheckboxItemIndicator.test.tsx b/packages/react/src/menu/checkbox-item-indicator/MenuCheckboxItemIndicator.test.tsx
index 3cf0825e44..9fdcb58043 100644
--- a/packages/react/src/menu/checkbox-item-indicator/MenuCheckboxItemIndicator.test.tsx
+++ b/packages/react/src/menu/checkbox-item-indicator/MenuCheckboxItemIndicator.test.tsx
@@ -16,11 +16,13 @@ describe(' ', () => {
render(node) {
return render(
-
-
- {node}
-
-
+
+
+
+ {node}
+
+
+
,
);
},
@@ -39,13 +41,15 @@ describe(' ', () => {
setChecked(false)}>Close
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
@@ -98,18 +102,20 @@ describe(' ', () => {
setChecked(false)}>Close
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
diff --git a/packages/react/src/menu/checkbox-item/MenuCheckboxItem.test.tsx b/packages/react/src/menu/checkbox-item/MenuCheckboxItem.test.tsx
index d39bd9d53d..28c2cc79ac 100644
--- a/packages/react/src/menu/checkbox-item/MenuCheckboxItem.test.tsx
+++ b/packages/react/src/menu/checkbox-item/MenuCheckboxItem.test.tsx
@@ -74,22 +74,24 @@ describe(' ', () => {
const { getAllByRole } = await render(
-
-
- } id="item-1">
- 1
-
- } id="item-2">
- 2
-
- } id="item-3">
- 3
-
- } id="item-4">
- 4
-
-
-
+
+
+
+ } id="item-1">
+ 1
+
+ } id="item-2">
+ 2
+
+ } id="item-3">
+ 3
+
+ } id="item-4">
+ 4
+
+
+
+
,
);
@@ -140,11 +142,13 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -161,11 +165,13 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -188,11 +194,13 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -224,11 +232,13 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -253,11 +263,13 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -280,11 +292,13 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -309,11 +323,13 @@ describe(' ', () => {
const { getByRole, queryByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -330,11 +346,13 @@ describe(' ', () => {
const { getByRole, queryByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
diff --git a/packages/react/src/menu/group-label/MenuGroupLabel.test.tsx b/packages/react/src/menu/group-label/MenuGroupLabel.test.tsx
index ea3fafd8cf..35ebfb8afd 100644
--- a/packages/react/src/menu/group-label/MenuGroupLabel.test.tsx
+++ b/packages/react/src/menu/group-label/MenuGroupLabel.test.tsx
@@ -24,13 +24,15 @@ describe(' ', () => {
it('should have the role `presentation`', async () => {
const { getByText } = await render(
-
-
-
- Test group
-
-
-
+
+
+
+
+ Test group
+
+
+
+
,
);
@@ -41,13 +43,15 @@ describe(' ', () => {
it("should reference the generated id in Group's `aria-labelledby`", async () => {
const { getByText, getByRole } = await render(
-
-
-
- Test group
-
-
-
+
+
+
+
+ Test group
+
+
+
+
,
);
@@ -60,13 +64,15 @@ describe(' ', () => {
it("should reference the provided id in Group's `aria-labelledby`", async () => {
const { getByRole } = await render(
-
-
-
- Test group
-
-
-
+
+
+
+
+ Test group
+
+
+
+
,
);
diff --git a/packages/react/src/menu/index.parts.ts b/packages/react/src/menu/index.parts.ts
index 80cf69cc33..7976b4de94 100644
--- a/packages/react/src/menu/index.parts.ts
+++ b/packages/react/src/menu/index.parts.ts
@@ -6,7 +6,7 @@ export { MenuGroup as Group } from './group/MenuGroup';
export { MenuGroupLabel as GroupLabel } from './group-label/MenuGroupLabel';
export { MenuItem as Item } from './item/MenuItem';
export { MenuPopup as Popup } from './popup/MenuPopup';
-export { Portal } from '../portal/Portal';
+export { MenuPortal as Portal } from './portal/MenuPortal';
export { MenuPositioner as Positioner } from './positioner/MenuPositioner';
export { MenuRadioGroup as RadioGroup } from './radio-group/MenuRadioGroup';
export { MenuRadioItem as RadioItem } from './radio-item/MenuRadioItem';
diff --git a/packages/react/src/menu/item/MenuItem.test.tsx b/packages/react/src/menu/item/MenuItem.test.tsx
index d9fecfcee1..a052740abc 100644
--- a/packages/react/src/menu/item/MenuItem.test.tsx
+++ b/packages/react/src/menu/item/MenuItem.test.tsx
@@ -55,13 +55,15 @@ describe(' ', () => {
const onClick = spy();
const { user } = await render(
-
-
-
- Item
-
-
-
+
+
+
+
+ Item
+
+
+
+
,
);
@@ -94,22 +96,24 @@ describe(' ', () => {
const { getAllByRole, user } = await render(
-
-
- } id="item-1">
- 1
-
- } id="item-2">
- 2
-
- } id="item-3">
- 3
-
- } id="item-4">
- 4
-
-
-
+
+
+
+ } id="item-1">
+ 1
+
+ } id="item-2">
+ 2
+
+ } id="item-3">
+ 3
+
+ } id="item-4">
+ 4
+
+
+
+
,
);
@@ -153,11 +157,13 @@ describe(' ', () => {
const { getByRole, queryByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -174,11 +180,13 @@ describe(' ', () => {
const { getByRole, queryByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
diff --git a/packages/react/src/menu/popup/MenuPopup.test.tsx b/packages/react/src/menu/popup/MenuPopup.test.tsx
index c566ea1b3e..ea0ee36c3e 100644
--- a/packages/react/src/menu/popup/MenuPopup.test.tsx
+++ b/packages/react/src/menu/popup/MenuPopup.test.tsx
@@ -9,7 +9,9 @@ describe(' ', () => {
render: (node) => {
return render(
- {node}
+
+ {node}
+
,
);
},
diff --git a/packages/react/src/portal/Portal.tsx b/packages/react/src/menu/portal/MenuPortal.tsx
similarity index 66%
rename from packages/react/src/portal/Portal.tsx
rename to packages/react/src/menu/portal/MenuPortal.tsx
index 73cf76d330..0ebb386d17 100644
--- a/packages/react/src/portal/Portal.tsx
+++ b/packages/react/src/menu/portal/MenuPortal.tsx
@@ -2,46 +2,49 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { FloatingPortal } from '@floating-ui/react';
-import { usePortalContext } from './PortalContext';
-import { HTMLElementType, refType } from '../utils/proptypes';
+import { useMenuRootContext } from '../root/MenuRootContext';
+import { MenuPortalContext } from './MenuPortalContext';
+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: https://base-ui.com
+ * Documentation: [Base UI Menu](https://base-ui.com/react/components/menu)
*/
-function Portal(props: Portal.Props) {
- const { children, container, keepMounted = false } = props;
+function MenuPortal(props: MenuPortal.Props) {
+ const { children, keepMounted = false, container } = props;
- const mounted = usePortalContext();
+ const { mounted } = useMenuRootContext();
const shouldRender = mounted || keepMounted;
if (!shouldRender) {
return null;
}
- return {children} ;
+ return (
+
+ {children}
+
+ );
}
-namespace Portal {
+namespace MenuPortal {
export interface Props {
children?: React.ReactNode;
- /**
- * A parent element to render the portal into.
- */
- container?: HTMLElement | null | React.RefObject;
/**
* 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 interface State {}
}
-Portal.propTypes /* remove-proptypes */ = {
+MenuPortal.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
@@ -51,7 +54,7 @@ Portal.propTypes /* remove-proptypes */ = {
*/
children: PropTypes.node,
/**
- * A parent element to render the portal into.
+ * A parent element to render the portal element into.
*/
container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, refType]),
/**
@@ -61,4 +64,4 @@ Portal.propTypes /* remove-proptypes */ = {
keepMounted: PropTypes.bool,
} as any;
-export { Portal };
+export { MenuPortal };
diff --git a/packages/react/src/menu/portal/MenuPortalContext.ts b/packages/react/src/menu/portal/MenuPortalContext.ts
new file mode 100644
index 0000000000..fd5ed78517
--- /dev/null
+++ b/packages/react/src/menu/portal/MenuPortalContext.ts
@@ -0,0 +1,11 @@
+import * as React from 'react';
+
+export const MenuPortalContext = React.createContext(undefined);
+
+export function useMenuPortalContext() {
+ const value = React.useContext(MenuPortalContext);
+ if (value === undefined) {
+ throw new Error('Base UI: is missing.');
+ }
+ return value;
+}
diff --git a/packages/react/src/menu/positioner/MenuPositioner.test.tsx b/packages/react/src/menu/positioner/MenuPositioner.test.tsx
index 934fb73d2f..73a97bd4fd 100644
--- a/packages/react/src/menu/positioner/MenuPositioner.test.tsx
+++ b/packages/react/src/menu/positioner/MenuPositioner.test.tsx
@@ -38,7 +38,9 @@ describe(' ', () => {
render: (node) => {
return render(
- {node}
+
+ {node}
+
,
);
},
@@ -59,18 +61,20 @@ describe(' ', () => {
return (
-
-
- 1
- 2
-
-
+
+
+
+ 1
+ 2
+
+
+
@@ -107,18 +111,20 @@ describe(' ', () => {
return (
-
-
- 1
- 2
-
-
+
+
+
+ 1
+ 2
+
+
+
@@ -157,18 +163,20 @@ describe(' ', () => {
return (
-
-
- 1
- 2
-
-
+
+
+
+ 1
+ 2
+
+
+
@@ -212,18 +220,20 @@ describe(' ', () => {
const { getByTestId } = await render(
-
-
- 1
- 2
-
-
+
+
+
+ 1
+ 2
+
+
+
,
);
@@ -239,12 +249,14 @@ describe(' ', () => {
const { getByRole, queryByRole } = await render(
Toggle
-
-
- 1
- 2
-
-
+
+
+
+ 1
+ 2
+
+
+
,
);
@@ -267,12 +279,14 @@ describe(' ', () => {
const { getByRole, queryByRole } = await render(
Toggle
-
-
- 1
- 2
-
-
+
+
+
+ 1
+ 2
+
+
+
,
);
diff --git a/packages/react/src/menu/positioner/MenuPositioner.tsx b/packages/react/src/menu/positioner/MenuPositioner.tsx
index d2ef2f12f3..770993897e 100644
--- a/packages/react/src/menu/positioner/MenuPositioner.tsx
+++ b/packages/react/src/menu/positioner/MenuPositioner.tsx
@@ -18,6 +18,7 @@ import { BaseUIComponentProps } from '../../utils/types';
import { popupStateMapping } from '../../utils/popupStateMapping';
import { CompositeList } from '../../composite/list/CompositeList';
import { InternalBackdrop } from '../../utils/InternalBackdrop';
+import { useMenuPortalContext } from '../portal/MenuPortalContext';
/**
* Positions the menu popup against the trigger.
@@ -34,7 +35,6 @@ const MenuPositioner = React.forwardRef(function MenuPositioner(
positionMethod = 'absolute',
className,
render,
- keepMounted = false,
side,
align,
sideOffset = 0,
@@ -57,6 +57,7 @@ const MenuPositioner = React.forwardRef(function MenuPositioner(
setOpen,
modal,
} = useMenuRootContext();
+ const keepMounted = useMenuPortalContext();
const { events: menuEvents } = useFloatingTree()!;
@@ -90,6 +91,7 @@ const MenuPositioner = React.forwardRef(function MenuPositioner(
parentNodeId,
menuEvents,
setOpen,
+ keepMounted,
});
const state: MenuPositioner.State = React.useMemo(
@@ -134,11 +136,6 @@ const MenuPositioner = React.forwardRef(function MenuPositioner(
extraProps: otherProps,
});
- const shouldRender = keepMounted || mounted;
- if (!shouldRender) {
- return null;
- }
-
return (
{mounted && modal && parentNodeId === null && }
@@ -235,11 +232,6 @@ MenuPositioner.propTypes /* remove-proptypes */ = {
top: PropTypes.number,
}),
]),
- /**
- * Whether to keep the HTML element in the DOM while the menu is hidden.
- * @default false
- */
- keepMounted: PropTypes.bool,
/**
* Determines which CSS `position` property to use.
* @default 'absolute'
diff --git a/packages/react/src/menu/positioner/useMenuPositioner.ts b/packages/react/src/menu/positioner/useMenuPositioner.ts
index 7823fb8479..46c5c89aae 100644
--- a/packages/react/src/menu/positioner/useMenuPositioner.ts
+++ b/packages/react/src/menu/positioner/useMenuPositioner.ts
@@ -15,7 +15,7 @@ import { useMenuRootContext } from '../root/MenuRootContext';
export function useMenuPositioner(
params: useMenuPositioner.Parameters,
): useMenuPositioner.ReturnValue {
- const { keepMounted, mounted, menuEvents, nodeId, parentNodeId, setOpen } = params;
+ const { mounted, menuEvents, nodeId, parentNodeId, setOpen } = params;
const { open } = useMenuRootContext();
@@ -34,7 +34,7 @@ export function useMenuPositioner(
(externalProps = {}) => {
const hiddenStyles: React.CSSProperties = {};
- if (keepMounted && !open) {
+ if (!open) {
hiddenStyles.pointerEvents = 'none';
}
@@ -47,7 +47,7 @@ export function useMenuPositioner(
},
});
},
- [keepMounted, open, positionerStyles, mounted],
+ [open, positionerStyles, mounted],
);
React.useEffect(() => {
@@ -144,11 +144,6 @@ export namespace useMenuPositioner {
* @default 5
*/
collisionPadding?: Padding;
- /**
- * Whether to keep the HTML element in the DOM while the menu is hidden.
- * @default false
- */
- keepMounted?: boolean;
/**
* Whether to maintain the menu in the viewport after
* the anchor element is scrolled out of view.
@@ -165,6 +160,10 @@ export namespace useMenuPositioner {
}
export interface Parameters extends SharedParameters {
+ /**
+ * Whether the portal is kept mounted in the DOM while the popup is closed.
+ */
+ keepMounted: boolean;
/**
* Whether the Menu is mounted.
*/
diff --git a/packages/react/src/menu/radio-item-indicator/MenuRadioItemIndicator.test.tsx b/packages/react/src/menu/radio-item-indicator/MenuRadioItemIndicator.test.tsx
index 672ccc7715..a896a44b83 100644
--- a/packages/react/src/menu/radio-item-indicator/MenuRadioItemIndicator.test.tsx
+++ b/packages/react/src/menu/radio-item-indicator/MenuRadioItemIndicator.test.tsx
@@ -12,13 +12,15 @@ describe(' ', () => {
render(node) {
return render(
-
-
-
- {node}
-
-
-
+
+
+
+
+ {node}
+
+
+
+
,
);
},
@@ -37,20 +39,22 @@ describe(' ', () => {
setValue('b')}>Close
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
);
@@ -103,23 +107,25 @@ describe(' ', () => {
setValue('b')}>Close
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/packages/react/src/menu/radio-item/MenuRadioItem.test.tsx b/packages/react/src/menu/radio-item/MenuRadioItem.test.tsx
index 012555604d..5c6aa09b17 100644
--- a/packages/react/src/menu/radio-item/MenuRadioItem.test.tsx
+++ b/packages/react/src/menu/radio-item/MenuRadioItem.test.tsx
@@ -84,40 +84,42 @@ describe(' ', () => {
const { getAllByRole } = await render(
-
-
-
- }
- id="item-1"
- >
- 1
-
- }
- id="item-2"
- >
- 2
-
- }
- id="item-3"
- >
- 3
-
- }
- id="item-4"
- >
- 4
-
-
-
-
+
+
+
+
+ }
+ id="item-1"
+ >
+ 1
+
+ }
+ id="item-2"
+ >
+ 2
+
+ }
+ id="item-3"
+ >
+ 3
+
+ }
+ id="item-4"
+ >
+ 4
+
+
+
+
+
,
);
@@ -162,13 +164,15 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
-
- Item
-
-
-
+
+
+
+
+ Item
+
+
+
+
,
);
@@ -187,11 +191,13 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
- Item
-
-
+
+
+
+ Item
+
+
+
,
);
@@ -216,13 +222,15 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
-
- Item
-
-
-
+
+
+
+
+ Item
+
+
+
+
,
);
@@ -240,13 +248,15 @@ describe(' ', () => {
const { getByRole, user } = await render(
Open
-
-
-
- Item
-
-
-
+
+
+
+
+ Item
+
+
+
+
,
);
@@ -271,15 +281,17 @@ describe(' ', () => {
const { getByRole, queryByRole, user } = await render(
Open
-
-
-
-
- Item
-
-
-
-
+
+
+
+
+
+ Item
+
+
+
+
+
,
);
@@ -296,13 +308,15 @@ describe(' ', () => {
const { getByRole, queryByRole, user } = await render(
Open
-
-
-
- Item
-
-
-
+
+
+
+
+ Item
+
+
+
+
,
);
diff --git a/packages/react/src/menu/root/MenuRoot.test.tsx b/packages/react/src/menu/root/MenuRoot.test.tsx
index f13f33540f..5bb3303f03 100644
--- a/packages/react/src/menu/root/MenuRoot.test.tsx
+++ b/packages/react/src/menu/root/MenuRoot.test.tsx
@@ -20,13 +20,15 @@ describe(' ', () => {
const { getByRole, getByTestId } = await render(
Toggle
-
-
- 1
- 2
- 3
-
-
+
+
+
+ 1
+ 2
+ 3
+
+
+
,
);
@@ -65,13 +67,15 @@ describe(' ', () => {
const { getByRole, getByTestId } = await render(
Toggle
-
-
- 1
- 2
- 3
-
-
+
+
+
+ 1
+ 2
+ 3
+
+
+
,
);
@@ -103,14 +107,16 @@ describe(' ', () => {
const { getByRole, getByTestId } = await render(
Toggle
-
-
- 1
-
- 2
-
-
-
+
+
+
+ 1
+
+ 2
+
+
+
+
,
);
@@ -128,7 +134,7 @@ describe(' ', () => {
expect(item1).toHaveFocus();
});
- await userEvent.keyboard('[ArrowDown]');
+ await userEvent.keyboard('{ArrowDown}');
await waitFor(() => {
expect(item2).toHaveFocus();
@@ -149,16 +155,18 @@ describe(' ', () => {
const { getByText, getAllByRole } = await render(
-
-
- Aa
- Ba
- Bb
- Ca
- Cb
- Cd
-
-
+
+
+
+ Aa
+ Ba
+ Bb
+ Ca
+ Cb
+ Cd
+
+
+
,
);
@@ -194,14 +202,16 @@ describe(' ', () => {
const { getByRole, getAllByRole } = await render(
Toggle
-
-
- 1
- 2
- 3
- 4
-
-
+
+
+
+ 1
+ 2
+ 3
+ 4
+
+
+
,
);
@@ -249,19 +259,21 @@ describe(' ', () => {
const { getByText, getAllByRole } = await render(
-
-
- Aa
- Ba
-
-
- Nested Content
-
- {undefined}
- {null}
- Bc
-
-
+
+
+
+ Aa
+ Ba
+
+
+ Nested Content
+
+ {undefined}
+ {null}
+ Bc
+
+
+
,
);
@@ -295,14 +307,16 @@ describe(' ', () => {
const { getByText, getAllByRole } = await render(
-
-
- Aa
- Ba
- Bb
- Bą
-
-
+
+
+
+ Aa
+ Ba
+ Bb
+ Bą
+
+
+
,
);
@@ -336,14 +350,16 @@ describe(' ', () => {
const { getByText, getAllByRole } = await render(
-
-
- Aa
- ąa
- ąb
- ąc
-
-
+
+
+
+ Aa
+ ąa
+ ąb
+ ąc
+
+
+
,
);
@@ -373,13 +389,15 @@ describe(' ', () => {
const { getAllByRole } = await render(
-
-
- handleClick()}>Item One
- handleClick()}>Item Two
- handleClick()}>Item Three
-
-
+
+
+
+ handleClick()}>Item One
+ handleClick()}>Item Two
+ handleClick()}>Item Three
+
+
+
,
);
@@ -413,20 +431,24 @@ describe(' ', () => {
const { getByTestId, queryByTestId } = await render(
-
-
- 1
-
- 2
-
-
- 2.1
- 2.2
-
-
-
-
-
+
+
+
+ 1
+
+ 2
+
+
+
+ 2.1
+ 2.2
+
+
+
+
+
+
+
,
);
@@ -463,13 +485,15 @@ describe(' ', () => {
return (
Toggle
-
-
- 1
- 2
- 3
-
-
+
+
+
+ 1
+ 2
+ 3
+
+
+
);
}
@@ -539,11 +563,13 @@ describe(' ', () => {
Toggle
-
-
- Close
-
-
+
+
+
+ Close
+
+
+
,
@@ -571,11 +597,13 @@ describe(' ', () => {
Toggle
-
-
- Close
-
-
+
+
+
+ Close
+
+
+
,
@@ -598,20 +626,24 @@ describe(' ', () => {
const { getByRole, queryByRole } = await render(
Open
-
-
- 1
-
- 2
-
-
- 2.1
- 2.2
-
-
-
-
-
+
+
+
+ 1
+
+ 2
+
+
+
+ 2.1
+ 2.2
+
+
+
+
+
+
+
,
);
@@ -645,20 +677,24 @@ describe(' ', () => {
const { getByRole, queryAllByRole } = await render(
Open
-
-
-
+
+
+
+
+
,
);
@@ -708,9 +744,11 @@ describe(' ', () => {
setOpen(false)}>Close
-
-
-
+
+
+
+
+
);
@@ -765,12 +803,14 @@ describe(' ', () => {
setOpen(false)}>Close
-
-
-
+
+
+
+
+
);
diff --git a/packages/react/src/menu/root/MenuRoot.tsx b/packages/react/src/menu/root/MenuRoot.tsx
index c56b0129e1..f91c486a89 100644
--- a/packages/react/src/menu/root/MenuRoot.tsx
+++ b/packages/react/src/menu/root/MenuRoot.tsx
@@ -5,7 +5,6 @@ import { FloatingTree } from '@floating-ui/react';
import { useDirection } from '../../direction-provider/DirectionContext';
import { MenuRootContext, useMenuRootContext } from './MenuRootContext';
import { MenuOrientation, useMenuRoot } from './useMenuRoot';
-import { PortalContext } from '../../portal/PortalContext';
/**
* Groups all parts of the menu.
@@ -74,18 +73,12 @@ const MenuRoot: React.FC = function MenuRoot(props) {
// set up a FloatingTree to provide the context to nested menus
return (
-
- {children}
-
+ {children}
);
}
- return (
-
- {children}
-
- );
+ return {children} ;
};
namespace MenuRoot {
diff --git a/packages/react/src/menu/trigger/MenuTrigger.test.tsx b/packages/react/src/menu/trigger/MenuTrigger.test.tsx
index b994e047fd..4ffb900856 100644
--- a/packages/react/src/menu/trigger/MenuTrigger.test.tsx
+++ b/packages/react/src/menu/trigger/MenuTrigger.test.tsx
@@ -62,9 +62,11 @@ describe(' ', () => {
const { getByRole, queryByRole } = await render(
-
-
-
+
+
+
+
+
,
);
@@ -79,9 +81,11 @@ describe(' ', () => {
const { getByRole, queryByRole } = await render(
Open
-
-
-
+
+
+
+
+
,
);
@@ -109,11 +113,13 @@ describe(' ', () => {
const { getByRole, queryByRole } = await render(
{buttonComponent}
-
-
- 1
-
-
+
+
+
+ 1
+
+
+
,
);
diff --git a/packages/react/src/popover/arrow/PopoverArrow.test.tsx b/packages/react/src/popover/arrow/PopoverArrow.test.tsx
index 8e2957129c..9238dd09ab 100644
--- a/packages/react/src/popover/arrow/PopoverArrow.test.tsx
+++ b/packages/react/src/popover/arrow/PopoverArrow.test.tsx
@@ -10,9 +10,11 @@ describe(' ', () => {
render(node) {
return render(
-
- {node}
-
+
+
+ {node}
+
+
,
);
},
diff --git a/packages/react/src/popover/backdrop/PopoverBackdrop.tsx b/packages/react/src/popover/backdrop/PopoverBackdrop.tsx
index 0ebee86807..b530a178c3 100644
--- a/packages/react/src/popover/backdrop/PopoverBackdrop.tsx
+++ b/packages/react/src/popover/backdrop/PopoverBackdrop.tsx
@@ -24,7 +24,7 @@ const PopoverBackdrop = React.forwardRef(function PopoverBackdrop(
props: PopoverBackdrop.Props,
forwardedRef: React.ForwardedRef,
) {
- const { className, render, keepMounted = false, ...other } = props;
+ const { className, render, ...other } = props;
const { open, mounted, transitionStatus } = usePopoverRootContext();
@@ -45,8 +45,7 @@ const PopoverBackdrop = React.forwardRef(function PopoverBackdrop(
customStyleHookMapping,
});
- const shouldRender = keepMounted || mounted;
- if (!shouldRender) {
+ if (!mounted) {
return null;
}
@@ -62,13 +61,7 @@ namespace PopoverBackdrop {
transitionStatus: TransitionStatus;
}
- export interface Props extends BaseUIComponentProps<'div', State> {
- /**
- * Whether to keep the HTML element in the DOM while the popover is hidden.
- * @default false
- */
- keepMounted?: boolean;
- }
+ export interface Props extends BaseUIComponentProps<'div', State> {}
}
PopoverBackdrop.propTypes /* remove-proptypes */ = {
@@ -85,11 +78,6 @@ PopoverBackdrop.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 popover 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/popover/close/PopoverClose.test.tsx b/packages/react/src/popover/close/PopoverClose.test.tsx
index 61119ccc57..72daf95943 100644
--- a/packages/react/src/popover/close/PopoverClose.test.tsx
+++ b/packages/react/src/popover/close/PopoverClose.test.tsx
@@ -12,9 +12,11 @@ describe(' ', () => {
render(node) {
return render(
-
- {node}
-
+
+
+ {node}
+
+
,
);
},
@@ -23,12 +25,14 @@ describe(' ', () => {
it('should close popover when clicked', async () => {
await render(
-
-
- Content
-
-
-
+
+
+
+ Content
+
+
+
+
,
);
diff --git a/packages/react/src/popover/description/PopoverDescription.test.tsx b/packages/react/src/popover/description/PopoverDescription.test.tsx
index 879ca9965c..91de1605e4 100644
--- a/packages/react/src/popover/description/PopoverDescription.test.tsx
+++ b/packages/react/src/popover/description/PopoverDescription.test.tsx
@@ -12,9 +12,11 @@ describe(' ', () => {
render(node) {
return render(
-
- {node}
-
+
+
+ {node}
+
+
,
);
},
@@ -23,11 +25,13 @@ describe(' ', () => {
it('describes the popup element with its id', async () => {
await render(
-
-
- Title
-
-
+
+
+
+ Title
+
+
+
,
);
diff --git a/packages/react/src/popover/index.parts.ts b/packages/react/src/popover/index.parts.ts
index 3507f883e8..f2b763ec11 100644
--- a/packages/react/src/popover/index.parts.ts
+++ b/packages/react/src/popover/index.parts.ts
@@ -1,6 +1,6 @@
export { PopoverRoot as Root } from './root/PopoverRoot';
export { PopoverTrigger as Trigger } from './trigger/PopoverTrigger';
-export { Portal } from '../portal/Portal';
+export { PopoverPortal as Portal } from './portal/PopoverPortal';
export { PopoverPositioner as Positioner } from './positioner/PopoverPositioner';
export { PopoverPopup as Popup } from './popup/PopoverPopup';
export { PopoverArrow as Arrow } from './arrow/PopoverArrow';
diff --git a/packages/react/src/popover/popup/PopoverPopup.test.tsx b/packages/react/src/popover/popup/PopoverPopup.test.tsx
index 545f4cf65d..74bc4f3039 100644
--- a/packages/react/src/popover/popup/PopoverPopup.test.tsx
+++ b/packages/react/src/popover/popup/PopoverPopup.test.tsx
@@ -12,7 +12,9 @@ describe(' ', () => {
render(node) {
return render(
- {node}
+
+ {node}
+
,
);
},
@@ -21,9 +23,11 @@ describe(' ', () => {
it('should render the children', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -37,12 +41,14 @@ describe(' ', () => {
Open
-
-
-
- Close
-
-
+
+
+
+
+ Close
+
+
+
,
@@ -67,14 +73,16 @@ describe(' ', () => {
Open
-
-
-
-
-
- Close
-
-
+
+
+
+
+
+
+ Close
+
+
+
@@ -105,14 +113,16 @@ describe(' ', () => {
Open
-
-
-
-
-
- Close
-
-
+
+
+
+
+
+
+ Close
+
+
+
@@ -140,11 +150,13 @@ describe(' ', () => {
Open
-
-
- Close
-
-
+
+
+
+ Close
+
+
+
,
@@ -173,11 +185,13 @@ describe(' ', () => {
Open
-
-
- Close
-
-
+
+
+
+ Close
+
+
+
diff --git a/packages/react/src/popover/portal/PopoverPortal.tsx b/packages/react/src/popover/portal/PopoverPortal.tsx
new file mode 100644
index 0000000000..080d1d06b4
--- /dev/null
+++ b/packages/react/src/popover/portal/PopoverPortal.tsx
@@ -0,0 +1,67 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingPortal } from '@floating-ui/react';
+import { usePopoverRootContext } from '../root/PopoverRootContext';
+import { HTMLElementType, refType } from '../../utils/proptypes';
+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 ``.
+ *
+ * Documentation: [Base UI Popover](https://base-ui.com/react/components/popover)
+ */
+function PopoverPortal(props: PopoverPortal.Props) {
+ const { children, keepMounted = false, container } = props;
+
+ const { mounted } = usePopoverRootContext();
+
+ const shouldRender = mounted || keepMounted;
+ if (!shouldRender) {
+ return null;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+namespace PopoverPortal {
+ 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;
+ }
+}
+
+PopoverPortal.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 { PopoverPortal };
diff --git a/packages/react/src/popover/portal/PopoverPortalContext.ts b/packages/react/src/popover/portal/PopoverPortalContext.ts
new file mode 100644
index 0000000000..a47307eae9
--- /dev/null
+++ b/packages/react/src/popover/portal/PopoverPortalContext.ts
@@ -0,0 +1,11 @@
+import * as React from 'react';
+
+export const PopoverPortalContext = React.createContext(undefined);
+
+export function usePopoverPortalContext() {
+ const value = React.useContext(PopoverPortalContext);
+ if (value === undefined) {
+ throw new Error('Base UI: is missing.');
+ }
+ return value;
+}
diff --git a/packages/react/src/popover/positioner/PopoverPositioner.test.tsx b/packages/react/src/popover/positioner/PopoverPositioner.test.tsx
index 7a6c547ef8..1f407ac9ed 100644
--- a/packages/react/src/popover/positioner/PopoverPositioner.test.tsx
+++ b/packages/react/src/popover/positioner/PopoverPositioner.test.tsx
@@ -10,7 +10,11 @@ describe(' ', () => {
describeConformance( , () => ({
refInstanceof: window.HTMLDivElement,
render(node) {
- return render({node} );
+ return render(
+
+ {node}
+ ,
+ );
},
}));
@@ -18,7 +22,9 @@ describe(' ', () => {
it('has hidden attribute when closed', async () => {
await render(
-
+
+
+
,
);
@@ -28,7 +34,9 @@ describe(' ', () => {
it('does not have inert attribute when open', async () => {
await render(
-
+
+
+
,
);
diff --git a/packages/react/src/popover/positioner/PopoverPositioner.tsx b/packages/react/src/popover/positioner/PopoverPositioner.tsx
index 0b8ded616d..2536c191bf 100644
--- a/packages/react/src/popover/positioner/PopoverPositioner.tsx
+++ b/packages/react/src/popover/positioner/PopoverPositioner.tsx
@@ -10,6 +10,7 @@ import { HTMLElementType } from '../../utils/proptypes';
import type { BaseUIComponentProps } from '../../utils/types';
import type { Side, Align } from '../../utils/useAnchorPositioning';
import { popupStateMapping } from '../../utils/popupStateMapping';
+import { usePopoverPortalContext } from '../portal/PopoverPortalContext';
/**
* Positions the popover against the trigger.
@@ -25,7 +26,6 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner(
render,
className,
anchor,
- keepMounted = false,
positionMethod = 'absolute',
side = 'bottom',
align = 'center',
@@ -40,6 +40,7 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner(
const { floatingRootContext, open, mounted, setPositionerElement, popupRef, openMethod } =
usePopoverRootContext();
+ const keepMounted = usePopoverPortalContext();
const positioner = usePopoverPositioner({
anchor,
@@ -47,7 +48,6 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner(
positionMethod,
mounted,
open,
- keepMounted,
side,
sideOffset,
align,
@@ -58,6 +58,7 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner(
sticky,
popupRef,
openMethod,
+ keepMounted,
});
const state: PopoverPositioner.State = React.useMemo(
@@ -82,11 +83,6 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner(
customStyleHookMapping: popupStateMapping,
});
- const shouldRender = keepMounted || mounted;
- if (!shouldRender) {
- return null;
- }
-
return (
{renderElement()}
@@ -178,11 +174,6 @@ PopoverPositioner.propTypes /* remove-proptypes */ = {
top: PropTypes.number,
}),
]),
- /**
- * Whether to keep the HTML element in the DOM while the popover is hidden.
- * @default false
- */
- keepMounted: PropTypes.bool,
/**
* Determines which CSS `position` property to use.
* @default 'absolute'
diff --git a/packages/react/src/popover/positioner/usePopoverPositioner.tsx b/packages/react/src/popover/positioner/usePopoverPositioner.tsx
index 6d9456b5ec..68ec63b80a 100644
--- a/packages/react/src/popover/positioner/usePopoverPositioner.tsx
+++ b/packages/react/src/popover/positioner/usePopoverPositioner.tsx
@@ -13,7 +13,7 @@ import { InteractionType } from '../../utils/useEnhancedClickHandler';
export function usePopoverPositioner(
params: usePopoverPositioner.Parameters,
): usePopoverPositioner.ReturnValue {
- const { open = false, keepMounted = false, mounted } = params;
+ const { open = false, mounted } = params;
const {
positionerStyles,
@@ -31,7 +31,7 @@ export function usePopoverPositioner(
(externalProps = {}) => {
const hiddenStyles: React.CSSProperties = {};
- if (keepMounted && !open) {
+ if (!open) {
hiddenStyles.pointerEvents = 'none';
}
@@ -44,7 +44,7 @@ export function usePopoverPositioner(
},
});
},
- [keepMounted, open, mounted, positionerStyles],
+ [open, mounted, positionerStyles],
);
return React.useMemo(
@@ -132,11 +132,6 @@ export namespace usePopoverPositioner {
* @default 5
*/
arrowPadding?: number;
- /**
- * Whether to keep the HTML element in the DOM while the popover is hidden.
- * @default false
- */
- keepMounted?: boolean;
/**
* Whether the popover continuously tracks its anchor after the initial positioning upon mount.
* @default true
@@ -145,6 +140,10 @@ export namespace usePopoverPositioner {
}
export interface Parameters extends SharedParameters {
+ /**
+ * Whether the portal is kept mounted in the DOM while the popup is closed.
+ */
+ keepMounted: boolean;
/**
* Whether the popover is mounted.
*/
diff --git a/packages/react/src/popover/root/PopoverRoot.test.tsx b/packages/react/src/popover/root/PopoverRoot.test.tsx
index dba1471bd3..57f811692b 100644
--- a/packages/react/src/popover/root/PopoverRoot.test.tsx
+++ b/packages/react/src/popover/root/PopoverRoot.test.tsx
@@ -32,9 +32,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -51,9 +53,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -75,9 +79,11 @@ describe(' ', () => {
it('should open when controlled open is true', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -87,9 +93,11 @@ describe(' ', () => {
it('should close when controlled open is false', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -111,9 +119,11 @@ describe(' ', () => {
}}
>
-
- Content
-
+
+
+ Content
+
+
);
}
@@ -153,9 +163,11 @@ describe(' ', () => {
}}
>
-
- Content
-
+
+
+ Content
+
+
);
}
@@ -189,9 +201,11 @@ describe(' ', () => {
setOpen(false)}>Close
-
-
-
+
+
+
+
+
);
@@ -247,12 +261,14 @@ describe(' ', () => {
setOpen(false)}>Close
-
-
-
+
+
+
+
+
);
@@ -276,9 +292,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -289,9 +307,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -302,9 +322,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -315,9 +337,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -338,9 +362,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -368,9 +394,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -404,11 +432,13 @@ describe(' ', () => {
Toggle
-
-
- Close
-
-
+
+
+
+ Close
+
+
+
,
@@ -435,11 +465,13 @@ describe(' ', () => {
const { user } = await render(
Toggle
-
-
- Close
-
-
+
+
+
+ Close
+
+
+
,
);
@@ -478,9 +510,11 @@ describe(' ', () => {
Toggle
-
-
-
+
+
+
+
+
,
diff --git a/packages/react/src/popover/root/PopoverRoot.tsx b/packages/react/src/popover/root/PopoverRoot.tsx
index 1feeb4dd2c..9fedbbf161 100644
--- a/packages/react/src/popover/root/PopoverRoot.tsx
+++ b/packages/react/src/popover/root/PopoverRoot.tsx
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { PopoverRootContext } from './PopoverRootContext';
import { usePopoverRoot } from './usePopoverRoot';
import { OPEN_DELAY } from '../utils/constants';
-import { PortalContext } from '../../portal/PortalContext';
/**
* Groups all parts of the popover.
@@ -98,9 +97,7 @@ const PopoverRoot: React.FC = function PopoverRoot(props) {
);
return (
-
- {props.children}
-
+ {props.children}
);
};
diff --git a/packages/react/src/popover/title/PopoverTitle.test.tsx b/packages/react/src/popover/title/PopoverTitle.test.tsx
index 3aaecd40dc..b4047511c7 100644
--- a/packages/react/src/popover/title/PopoverTitle.test.tsx
+++ b/packages/react/src/popover/title/PopoverTitle.test.tsx
@@ -12,9 +12,11 @@ describe(' ', () => {
render(node) {
return render(
-
- {node}
-
+
+
+ {node}
+
+
,
);
},
@@ -23,11 +25,13 @@ describe(' ', () => {
it('labels the popup element with its id', async () => {
await render(
-
-
- Title
-
-
+
+
+
+ Title
+
+
+
,
);
diff --git a/packages/react/src/portal/PortalContext.ts b/packages/react/src/portal/PortalContext.ts
deleted file mode 100644
index 6f5c4fb5e5..0000000000
--- a/packages/react/src/portal/PortalContext.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as React from 'react';
-
-export const PortalContext = React.createContext(undefined);
-
-if (process.env.NODE_ENV !== 'production') {
- PortalContext.displayName = 'PortalContext';
-}
-
-export function usePortalContext() {
- const context = React.useContext(PortalContext);
- if (context === undefined) {
- throw new Error(
- 'Base UI: PortalContext is missing. Portal parts must be placed within the Root of a component.',
- );
- }
- return context;
-}
diff --git a/packages/react/src/preview-card/arrow/PreviewCardArrow.test.tsx b/packages/react/src/preview-card/arrow/PreviewCardArrow.test.tsx
index 1cff6f27c8..1dc6dce3d2 100644
--- a/packages/react/src/preview-card/arrow/PreviewCardArrow.test.tsx
+++ b/packages/react/src/preview-card/arrow/PreviewCardArrow.test.tsx
@@ -10,9 +10,11 @@ describe(' ', () => {
render(node) {
return render(
-
- {node}
-
+
+
+ {node}
+
+
,
);
},
diff --git a/packages/react/src/preview-card/backdrop/PreviewCardBackdrop.tsx b/packages/react/src/preview-card/backdrop/PreviewCardBackdrop.tsx
index 99c9424006..4163ab4f9f 100644
--- a/packages/react/src/preview-card/backdrop/PreviewCardBackdrop.tsx
+++ b/packages/react/src/preview-card/backdrop/PreviewCardBackdrop.tsx
@@ -24,7 +24,7 @@ const PreviewCardBackdrop = React.forwardRef(function PreviewCardBackdrop(
props: PreviewCardBackdrop.Props,
forwardedRef: React.ForwardedRef,
) {
- const { render, className, keepMounted = false, ...other } = props;
+ const { render, className, ...other } = props;
const { open, mounted, transitionStatus } = usePreviewCardRootContext();
@@ -45,8 +45,7 @@ const PreviewCardBackdrop = React.forwardRef(function PreviewCardBackdrop(
customStyleHookMapping,
});
- const shouldRender = keepMounted || mounted;
- if (!shouldRender) {
+ if (!mounted) {
return null;
}
@@ -62,13 +61,7 @@ namespace PreviewCardBackdrop {
transitionStatus: TransitionStatus;
}
- export interface Props extends BaseUIComponentProps<'div', State> {
- /**
- * Whether to keep the HTML element in the DOM while the preview card is hidden.
- * @default false
- */
- keepMounted?: boolean;
- }
+ export interface Props extends BaseUIComponentProps<'div', State> {}
}
PreviewCardBackdrop.propTypes /* remove-proptypes */ = {
@@ -85,11 +78,6 @@ PreviewCardBackdrop.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 preview card 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/preview-card/index.parts.ts b/packages/react/src/preview-card/index.parts.ts
index 8048ba6e7a..aa072531aa 100644
--- a/packages/react/src/preview-card/index.parts.ts
+++ b/packages/react/src/preview-card/index.parts.ts
@@ -1,5 +1,5 @@
export { PreviewCardRoot as Root } from './root/PreviewCardRoot';
-export { Portal } from '../portal/Portal';
+export { PreviewCardPortal as Portal } from './portal/PreviewCardPortal';
export { PreviewCardTrigger as Trigger } from './trigger/PreviewCardTrigger';
export { PreviewCardPositioner as Positioner } from './positioner/PreviewCardPositioner';
export { PreviewCardPopup as Popup } from './popup/PreviewCardPopup';
diff --git a/packages/react/src/preview-card/popup/PreviewCardPopup.test.tsx b/packages/react/src/preview-card/popup/PreviewCardPopup.test.tsx
index a96810cba4..93b976ed18 100644
--- a/packages/react/src/preview-card/popup/PreviewCardPopup.test.tsx
+++ b/packages/react/src/preview-card/popup/PreviewCardPopup.test.tsx
@@ -12,7 +12,9 @@ describe(' ', () => {
render(node) {
return render(
- {node}
+
+ {node}
+
,
);
},
@@ -21,9 +23,11 @@ describe(' ', () => {
it('should render the children', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
diff --git a/packages/react/src/preview-card/portal/PreviewCardPortal.tsx b/packages/react/src/preview-card/portal/PreviewCardPortal.tsx
new file mode 100644
index 0000000000..b9b92c7103
--- /dev/null
+++ b/packages/react/src/preview-card/portal/PreviewCardPortal.tsx
@@ -0,0 +1,67 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingPortal } from '@floating-ui/react';
+import { usePreviewCardRootContext } from '../root/PreviewCardContext';
+import { HTMLElementType, refType } from '../../utils/proptypes';
+import { PreviewCardPortalContext } from './PreviewCardPortalContext';
+
+/**
+ * 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 Preview Card](https://base-ui.com/react/components/preview-card)
+ */
+function PreviewCardPortal(props: PreviewCardPortal.Props) {
+ const { children, keepMounted = false, container } = props;
+
+ const { mounted } = usePreviewCardRootContext();
+
+ const shouldRender = mounted || keepMounted;
+ if (!shouldRender) {
+ return null;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+namespace PreviewCardPortal {
+ 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;
+ }
+}
+
+PreviewCardPortal.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 { PreviewCardPortal };
diff --git a/packages/react/src/preview-card/portal/PreviewCardPortalContext.ts b/packages/react/src/preview-card/portal/PreviewCardPortalContext.ts
new file mode 100644
index 0000000000..5e0e9d83f1
--- /dev/null
+++ b/packages/react/src/preview-card/portal/PreviewCardPortalContext.ts
@@ -0,0 +1,11 @@
+import * as React from 'react';
+
+export const PreviewCardPortalContext = React.createContext(undefined);
+
+export function usePreviewCardPortalContext() {
+ const value = React.useContext(PreviewCardPortalContext);
+ if (value === undefined) {
+ throw new Error('Base UI: is missing.');
+ }
+ return value;
+}
diff --git a/packages/react/src/preview-card/positioner/PreviewCardPositioner.test.tsx b/packages/react/src/preview-card/positioner/PreviewCardPositioner.test.tsx
index 22838875ed..d7c728e533 100644
--- a/packages/react/src/preview-card/positioner/PreviewCardPositioner.test.tsx
+++ b/packages/react/src/preview-card/positioner/PreviewCardPositioner.test.tsx
@@ -8,7 +8,11 @@ describe(' ', () => {
describeConformance( , () => ({
refInstanceof: window.HTMLDivElement,
render(node) {
- return render({node} );
+ return render(
+
+ {node}
+ ,
+ );
},
}));
});
diff --git a/packages/react/src/preview-card/positioner/PreviewCardPositioner.tsx b/packages/react/src/preview-card/positioner/PreviewCardPositioner.tsx
index 6f69caae0e..08f9dc095f 100644
--- a/packages/react/src/preview-card/positioner/PreviewCardPositioner.tsx
+++ b/packages/react/src/preview-card/positioner/PreviewCardPositioner.tsx
@@ -10,6 +10,7 @@ import { HTMLElementType } from '../../utils/proptypes';
import type { Side, Align } from '../../utils/useAnchorPositioning';
import type { BaseUIComponentProps } from '../../utils/types';
import { popupStateMapping } from '../../utils/popupStateMapping';
+import { usePreviewCardPortalContext } from '../portal/PreviewCardPortalContext';
/**
* Positions the popup against the trigger.
@@ -34,11 +35,11 @@ const PreviewCardPositioner = React.forwardRef(function PreviewCardPositioner(
collisionPadding = 5,
arrowPadding = 5,
sticky = false,
- keepMounted = false,
...otherProps
} = props;
const { open, mounted, floatingRootContext, setPositionerElement } = usePreviewCardRootContext();
+ const keepMounted = usePreviewCardPortalContext();
const positioner = usePreviewCardPositioner({
anchor,
@@ -46,7 +47,6 @@ const PreviewCardPositioner = React.forwardRef(function PreviewCardPositioner(
positionMethod,
open,
mounted,
- keepMounted,
side,
sideOffset,
align,
@@ -55,6 +55,7 @@ const PreviewCardPositioner = React.forwardRef(function PreviewCardPositioner(
collisionBoundary,
collisionPadding,
sticky,
+ keepMounted,
});
const state: PreviewCardPositioner.State = React.useMemo(
@@ -96,11 +97,6 @@ const PreviewCardPositioner = React.forwardRef(function PreviewCardPositioner(
customStyleHookMapping: popupStateMapping,
});
- const shouldRender = keepMounted || mounted;
- if (!shouldRender) {
- return null;
- }
-
return (
{renderElement()}
@@ -192,11 +188,6 @@ PreviewCardPositioner.propTypes /* remove-proptypes */ = {
top: PropTypes.number,
}),
]),
- /**
- * Whether to keep the HTML element in the DOM while the preview card is hidden.
- * @default false
- */
- keepMounted: PropTypes.bool,
/**
* Determines which CSS `position` property to use.
* @default 'absolute'
diff --git a/packages/react/src/preview-card/positioner/usePreviewCardPositioner.ts b/packages/react/src/preview-card/positioner/usePreviewCardPositioner.ts
index 82bc387025..2228d1e050 100644
--- a/packages/react/src/preview-card/positioner/usePreviewCardPositioner.ts
+++ b/packages/react/src/preview-card/positioner/usePreviewCardPositioner.ts
@@ -13,7 +13,7 @@ import { usePreviewCardRootContext } from '../root/PreviewCardContext';
export function usePreviewCardPositioner(
params: usePreviewCardPositioner.Parameters,
): usePreviewCardPositioner.ReturnValue {
- const { keepMounted, mounted } = params;
+ const { mounted } = params;
const { open } = usePreviewCardRootContext();
@@ -33,7 +33,7 @@ export function usePreviewCardPositioner(
(externalProps = {}) => {
const hiddenStyles: React.CSSProperties = {};
- if (keepMounted && !open) {
+ if (!open) {
hiddenStyles.pointerEvents = 'none';
}
@@ -46,7 +46,7 @@ export function usePreviewCardPositioner(
},
});
},
- [positionerStyles, open, keepMounted, mounted],
+ [positionerStyles, open, mounted],
);
return React.useMemo(
@@ -134,11 +134,6 @@ export namespace usePreviewCardPositioner {
* @default 5
*/
arrowPadding?: number;
- /**
- * Whether to keep the HTML element in the DOM while the preview card is hidden.
- * @default false
- */
- keepMounted?: boolean;
/**
* Whether the preview card popup continuously tracks its anchor after the initial positioning
* upon mount.
@@ -148,6 +143,10 @@ export namespace usePreviewCardPositioner {
}
export interface Parameters extends SharedParameters {
+ /**
+ * Whether the portal is kept mounted in the DOM while the popup is closed.
+ */
+ keepMounted: boolean;
/**
* Whether the preview card is mounted.
*/
diff --git a/packages/react/src/preview-card/root/PreviewCardRoot.test.tsx b/packages/react/src/preview-card/root/PreviewCardRoot.test.tsx
index f90c216bb4..876e5e84b0 100644
--- a/packages/react/src/preview-card/root/PreviewCardRoot.test.tsx
+++ b/packages/react/src/preview-card/root/PreviewCardRoot.test.tsx
@@ -32,9 +32,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -55,9 +57,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -87,9 +91,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -108,9 +114,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -131,9 +139,11 @@ describe(' ', () => {
it('should open when controlled open is true', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -143,9 +153,11 @@ describe(' ', () => {
it('should close when controlled open is false', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -166,9 +178,11 @@ describe(' ', () => {
setOpen(false)}>Close
-
- Content
-
+
+
+ Content
+
+
);
@@ -223,14 +237,16 @@ describe(' ', () => {
setOpen(false)}>Close
-
-
- Content
-
-
+
+
+
+ Content
+
+
+
);
@@ -267,9 +283,11 @@ describe(' ', () => {
}}
>
-
- Content
-
+
+
+ Content
+
+
);
}
@@ -314,9 +332,11 @@ describe(' ', () => {
}}
>
-
- Content
-
+
+
+ Content
+
+
);
}
@@ -347,9 +367,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -360,9 +382,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -373,9 +397,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -386,9 +412,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -411,9 +439,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -441,9 +471,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
diff --git a/packages/react/src/preview-card/root/PreviewCardRoot.tsx b/packages/react/src/preview-card/root/PreviewCardRoot.tsx
index 00055bc04a..0cc3e40816 100644
--- a/packages/react/src/preview-card/root/PreviewCardRoot.tsx
+++ b/packages/react/src/preview-card/root/PreviewCardRoot.tsx
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { PreviewCardRootContext } from './PreviewCardContext';
import { usePreviewCardRoot } from './usePreviewCardRoot';
import { CLOSE_DELAY, OPEN_DELAY } from '../utils/constants';
-import { PortalContext } from '../../portal/PortalContext';
/**
* Groups all parts of the preview card.
@@ -79,7 +78,7 @@ const PreviewCardRoot: React.FC = function PreviewCardRoo
return (
- {props.children}
+ {props.children}
);
};
diff --git a/packages/react/src/select/backdrop/SelectBackdrop.tsx b/packages/react/src/select/backdrop/SelectBackdrop.tsx
index f45cf3f592..3dce768d1d 100644
--- a/packages/react/src/select/backdrop/SelectBackdrop.tsx
+++ b/packages/react/src/select/backdrop/SelectBackdrop.tsx
@@ -24,7 +24,7 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop(
props: SelectBackdrop.Props,
forwardedRef: React.ForwardedRef,
) {
- const { className, render, keepMounted = false, ...other } = props;
+ const { className, render, ...other } = props;
const { open, mounted, transitionStatus } = useSelectRootContext();
@@ -42,8 +42,7 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop(
customStyleHookMapping,
});
- const shouldRender = keepMounted || mounted;
- if (!shouldRender) {
+ if (!mounted) {
return null;
}
@@ -51,13 +50,7 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop(
});
namespace SelectBackdrop {
- export interface Props extends BaseUIComponentProps<'div', State> {
- /**
- * Whether to keep the HTML element in the DOM while the select menu is hidden.
- * @default false
- */
- keepMounted?: boolean;
- }
+ export interface Props extends BaseUIComponentProps<'div', State> {}
export interface State {
/**
@@ -82,11 +75,6 @@ SelectBackdrop.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 select menu 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/select/item/SelectItem.test.tsx b/packages/react/src/select/item/SelectItem.test.tsx
index b14ac7c073..a1d561e2fa 100644
--- a/packages/react/src/select/item/SelectItem.test.tsx
+++ b/packages/react/src/select/item/SelectItem.test.tsx
@@ -53,13 +53,15 @@ describe(' ', () => {
-
-
- one
- two
- three
-
-
+
+
+
+ one
+ two
+ three
+
+
+
,
);
@@ -92,12 +94,14 @@ describe(' ', () => {
-
-
- one
- two
-
-
+
+
+
+ one
+ two
+
+
+
,
);
@@ -123,14 +127,16 @@ describe(' ', () => {
-
-
- one
-
- two
-
-
-
+
+
+
+ one
+
+ two
+
+
+
+
,
);
@@ -152,13 +158,15 @@ describe(' ', () => {
-
-
- one
- two
- three
-
-
+
+
+
+ one
+ two
+ three
+
+
+
,
);
@@ -189,12 +197,14 @@ describe(' ', () => {
const { user } = await render(
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
,
);
@@ -219,12 +229,14 @@ describe(' ', () => {
await render(
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
,
);
diff --git a/packages/react/src/select/item/SelectItem.tsx b/packages/react/src/select/item/SelectItem.tsx
index d9365f9c7d..da8aeefda5 100644
--- a/packages/react/src/select/item/SelectItem.tsx
+++ b/packages/react/src/select/item/SelectItem.tsx
@@ -233,16 +233,28 @@ const SelectItem = React.forwardRef(function SelectItem(
const listItem = useCompositeListItem({ label });
const { activeIndex, selectedIndex, setActiveIndex } = useSelectIndexContext();
- const { getItemProps, setOpen, setValue, open, selectionRef, typingRef, valuesRef, popupRef } =
- useSelectRootContext();
+ const {
+ getItemProps,
+ setOpen,
+ setValue,
+ open,
+ selectionRef,
+ typingRef,
+ valuesRef,
+ popupRef,
+ registerSelectedItem,
+ value,
+ } = useSelectRootContext();
+ const itemRef = React.useRef(null);
const selectedIndexRef = useLatestRef(selectedIndex);
const indexRef = useLatestRef(listItem.index);
+ const mergedRef = useForkRef(listItem.ref, forwardedRef, itemRef);
- const mergedRef = useForkRef(listItem.ref, forwardedRef);
+ const hasRegistered = listItem.index !== -1;
useEnhancedEffect(() => {
- if (listItem.index === -1) {
+ if (!hasRegistered) {
return undefined;
}
@@ -252,7 +264,13 @@ const SelectItem = React.forwardRef(function SelectItem(
return () => {
delete values[listItem.index];
};
- }, [listItem.index, valueProp, valuesRef]);
+ }, [hasRegistered, listItem.index, valueProp, valuesRef]);
+
+ useEnhancedEffect(() => {
+ if (hasRegistered && valueProp === value) {
+ registerSelectedItem(listItem.index);
+ }
+ }, [hasRegistered, listItem.index, registerSelectedItem, valueProp, value]);
const highlighted = activeIndex === listItem.index;
const selected = selectedIndex === listItem.index;
diff --git a/packages/react/src/select/popup/SelectPopup.test.tsx b/packages/react/src/select/popup/SelectPopup.test.tsx
index 7351110949..93907e1bb1 100644
--- a/packages/react/src/select/popup/SelectPopup.test.tsx
+++ b/packages/react/src/select/popup/SelectPopup.test.tsx
@@ -10,7 +10,9 @@ describe(' ', () => {
render(node) {
return render(
- {node}
+
+ {node}
+
,
);
},
diff --git a/packages/react/src/select/portal/SelectPortal.tsx b/packages/react/src/select/portal/SelectPortal.tsx
index 4d188f30da..0a1f0c5e6f 100644
--- a/packages/react/src/select/portal/SelectPortal.tsx
+++ b/packages/react/src/select/portal/SelectPortal.tsx
@@ -1,8 +1,9 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
-import { Portal } from '../../portal/Portal';
+import { FloatingPortal } from '@floating-ui/react';
import { HTMLElementType, refType } from '../../utils/proptypes';
+import { SelectPortalContext } from './SelectPortalContext';
/**
* A portal element that moves the popup to a different part of the DOM.
@@ -12,16 +13,22 @@ import { HTMLElementType, refType } from '../../utils/proptypes';
*/
function SelectPortal(props: SelectPortal.Props) {
const { children, container } = props;
+
return (
-
- {children}
-
+
+ {children}
+
);
}
namespace SelectPortal {
- export interface Props extends Omit {}
- export interface State extends Portal.State {}
+ export interface Props {
+ children?: React.ReactNode;
+ /**
+ * A parent element to render the portal element into.
+ */
+ container?: HTMLElement | null | React.RefObject;
+ }
}
SelectPortal.propTypes /* remove-proptypes */ = {
@@ -34,7 +41,7 @@ SelectPortal.propTypes /* remove-proptypes */ = {
*/
children: PropTypes.node,
/**
- * A parent element to render the portal into.
+ * A parent element to render the portal element into.
*/
container: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([HTMLElementType, refType]),
} as any;
diff --git a/packages/react/src/select/portal/SelectPortalContext.ts b/packages/react/src/select/portal/SelectPortalContext.ts
new file mode 100644
index 0000000000..1e8ac9d425
--- /dev/null
+++ b/packages/react/src/select/portal/SelectPortalContext.ts
@@ -0,0 +1,11 @@
+import * as React from 'react';
+
+export const SelectPortalContext = React.createContext(undefined);
+
+export function useSelectPortalContext() {
+ const value = React.useContext(SelectPortalContext);
+ if (value === undefined) {
+ throw new Error('Base UI: is missing.');
+ }
+ return value;
+}
diff --git a/packages/react/src/select/positioner/SelectPositioner.test.tsx b/packages/react/src/select/positioner/SelectPositioner.test.tsx
index ec9ab3fef2..ac2e00e865 100644
--- a/packages/react/src/select/positioner/SelectPositioner.test.tsx
+++ b/packages/react/src/select/positioner/SelectPositioner.test.tsx
@@ -8,7 +8,11 @@ describe(' ', () => {
describeConformance( , () => ({
refInstanceof: window.HTMLDivElement,
render(node) {
- return render({node} );
+ return render(
+
+ {node}
+ ,
+ );
},
}));
});
diff --git a/packages/react/src/select/positioner/useSelectPositioner.ts b/packages/react/src/select/positioner/useSelectPositioner.ts
index d672929ac6..e0aa0be52d 100644
--- a/packages/react/src/select/positioner/useSelectPositioner.ts
+++ b/packages/react/src/select/positioner/useSelectPositioner.ts
@@ -144,11 +144,6 @@ export namespace useSelectPositioner {
* @default 5
*/
collisionPadding?: Padding;
- /**
- * Whether to keep the HTML element in the DOM while the select menu is hidden.
- * @default true
- */
- keepMounted?: boolean;
/**
* Whether to maintain the select menu in the viewport after
* the anchor element is scrolled out of view.
diff --git a/packages/react/src/select/root/SelectRoot.test.tsx b/packages/react/src/select/root/SelectRoot.test.tsx
index 8110d4b23b..ced457dba4 100644
--- a/packages/react/src/select/root/SelectRoot.test.tsx
+++ b/packages/react/src/select/root/SelectRoot.test.tsx
@@ -6,6 +6,10 @@ import { expect } from 'chai';
import { spy } from 'sinon';
describe(' ', () => {
+ beforeEach(() => {
+ (globalThis as any).BASE_UI_ANIMATIONS_DISABLED = true;
+ });
+
const { render } = createRenderer();
describe('prop: defaultValue', () => {
@@ -15,12 +19,14 @@ describe(' ', () => {
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
,
);
@@ -44,12 +50,14 @@ describe(' ', () => {
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
,
);
@@ -71,12 +79,14 @@ describe(' ', () => {
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
,
);
@@ -120,12 +130,14 @@ describe(' ', () => {
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
);
}
@@ -153,12 +165,14 @@ describe(' ', () => {
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
,
);
@@ -174,12 +188,14 @@ describe(' ', () => {
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
);
}
@@ -209,9 +225,11 @@ describe(' ', () => {
setOpen(false)}>Close
-
-
-
+
+
+
+
+
);
@@ -266,12 +284,14 @@ describe(' ', () => {
setOpen(false)}>Close
-
-
-
+
+
+
+
+
);
@@ -287,8 +307,6 @@ describe(' ', () => {
});
expect(animationFinished).to.equal(true);
-
- (globalThis as any).BASE_UI_ANIMATIONS_DISABLED = true;
});
});
@@ -301,12 +319,14 @@ describe(' ', () => {
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
,
);
@@ -324,12 +344,14 @@ describe(' ', () => {
-
-
- a
- b
-
-
+
+
+
+ a
+ b
+
+
+
,
);
diff --git a/packages/react/src/select/root/SelectRoot.tsx b/packages/react/src/select/root/SelectRoot.tsx
index c7848d5f23..2f48d6498d 100644
--- a/packages/react/src/select/root/SelectRoot.tsx
+++ b/packages/react/src/select/root/SelectRoot.tsx
@@ -6,7 +6,6 @@ import { SelectRootContext } from './SelectRootContext';
import { SelectIndexContext } from './SelectIndexContext';
import { useFieldRootContext } from '../../field/root/FieldRootContext';
import { visuallyHidden } from '../../utils/visuallyHidden';
-import { PortalContext } from '../../portal/PortalContext';
/**
* Groups all parts of the select.
@@ -65,9 +64,7 @@ const SelectRoot: SelectRoot = function SelectRoot(
return (
-
- {props.children}
-
+ {props.children}
;
modal: boolean;
+ registerSelectedItem: (index: number) => void;
}
export const SelectRootContext = React.createContext(null);
diff --git a/packages/react/src/select/root/useSelectRoot.ts b/packages/react/src/select/root/useSelectRoot.ts
index ea66f29e15..c1c381eaa8 100644
--- a/packages/react/src/select/root/useSelectRoot.ts
+++ b/packages/react/src/select/root/useSelectRoot.ts
@@ -140,20 +140,31 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelect
setLabel(labelsRef.current[index] ?? '');
});
+ const hasRegisteredRef = React.useRef(false);
+
+ const registerSelectedItem = useEventCallback((suppliedIndex: number | undefined) => {
+ if (suppliedIndex !== undefined) {
+ hasRegisteredRef.current = true;
+ }
+
+ const stringValue = typeof value === 'string' || value === null ? value : JSON.stringify(value);
+ const index = suppliedIndex ?? valuesRef.current.indexOf(stringValue);
+
+ if (index !== -1) {
+ setSelectedIndex(index);
+ setLabel(labelsRef.current[index] ?? '');
+ } else if (value) {
+ warn(`The value \`${stringValue}\` is not present in the select items.`);
+ }
+ });
+
useEnhancedEffect(() => {
- // Wait for the items to have registered their values in `valuesRef`.
- queueMicrotask(() => {
- const stringValue =
- typeof value === 'string' || value === null ? value : JSON.stringify(value);
- const index = valuesRef.current.indexOf(stringValue);
- if (index !== -1) {
- setSelectedIndex(index);
- setLabel(labelsRef.current[index] ?? '');
- } else if (value) {
- warn(`The value \`${stringValue}\` is not present in the select items.`);
- }
- });
- }, [value]);
+ if (!hasRegisteredRef.current) {
+ return;
+ }
+
+ registerSelectedItem(undefined);
+ }, [value, registerSelectedItem]);
const floatingRootContext = useFloatingRootContext({
open,
@@ -263,6 +274,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelect
transitionStatus,
fieldControlValidation,
modal,
+ registerSelectedItem,
}),
[
id,
@@ -290,6 +302,7 @@ export function useSelectRoot(params: useSelectRoot.Parameters): useSelect
transitionStatus,
fieldControlValidation,
modal,
+ registerSelectedItem,
],
);
diff --git a/packages/react/src/tooltip/arrow/TooltipArrow.test.tsx b/packages/react/src/tooltip/arrow/TooltipArrow.test.tsx
index 0f29392280..5942263e56 100644
--- a/packages/react/src/tooltip/arrow/TooltipArrow.test.tsx
+++ b/packages/react/src/tooltip/arrow/TooltipArrow.test.tsx
@@ -10,9 +10,11 @@ describe(' ', () => {
render(node) {
return render(
-
- {node}
-
+
+
+ {node}
+
+
,
);
},
diff --git a/packages/react/src/tooltip/index.parts.ts b/packages/react/src/tooltip/index.parts.ts
index 9e850090ea..937a51b96f 100644
--- a/packages/react/src/tooltip/index.parts.ts
+++ b/packages/react/src/tooltip/index.parts.ts
@@ -1,6 +1,6 @@
export { TooltipRoot as Root } from './root/TooltipRoot';
export { TooltipTrigger as Trigger } from './trigger/TooltipTrigger';
-export { Portal } from '../portal/Portal';
+export { TooltipPortal as Portal } from './portal/TooltipPortal';
export { TooltipPositioner as Positioner } from './positioner/TooltipPositioner';
export { TooltipPopup as Popup } from './popup/TooltipPopup';
export { TooltipArrow as Arrow } from './arrow/TooltipArrow';
diff --git a/packages/react/src/tooltip/popup/TooltipPopup.test.tsx b/packages/react/src/tooltip/popup/TooltipPopup.test.tsx
index c1d4557795..7170bc7adc 100644
--- a/packages/react/src/tooltip/popup/TooltipPopup.test.tsx
+++ b/packages/react/src/tooltip/popup/TooltipPopup.test.tsx
@@ -12,7 +12,9 @@ describe(' ', () => {
render(node) {
return render(
- {node}
+
+ {node}
+
,
);
},
@@ -21,9 +23,11 @@ describe(' ', () => {
it('should render the children', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
diff --git a/packages/react/src/tooltip/portal/TooltipPortal.tsx b/packages/react/src/tooltip/portal/TooltipPortal.tsx
new file mode 100644
index 0000000000..02014be2f2
--- /dev/null
+++ b/packages/react/src/tooltip/portal/TooltipPortal.tsx
@@ -0,0 +1,67 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingPortal } from '@floating-ui/react';
+import { useTooltipRootContext } from '../root/TooltipRootContext';
+import { HTMLElementType, refType } from '../../utils/proptypes';
+import { TooltipPortalContext } from './TooltipPortalContext';
+
+/**
+ * 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 Tooltip](https://base-ui.com/react/components/tooltip)
+ */
+function TooltipPortal(props: TooltipPortal.Props) {
+ const { children, keepMounted = false, container } = props;
+
+ const { mounted } = useTooltipRootContext();
+
+ const shouldRender = mounted || keepMounted;
+ if (!shouldRender) {
+ return null;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+namespace TooltipPortal {
+ 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;
+ }
+}
+
+TooltipPortal.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 { TooltipPortal };
diff --git a/packages/react/src/tooltip/portal/TooltipPortalContext.ts b/packages/react/src/tooltip/portal/TooltipPortalContext.ts
new file mode 100644
index 0000000000..78c2456adc
--- /dev/null
+++ b/packages/react/src/tooltip/portal/TooltipPortalContext.ts
@@ -0,0 +1,11 @@
+import * as React from 'react';
+
+export const TooltipPortalContext = React.createContext(undefined);
+
+export function useTooltipPortalContext() {
+ const value = React.useContext(TooltipPortalContext);
+ if (value === undefined) {
+ throw new Error('Base UI: is missing.');
+ }
+ return value;
+}
diff --git a/packages/react/src/tooltip/positioner/TooltipPositioner.test.tsx b/packages/react/src/tooltip/positioner/TooltipPositioner.test.tsx
index 882fed0f25..fcf030421a 100644
--- a/packages/react/src/tooltip/positioner/TooltipPositioner.test.tsx
+++ b/packages/react/src/tooltip/positioner/TooltipPositioner.test.tsx
@@ -8,7 +8,11 @@ describe(' ', () => {
describeConformance( , () => ({
refInstanceof: window.HTMLDivElement,
render(node) {
- return render({node} );
+ return render(
+
+ {node}
+ ,
+ );
},
}));
});
diff --git a/packages/react/src/tooltip/positioner/TooltipPositioner.tsx b/packages/react/src/tooltip/positioner/TooltipPositioner.tsx
index 8d9b5e5eb9..4a08765820 100644
--- a/packages/react/src/tooltip/positioner/TooltipPositioner.tsx
+++ b/packages/react/src/tooltip/positioner/TooltipPositioner.tsx
@@ -10,6 +10,7 @@ import { useTooltipPositioner } from './useTooltipPositioner';
import type { BaseUIComponentProps } from '../../utils/types';
import type { Side, Align } from '../../utils/useAnchorPositioning';
import { popupStateMapping } from '../../utils/popupStateMapping';
+import { useTooltipPortalContext } from '../portal/TooltipPortalContext';
/**
* Positions the tooltip against the trigger.
@@ -25,7 +26,6 @@ const TooltipPositioner = React.forwardRef(function TooltipPositioner(
render,
className,
anchor,
- keepMounted = false,
positionMethod = 'absolute',
side = 'top',
align = 'center',
@@ -40,6 +40,7 @@ const TooltipPositioner = React.forwardRef(function TooltipPositioner(
const { open, setPositionerElement, mounted, floatingRootContext, trackCursorAxis } =
useTooltipRootContext();
+ const keepMounted = useTooltipPortalContext();
const positioner = useTooltipPositioner({
anchor,
@@ -47,7 +48,6 @@ const TooltipPositioner = React.forwardRef(function TooltipPositioner(
positionMethod,
open,
mounted,
- keepMounted,
side,
sideOffset,
align,
@@ -57,6 +57,7 @@ const TooltipPositioner = React.forwardRef(function TooltipPositioner(
sticky,
trackCursorAxis,
arrowPadding,
+ keepMounted,
});
const mergedRef = useForkRef(forwardedRef, setPositionerElement);
@@ -91,11 +92,6 @@ const TooltipPositioner = React.forwardRef(function TooltipPositioner(
customStyleHookMapping: popupStateMapping,
});
- const shouldRender = keepMounted || mounted;
- if (!shouldRender) {
- return null;
- }
-
return (
{renderElement()}
@@ -187,11 +183,6 @@ TooltipPositioner.propTypes /* remove-proptypes */ = {
top: PropTypes.number,
}),
]),
- /**
- * Whether to keep the HTML element in the DOM while the tooltip is hidden.
- * @default false
- */
- keepMounted: PropTypes.bool,
/**
* Determines which CSS `position` property to use.
* @default 'absolute'
diff --git a/packages/react/src/tooltip/positioner/useTooltipPositioner.ts b/packages/react/src/tooltip/positioner/useTooltipPositioner.ts
index 4bfa1e1a16..59ca294b7f 100644
--- a/packages/react/src/tooltip/positioner/useTooltipPositioner.ts
+++ b/packages/react/src/tooltip/positioner/useTooltipPositioner.ts
@@ -8,7 +8,7 @@ import { useTooltipRootContext } from '../root/TooltipRootContext';
export function useTooltipPositioner(
params: useTooltipPositioner.Parameters,
): useTooltipPositioner.ReturnValue {
- const { keepMounted, mounted } = params;
+ const { mounted } = params;
const { open, trackCursorAxis } = useTooltipRootContext();
@@ -27,7 +27,7 @@ export function useTooltipPositioner(
(externalProps = {}) => {
const hiddenStyles: React.CSSProperties = {};
- if (keepMounted && !open) {
+ if (!open) {
hiddenStyles.pointerEvents = 'none';
}
@@ -44,7 +44,7 @@ export function useTooltipPositioner(
},
});
},
- [keepMounted, open, trackCursorAxis, mounted, positionerStyles],
+ [open, trackCursorAxis, mounted, positionerStyles],
);
return React.useMemo(
@@ -134,11 +134,6 @@ export namespace useTooltipPositioner {
* @default 5
*/
arrowPadding?: number;
- /**
- * Whether to keep the HTML element in the DOM while the tooltip is hidden.
- * @default false
- */
- keepMounted?: boolean;
/**
* Whether the tooltip continuously tracks its anchor after the initial positioning upon
* mount.
@@ -157,6 +152,10 @@ export namespace useTooltipPositioner {
}
export interface Parameters extends SharedParameters {
+ /**
+ * Whether the portal is kept mounted in the DOM while the popup is closed.
+ */
+ keepMounted: boolean;
/**
* Whether the tooltip is mounted.
*/
diff --git a/packages/react/src/tooltip/provider/TooltipProvider.test.tsx b/packages/react/src/tooltip/provider/TooltipProvider.test.tsx
index feae3cc0be..8424bd0bf6 100644
--- a/packages/react/src/tooltip/provider/TooltipProvider.test.tsx
+++ b/packages/react/src/tooltip/provider/TooltipProvider.test.tsx
@@ -16,9 +16,11 @@ describe(' ', () => {
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -50,9 +52,11 @@ describe(' ', () => {
-
- Content
-
+
+
+ Content
+
+
,
);
diff --git a/packages/react/src/tooltip/root/TooltipRoot.test.tsx b/packages/react/src/tooltip/root/TooltipRoot.test.tsx
index 70c8c637ab..ed19f1c704 100644
--- a/packages/react/src/tooltip/root/TooltipRoot.test.tsx
+++ b/packages/react/src/tooltip/root/TooltipRoot.test.tsx
@@ -22,9 +22,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -45,9 +47,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -76,9 +80,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -95,9 +101,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -126,9 +134,11 @@ describe(' ', () => {
it('should open when controlled open is true', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -138,9 +148,11 @@ describe(' ', () => {
it('should close when controlled open is false', async () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -162,9 +174,11 @@ describe(' ', () => {
}}
>
-
- Content
-
+
+
+ Content
+
+
);
}
@@ -207,9 +221,11 @@ describe(' ', () => {
}}
>
-
- Content
-
+
+
+ Content
+
+
);
}
@@ -238,9 +254,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -253,9 +271,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -268,9 +288,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -283,9 +305,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -310,9 +334,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
@@ -340,9 +366,11 @@ describe(' ', () => {
await render(
-
- Content
-
+
+
+ Content
+
+
,
);
diff --git a/packages/react/src/tooltip/root/TooltipRoot.tsx b/packages/react/src/tooltip/root/TooltipRoot.tsx
index ed29bc7304..6f5789f710 100644
--- a/packages/react/src/tooltip/root/TooltipRoot.tsx
+++ b/packages/react/src/tooltip/root/TooltipRoot.tsx
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import { TooltipRootContext } from './TooltipRootContext';
import { useTooltipRoot } from './useTooltipRoot';
import { OPEN_DELAY } from '../utils/constants';
-import { PortalContext } from '../../portal/PortalContext';
/**
* Groups all parts of the tooltip.
@@ -82,9 +81,7 @@ const TooltipRoot: React.FC = function TooltipRoot(props) {
);
return (
-
- {props.children}
-
+ {props.children}
);
};
diff --git a/packages/react/src/utils/useAnchorPositioning.ts b/packages/react/src/utils/useAnchorPositioning.ts
index 5fc6b328c6..a3c51b09e7 100644
--- a/packages/react/src/utils/useAnchorPositioning.ts
+++ b/packages/react/src/utils/useAnchorPositioning.ts
@@ -42,7 +42,7 @@ interface UseAnchorPositioningParameters {
collisionBoundary?: Boundary;
collisionPadding?: Padding;
sticky?: boolean;
- keepMounted?: boolean;
+ keepMounted: boolean;
arrowPadding?: number;
floatingRootContext?: FloatingRootContext;
mounted: boolean;
From 7b415f17a5ef8a297ae495265449f68ca959c12e Mon Sep 17 00:00:00 2001
From: atomiks
Date: Tue, 24 Dec 2024 17:39:02 +1100
Subject: [PATCH 2/4] Fix experiment
---
.../experiments/popups-in-popups.tsx | 250 ++++++++++--------
1 file changed, 135 insertions(+), 115 deletions(-)
diff --git a/docs/src/app/(private)/experiments/popups-in-popups.tsx b/docs/src/app/(private)/experiments/popups-in-popups.tsx
index 8568edcea1..1d4383da86 100644
--- a/docs/src/app/(private)/experiments/popups-in-popups.tsx
+++ b/docs/src/app/(private)/experiments/popups-in-popups.tsx
@@ -66,22 +66,24 @@ function SelectDemo({ modal }: Props) {
- }>
-
-
- } />
- System font
-
-
- } />
- Arial
-
-
- } />
- Roboto
-
-
-
+
+ }>
+
+
+ } />
+ System font
+
+
+ } />
+ Arial
+
+
+ } />
+ Roboto
+
+
+
+
);
}
@@ -96,105 +98,123 @@ 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
+
+
+
+
);
}
From 2d41b5efddb88ee66183c209714804c91d69be03 Mon Sep 17 00:00:00 2001
From: atomiks
Date: Tue, 24 Dec 2024 17:56:44 +1100
Subject: [PATCH 3/4] Remove conditional returns in backdrops
---
packages/react/src/dialog/backdrop/DialogBackdrop.tsx | 2 +-
packages/react/src/popover/backdrop/PopoverBackdrop.tsx | 4 ----
.../react/src/preview-card/backdrop/PreviewCardBackdrop.tsx | 4 ----
packages/react/src/select/backdrop/SelectBackdrop.tsx | 4 ----
4 files changed, 1 insertion(+), 13 deletions(-)
diff --git a/packages/react/src/dialog/backdrop/DialogBackdrop.tsx b/packages/react/src/dialog/backdrop/DialogBackdrop.tsx
index f5961a54cd..c224c729d2 100644
--- a/packages/react/src/dialog/backdrop/DialogBackdrop.tsx
+++ b/packages/react/src/dialog/backdrop/DialogBackdrop.tsx
@@ -45,7 +45,7 @@ const DialogBackdrop = React.forwardRef(function DialogBackdrop(
});
// no need to render nested backdrops
- const shouldRender = mounted && !nested;
+ const shouldRender = !nested;
if (!shouldRender) {
return null;
}
diff --git a/packages/react/src/popover/backdrop/PopoverBackdrop.tsx b/packages/react/src/popover/backdrop/PopoverBackdrop.tsx
index b530a178c3..1f2128bc10 100644
--- a/packages/react/src/popover/backdrop/PopoverBackdrop.tsx
+++ b/packages/react/src/popover/backdrop/PopoverBackdrop.tsx
@@ -45,10 +45,6 @@ const PopoverBackdrop = React.forwardRef(function PopoverBackdrop(
customStyleHookMapping,
});
- if (!mounted) {
- return null;
- }
-
return renderElement();
});
diff --git a/packages/react/src/preview-card/backdrop/PreviewCardBackdrop.tsx b/packages/react/src/preview-card/backdrop/PreviewCardBackdrop.tsx
index 4163ab4f9f..947e2a658e 100644
--- a/packages/react/src/preview-card/backdrop/PreviewCardBackdrop.tsx
+++ b/packages/react/src/preview-card/backdrop/PreviewCardBackdrop.tsx
@@ -45,10 +45,6 @@ const PreviewCardBackdrop = React.forwardRef(function PreviewCardBackdrop(
customStyleHookMapping,
});
- if (!mounted) {
- return null;
- }
-
return renderElement();
});
diff --git a/packages/react/src/select/backdrop/SelectBackdrop.tsx b/packages/react/src/select/backdrop/SelectBackdrop.tsx
index 3dce768d1d..ef7dcdea37 100644
--- a/packages/react/src/select/backdrop/SelectBackdrop.tsx
+++ b/packages/react/src/select/backdrop/SelectBackdrop.tsx
@@ -42,10 +42,6 @@ const SelectBackdrop = React.forwardRef(function SelectBackdrop(
customStyleHookMapping,
});
- if (!mounted) {
- return null;
- }
-
return renderElement();
});
From 9a0860a9819734ec48c04a2208118e560a334b7b Mon Sep 17 00:00:00 2001
From: atomiks
Date: Fri, 27 Dec 2024 15:08:27 +1100
Subject: [PATCH 4/4] [POC] Unmount ref
---
docs/package.json | 2 +-
docs/reference/generated/popover-root.json | 4 +
.../experiments/collapsible-framer.tsx | 2 +-
docs/src/app/(private)/experiments/motion.tsx | 106 +++++++++++++++
.../src/app/(private)/experiments/tooltip.tsx | 2 +-
.../react/handbook/animation/page.mdx | 125 +++++++++++++++++-
.../react/src/popover/root/PopoverRoot.tsx | 11 +-
.../react/src/popover/root/usePopoverRoot.ts | 17 ++-
.../react/src/utils/useAfterExitAnimation.tsx | 13 +-
pnpm-lock.yaml | 51 +++++--
10 files changed, 311 insertions(+), 22 deletions(-)
create mode 100644 docs/src/app/(private)/experiments/motion.tsx
diff --git a/docs/package.json b/docs/package.json
index 62910c3523..ab0e070da2 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -87,9 +87,9 @@
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@types/unist": "^3.0.3",
"chai": "^4.5.0",
- "framer-motion": "^11.12.0",
"fs-extra": "^11.2.0",
"mdast-util-mdx-jsx": "^3.1.3",
+ "motion": "^11.15.0",
"prettier": "^3.4.1",
"rimraf": "^5.0.10",
"serve": "^14.2.4",
diff --git a/docs/reference/generated/popover-root.json b/docs/reference/generated/popover-root.json
index 6b3efcddcc..6a14df1a02 100644
--- a/docs/reference/generated/popover-root.json
+++ b/docs/reference/generated/popover-root.json
@@ -15,6 +15,10 @@
"type": "(open, event, reason) => void",
"description": "Event handler called when the popover is opened or closed."
},
+ "unmountRef": {
+ "type": "{ current: { unmount: func } }",
+ "description": "A ref to manually unmount the popover."
+ },
"openOnHover": {
"type": "boolean",
"default": "false",
diff --git a/docs/src/app/(private)/experiments/collapsible-framer.tsx b/docs/src/app/(private)/experiments/collapsible-framer.tsx
index 64f032aedd..f876c569c1 100644
--- a/docs/src/app/(private)/experiments/collapsible-framer.tsx
+++ b/docs/src/app/(private)/experiments/collapsible-framer.tsx
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import { Collapsible } from '@base-ui-components/react/collapsible';
-import { motion } from 'framer-motion';
+import { motion } from 'motion/react';
import c from './collapsible.module.css';
export default function CollapsibleFramer() {
diff --git a/docs/src/app/(private)/experiments/motion.tsx b/docs/src/app/(private)/experiments/motion.tsx
new file mode 100644
index 0000000000..dcabcbed32
--- /dev/null
+++ b/docs/src/app/(private)/experiments/motion.tsx
@@ -0,0 +1,106 @@
+'use client';
+import * as React from 'react';
+import { Popover } from '@base-ui-components/react/popover';
+import { motion, AnimatePresence } from 'motion/react';
+
+function ConditionallyMounted() {
+ const [open, setOpen] = React.useState(false);
+ return (
+
+ Trigger
+
+ {open && (
+
+
+
+ }
+ >
+ Popup
+
+
+
+ )}
+
+
+ );
+}
+
+function AlwaysMounted() {
+ const [open, setOpen] = React.useState(false);
+ return (
+
+ Trigger
+
+
+
+ }
+ >
+ Popup
+
+
+
+
+ );
+}
+
+function NoOpacity() {
+ const [open, setOpen] = React.useState(false);
+ const unmountRef = React.useRef({ unmount: () => {} });
+
+ return (
+
+ Trigger
+
+ {open && (
+
+
+ {
+ if (!open) {
+ unmountRef.current.unmount();
+ }
+ }}
+ />
+ }
+ >
+ Popup
+
+
+
+ )}
+
+
+ );
+}
+
+export default function Page() {
+ return (
+
+
Conditionally mounted
+
+
Always mounted
+
+
No opacity
+
+
+ );
+}
diff --git a/docs/src/app/(private)/experiments/tooltip.tsx b/docs/src/app/(private)/experiments/tooltip.tsx
index e205e70991..9c77b8d5ce 100644
--- a/docs/src/app/(private)/experiments/tooltip.tsx
+++ b/docs/src/app/(private)/experiments/tooltip.tsx
@@ -2,7 +2,7 @@
import * as React from 'react';
import { Tooltip } from '@base-ui-components/react/tooltip';
import { styled, keyframes } from '@mui/system';
-import { motion, AnimatePresence } from 'framer-motion';
+import { motion, AnimatePresence } from 'motion/react';
const scaleIn = keyframes`
from {
diff --git a/docs/src/app/(public)/(content)/react/handbook/animation/page.mdx b/docs/src/app/(public)/(content)/react/handbook/animation/page.mdx
index 792148415a..f61a979ab5 100644
--- a/docs/src/app/(public)/(content)/react/handbook/animation/page.mdx
+++ b/docs/src/app/(public)/(content)/react/handbook/animation/page.mdx
@@ -74,5 +74,126 @@ Use the following Base UI attributes for creating CSS animations when a compone
## JavaScript animations
-JavaScript animation libraries such as [Motion](https://motion.dev) require control of the mounting and unmounting lifecycle of components.
-Most Base UI components are unmounted when hidden. These components usually provide the `keepMounted` prop to allow JavaScript animation libraries to take control.
+JavaScript animation libraries such as [Motion](https://motion.dev) require control of the mounting and unmounting lifecycle of components in order for exit animations to play.
+
+Base UI relies on [`element.getAnimations()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getAnimations) to detect if animations have finished on an element.
+When using Motion, the `opacity` property lets this detection work easily, so always animating `opacity` to a new value for exit animations will work.
+If it shouldn't be animated, you can use a value close to `1`, such as `opacity: 0.9999`.
+
+### Elements removed from the DOM when closed
+
+Most components like Popover are unmounted from the DOM when they are closed. To animate them:
+
+- Make the component controlled with the `open` prop so `AnimatePresence` can see the state as a child
+- Specify `keepMounted` on the `Portal` part
+- Use the `render` prop to compose the `Popup` with `motion.div`
+
+```jsx title="animated-popover.tsx" {11-17} "keepMounted"
+function App() {
+ const [open, setOpen] = React.useState(false);
+ return (
+
+ Trigger
+
+ {open && (
+
+
+
+ }
+ >
+ Popup
+
+
+
+ )}
+
+
+ );
+}
+```
+
+### Elements kept in the DOM when closed
+
+The `Select` component must be kept mounted in the DOM even when closed. In this case, a
+different approach is needed to animate it with Motion.
+
+- Make the component controlled with the `open` prop
+- Use the `render` prop to compose the `Popup` with `motion.div`
+- Animate the properties based on the `open` state, avoiding `AnimatePresence`
+
+```jsx title="animated-select.tsx" {11-19}
+function App() {
+ const [open, setOpen] = React.useState(false);
+ return (
+
+
+
+
+
+
+
+ }
+ >
+ Popup
+
+
+
+
+ );
+}
+```
+
+### Manual unmounting
+
+For full control, you can manually unmount the component when it's closed once animations have finished using an `unmountRef` passed to the `Root`:
+
+```jsx title="manual-unmount.tsx" "unmountRef"
+function App() {
+ const [open, setOpen] = React.useState(false);
+ const unmountRef = React.useRef({ unmount: () => {} });
+
+ return (
+
+ Trigger
+
+ {open && (
+
+
+ {
+ if (!open) {
+ unmountRef.current.unmount();
+ }
+ }}
+ />
+ }
+ >
+ Popup
+
+
+
+ )}
+
+
+ );
+}
+```
diff --git a/packages/react/src/popover/root/PopoverRoot.tsx b/packages/react/src/popover/root/PopoverRoot.tsx
index 9fedbbf161..6231c11370 100644
--- a/packages/react/src/popover/root/PopoverRoot.tsx
+++ b/packages/react/src/popover/root/PopoverRoot.tsx
@@ -12,7 +12,7 @@ import { OPEN_DELAY } from '../utils/constants';
* Documentation: [Base UI Popover](https://base-ui.com/react/components/popover)
*/
const PopoverRoot: React.FC = function PopoverRoot(props) {
- const { openOnHover = false, delay, closeDelay = 0 } = props;
+ const { openOnHover = false, delay, closeDelay = 0, unmountRef } = props;
const delayWithDefault = delay ?? OPEN_DELAY;
@@ -43,6 +43,7 @@ const PopoverRoot: React.FC = function PopoverRoot(props) {
open: props.open,
onOpenChange: props.onOpenChange,
defaultOpen: props.defaultOpen,
+ unmountRef,
});
const contextValue: PopoverRootContext = React.useMemo(
@@ -153,6 +154,14 @@ PopoverRoot.propTypes /* remove-proptypes */ = {
* @default false
*/
openOnHover: PropTypes.bool,
+ /**
+ * A ref to manually unmount the popover.
+ */
+ unmountRef: PropTypes.shape({
+ current: PropTypes.shape({
+ unmount: PropTypes.func.isRequired,
+ }).isRequired,
+ }),
} as any;
export { PopoverRoot };
diff --git a/packages/react/src/popover/root/usePopoverRoot.ts b/packages/react/src/popover/root/usePopoverRoot.ts
index 02ecfe22f5..a972418305 100644
--- a/packages/react/src/popover/root/usePopoverRoot.ts
+++ b/packages/react/src/popover/root/usePopoverRoot.ts
@@ -75,15 +75,20 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo
},
);
+ const handleUnmount = useEventCallback(() => {
+ setMounted(false);
+ setOpenReason(null);
+ });
+
useAfterExitAnimation({
+ enabled: !params.unmountRef,
open,
animatedElementRef: popupRef,
- onFinished: () => {
- setMounted(false);
- setOpenReason(null);
- },
+ onFinished: handleUnmount,
});
+ React.useImperativeHandle(params.unmountRef, () => ({ unmount: handleUnmount }), [handleUnmount]);
+
React.useEffect(() => {
return () => {
clearTimeout(clickEnabledTimeoutRef.current);
@@ -229,6 +234,10 @@ export namespace usePopoverRoot {
* @default 0
*/
closeDelay?: number;
+ /**
+ * A ref to manually unmount the popover.
+ */
+ unmountRef?: React.RefObject<{ unmount: () => void }>;
}
export interface ReturnValue {
diff --git a/packages/react/src/utils/useAfterExitAnimation.tsx b/packages/react/src/utils/useAfterExitAnimation.tsx
index 4ad5f1f3e8..5843965fd0 100644
--- a/packages/react/src/utils/useAfterExitAnimation.tsx
+++ b/packages/react/src/utils/useAfterExitAnimation.tsx
@@ -1,5 +1,5 @@
+import * as React from 'react';
import { useAnimationsFinished } from './useAnimationsFinished';
-import { useEnhancedEffect } from './useEnhancedEffect';
import { useEventCallback } from './useEventCallback';
import { useLatestRef } from './useLatestRef';
@@ -8,13 +8,17 @@ import { useLatestRef } from './useLatestRef';
* Useful for unmounting the component after animating out.
*/
export function useAfterExitAnimation(parameters: useAfterExitAnimation.Parameters) {
- const { open, animatedElementRef, onFinished: onFinishedParam } = parameters;
+ const { enabled = true, open, animatedElementRef, onFinished: onFinishedParam } = parameters;
const onFinished = useEventCallback(onFinishedParam);
const runOnceAnimationsFinish = useAnimationsFinished(animatedElementRef);
const openRef = useLatestRef(open);
- useEnhancedEffect(() => {
+ React.useEffect(() => {
+ if (!enabled) {
+ return;
+ }
+
function callOnFinished() {
if (!openRef.current) {
onFinished();
@@ -24,11 +28,12 @@ export function useAfterExitAnimation(parameters: useAfterExitAnimation.Paramete
if (!open) {
runOnceAnimationsFinish(callOnFinished);
}
- }, [open, openRef, runOnceAnimationsFinish, onFinished]);
+ }, [enabled, open, openRef, runOnceAnimationsFinish, onFinished]);
}
export namespace useAfterExitAnimation {
export interface Parameters {
+ enabled?: boolean;
/**
* Determines if the component is open.
* The logic runs when the component goes from open to closed.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9165f640a6..cbec199fba 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -576,15 +576,15 @@ importers:
chai:
specifier: ^4.5.0
version: 4.5.0
- framer-motion:
- specifier: ^11.12.0
- version: 11.12.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0-rc-fb9a90fa48-20240614(react@19.0.0-rc-fb9a90fa48-20240614))(react@19.0.0-rc-fb9a90fa48-20240614)
fs-extra:
specifier: ^11.2.0
version: 11.2.0
mdast-util-mdx-jsx:
specifier: ^3.1.3
version: 3.1.3
+ motion:
+ specifier: ^11.15.0
+ version: 11.15.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0-rc-fb9a90fa48-20240614(react@19.0.0-rc-fb9a90fa48-20240614))(react@19.0.0-rc-fb9a90fa48-20240614)
prettier:
specifier: ^3.4.1
version: 3.4.1
@@ -5376,12 +5376,12 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
- framer-motion@11.12.0:
- resolution: {integrity: sha512-gZaZeqFM6pX9kMVti60hYAa75jGpSsGYWAHbBfIkuHN7DkVHVkxSxeNYnrGmHuM0zPkWTzQx10ZT+fDjn7N4SA==}
+ framer-motion@11.15.0:
+ resolution: {integrity: sha512-MLk8IvZntxOMg7lDBLw2qgTHHv664bYoYmnFTmE0Gm/FW67aOJk0WM3ctMcG+Xhcv+vh5uyyXwxvxhSeJzSe+w==}
peerDependencies:
'@emotion/is-prop-valid': '*'
- react: ^18.0.0
- react-dom: ^18.0.0
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
@@ -7128,6 +7128,26 @@ packages:
resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==}
engines: {node: '>=0.10.0'}
+ motion-dom@11.14.3:
+ resolution: {integrity: sha512-lW+D2wBy5vxLJi6aCP0xyxTxlTfiu+b+zcpVbGVFUxotwThqhdpPRSmX8xztAgtZMPMeU0WGVn/k1w4I+TbPqA==}
+
+ motion-utils@11.14.3:
+ resolution: {integrity: sha512-Xg+8xnqIJTpr0L/cidfTTBFkvRw26ZtGGuIhA94J9PQ2p4mEa06Xx7QVYZH0BP+EpMSaDlu+q0I0mmvwADPsaQ==}
+
+ motion@11.15.0:
+ resolution: {integrity: sha512-iZ7dwADQJWGsqsSkBhNHdI2LyYWU+hA1Nhy357wCLZq1yHxGImgt3l7Yv0HT/WOskcYDq9nxdedyl4zUv7UFFw==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -15270,8 +15290,10 @@ snapshots:
forwarded@0.2.0: {}
- framer-motion@11.12.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0-rc-fb9a90fa48-20240614(react@19.0.0-rc-fb9a90fa48-20240614))(react@19.0.0-rc-fb9a90fa48-20240614):
+ framer-motion@11.15.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0-rc-fb9a90fa48-20240614(react@19.0.0-rc-fb9a90fa48-20240614))(react@19.0.0-rc-fb9a90fa48-20240614):
dependencies:
+ motion-dom: 11.14.3
+ motion-utils: 11.14.3
tslib: 2.6.2
optionalDependencies:
'@emotion/is-prop-valid': 1.3.0
@@ -17559,6 +17581,19 @@ snapshots:
modify-values@1.0.1: {}
+ motion-dom@11.14.3: {}
+
+ motion-utils@11.14.3: {}
+
+ motion@11.15.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0-rc-fb9a90fa48-20240614(react@19.0.0-rc-fb9a90fa48-20240614))(react@19.0.0-rc-fb9a90fa48-20240614):
+ dependencies:
+ framer-motion: 11.15.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0-rc-fb9a90fa48-20240614(react@19.0.0-rc-fb9a90fa48-20240614))(react@19.0.0-rc-fb9a90fa48-20240614)
+ tslib: 2.6.2
+ optionalDependencies:
+ '@emotion/is-prop-valid': 1.3.0
+ react: 19.0.0-rc-fb9a90fa48-20240614
+ react-dom: 19.0.0-rc-fb9a90fa48-20240614(react@19.0.0-rc-fb9a90fa48-20240614)
+
mri@1.2.0: {}
mrmime@2.0.0: {}