diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js index c163b7f674..1a51f52522 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.js @@ -8,10 +8,12 @@ export default function UnstyledPopoverIntroduction() { Anchor - - Popover - - + + + Popover + + + ); } diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx index c163b7f674..1a51f52522 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx @@ -8,10 +8,12 @@ export default function UnstyledPopoverIntroduction() { Anchor - - Popover - - + + + Popover + + + ); } diff --git a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview index fde17f49ec..dc07d39951 100644 --- a/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview +++ b/docs/data/base/components/popover/UnstyledPopoverIntroduction/system/index.tsx.preview @@ -2,8 +2,10 @@ Anchor - - Popover - - + + + Popover + + + \ No newline at end of file diff --git a/docs/data/base/components/popover/popover.md b/docs/data/base/components/popover/popover.md index c599586d75..5873ede3d4 100644 --- a/docs/data/base/components/popover/popover.md +++ b/docs/data/base/components/popover/popover.md @@ -1,16 +1,16 @@ --- productId: base-ui title: React Popover Component -components: PopoverRoot, PopoverTrigger, PopoverPositioner, PopoverPopup, PopoverArrow, PopoverBackdrop, PopoverGroup +components: PopoverProvider, PopoverRoot, PopoverTrigger, PopoverPositioner, PopoverPopup, PopoverArrow, PopoverBackdrop hooks: usePopoverRoot, usePopoverPositioner githubLabel: 'component: popover' --- # Popover -

Popovers are interactive anchored dialogs.

+

Popovers are interactive anchored dialogs that open on click by default, or hover.

-{{"component": "modules/components/ComponentLinkHeader.js", "design": false}} +{{"component": "@mui/docs/ComponentLinkHeader", "design": false}} {{"component": "modules/components/ComponentPageTabs.js"}} @@ -48,84 +48,83 @@ import * as Popover from '@base_ui/react/Popover'; Popover is implemented using a collection of related components: -- `` is a top-level component that wraps all other components. -- `` contains the trigger element. -- `` contains the popover content. +- `` wraps around `` or a group of ``s. +- `` is a top-level component that wraps the other components. +- `` renders the trigger element. +- `` renders the popover's positioning element. +- `` renders the popover popup itself. - `` renders an optional pointing arrow, placed inside the popup. ```tsx - - - - - - - Popover - - + + + + + + + + + + +``` + +## Provider + +`Popover.Provider` provides a shared delay for hoverable popovers so that once a popover is shown, the rest of the popovers in the group don't wait for the delay before showing. You can wrap this globally, or around an individual group of hoverable popovers anywhere in your React tree (or both). + +```tsx + + + ``` ## Placement -By default, the popover is placed on the top side of its anchor. To change this, use the `side` prop on `Popover.Popup`: +By default, the popover is placed on the top side of its trigger, the default anchor. To change this, use the `side` prop: ```jsx - - - - Popover + + + Popover + ``` You can also change the alignment of the popover in relation to its anchor. By default, it is centered, but it can be aligned to an edge of the anchor using the `alignment` prop: ```jsx - - Popover - + + Popover + ``` -Possible alignment values are `center`, `start`, and `end`. The latter two are logical values that adapt to the writing direction (LTR or RTL). - Due to collision detection, the popover may change its placement to avoid overflow. Therefore, your explicitly specified `side` and `alignment` props act as "ideal", or preferred, values. -To access the true rendered values, which may change as the result of a collision, the content element receives data attributes: +To access the true rendered values, which may change as the result of a collision, the popup element receives data attributes: ```jsx -// Rendered HTML -
- Content +// Rendered HTML (simplified) +
+
+ Popover +
``` This allows you to conditionally style the popover based on its rendered side or alignment. -## Offsets - -### Side +## Offset -To offset the side position, use the `sideOffset` prop: +The `sideOffset` prop creates a gap between the anchor and popover popup, while `alignmentOffset` slides the popover popup from its alignment, acting logically for `start` and `end` alignments. ```jsx - + ``` -This creates a gap between the anchor and its popover content. - -### Alignment - -To offset the alignment position, use the `alignmentOffset` prop: - -```jsx - -``` - -This prop acts logically for the `start` and `end` alignments. - ## Hover -To create an infotip, which is a tooltip-like popover that is anchored to an icon that may contain interactive content, add the `openOnHover` prop: +To open the popover on hover instead of click for pointer users, which enables creating tooltip-like popovers that may contain interactive content, add the `openOnHover` prop: ```jsx @@ -133,28 +132,26 @@ To create an infotip, which is a tooltip-like popover that is anchored to an ico ### Delay -By default, a popover that can open on hover waits until the user's cursor is at rest over the anchor element before it is opened. To change this timeout, use the `delay` prop, which represents how long the popover waits after the cursor rests to open in milliseconds: +To change how long the popover waits until it opens or closes when `openOnHover` is enabled, use the `delay` and `closeDelay` props, which represent how long the poover waits after the cursor rests on the trigger to open, or moves away from the trigger to close, in milliseconds: ```jsx - + ``` -The close delay can also be configured: +The delay type can be changed from `"rest"` (user's cursor is static over the trigger for the given timeout in milliseconds) to `"hover"` (the user's cursor has entered the trigger): ```jsx - + ``` -The delay type can be changed from `"rest"` (user's cursor is static for the given timeout in milliseconds) to `"hover"`: +### Cursor following + +The popover can follow the cursor on both axes or one axis using the `followCursorAxis` prop when `openOnHover` is enabled. Possible values are: `none` (default), `both`, `x`, or `y`. ```jsx - + ``` -### Grouping - -To ensure nearby trigger elements' delays become `0` once one of the popovers of the group opens, use the `Popover.Group` component, wrapping the `Popover.Root`s with it. - ## Controlled To control the popover with external state, use the `open` and `onOpenChange` props: @@ -170,54 +167,54 @@ function App() { } ``` -## Default open - -To show the popover initially while leaving it uncontrolled, use the `defaultOpen` prop: - -```jsx - -``` - ## Arrow To add an arrow (caret or triangle) inside the popover content that points toward the center of the anchor element, use the `Popover.Arrow` component: ```js - - - Popover - + + + + Popover + + ``` It automatically positions a wrapper element that can be styled or contain a custom SVG shape. -## Cursor following +## Anchoring -The popover can follow the cursor on both axes or one axis using the `followCursorAxis` prop on `Popover.Popup`. Possible values are: `none` (default), `both`, `x`, or `y`. +By default, the `Trigger` acts as the anchor, but this can be changed to another element. -## Anchoring +- A DOM element (stored in React state): + +```jsx + +``` -By default, the `Trigger` acts as the anchor. This can be changed to another element, either virtual or real: +- A React ref: -```js - +``` + +- A virtual element object, consisting of a `getBoundingClientRect` method and an optional `contextElement` property: + +```jsx + DOMRect, - contextElement: domElement, // optional + // `contextElement` is an optional but recommended property when `getBoundingClientRect` is + // derived from a real element, to ensure collision detection and position updates work as + // expected in certain DOM trees. + contextElement: domNode, }} > - Popover - ``` ## Styling -The `Popover.Popup` element receives the following CSS variables: +The `Popover.Positioner` element receives the following CSS variables, which can be used by `Popover.Popup`: - `--anchor-width`: Specifies the width of the anchor element. You can use this to match the width of the popover with its anchor. - `--anchor-height`: Specifies the height of the anchor element. You can use this to match the height of the popover with its anchor. @@ -225,31 +222,19 @@ The `Popover.Popup` element receives the following CSS variables: - `--available-height`: Specifies the available height of the popup before it overflows the viewport. - `--transform-origin`: Specifies the origin of the popup element that represents the point of the anchor element's center. When animating scale, this allows it to correctly emanate from the center of the anchor. -```jsx - - Content - -``` - -By default, `maxWidth` and `maxHeight` are already specified using `--available-{width,height}` to prevent the popover from being too big to fit on the screen. +By default, `maxWidth` and `maxHeight` are already specified on the positioner using `--available-{width,height}` to prevent the popover from being too big to fit on the screen. ## Animations -CSS transitions or animations can be used to animate the popover opening or closing. +The popover can animate when opening or closing with either: -`Popover.Popup` receives a `data-status` attribute in one of four states: +- CSS transitions +- CSS animations +- JavaScript animations -- `unmounted`, indicating the popover is not mounted on the DOM. -- `initial`, indicating the popover has been inserted into the DOM. -- `opening`, indicating the popover is transitioning into the open state, immediately after insertion. -- `closing`, indicating the popover is transitioning into the closed state. +### CSS transitions -Here is an example of how to apply a symmetric scale and fade transition: +Here is an example of how to apply a symmetric scale and fade transition with the default conditionally-rendered behavior: ```jsx Popover @@ -257,20 +242,54 @@ Here is an example of how to apply a symmetric scale and fade transition: ```css .PopoverPopup { + transform-origin: var(--transform-origin); transition-property: opacity, transform; transition-duration: 0.2s; + /* Represents the final styles once exited */ opacity: 0; transform: scale(0.9); - transform-origin: var(--transform-origin); } -.PopoverPopup[data-status='opening'] { +/* Represents the final styles once entered */ +.PopoverPopup[data-state='open'] { opacity: 1; transform: scale(1); } + +/* Represents the initial styles when entering */ +.PopoverPopup[data-entering] { + opacity: 0; + transform: scale(0.9); +} ``` -CSS animations can also be used—useful for more complex animations with differing property durations: +Styles need to be applied in three states: + +- The exiting styles, placed on the base element class +- The open styles, placed on the base element class with `[data-state="open"]` +- The entering styles, placed on the base element class with `[data-entering]` + +In newer browsers, there is a feature called `@starting-style` which allows transitions to occur on open for conditionally-mounted components: + +```css +/* Base UI API - Polyfill */ +.PopoverPopup[data-entering] { + opacity: 0; + transform: scale(0.9); +} + +/* Official Browser API - no Firefox support as of May 2024 */ +@starting-style { + .PopoverPopup[data-state='open'] { + opacity: 0; + transform: scale(0.9); + } +} +``` + +### CSS animations + +CSS animations can also be used, requiring only two separate declarations: ```css @keyframes scale-in { @@ -288,14 +307,55 @@ CSS animations can also be used—useful for more complex animations with differ } .PopoverPopup { - animation: scale-in 0.2s; + animation: scale-in 0.2s forwards; } -.PopoverPopup[data-status='closing'] { +.PopoverPopup[data-exiting] { animation: scale-out 0.2s forwards; } ``` +### JavaScript animations + +The `keepMounted` prop lets an external library control the mounting, for example `framer-motion`'s `AnimatePresence` component. + +```js +function App() { + const [open, setOpen] = useState(false); + return ( + + Trigger + + {open && ( + + + } + > + Popover + + + )} + + + ); +} +``` + +### Animation states + +Four states are available as data attributes to animate the popup, which enables full control depending on whether the popup is being animated with CSS transitions or animations, JavaScript, or is using the `keepMounted` prop. + +- `[data-state="open"]` - `open` state is `true`. +- `[data-state="closed"]` - `open` state is `false`. Can still be mounted to the DOM if closing. +- `[data-entering]` - the popup was just inserted to the DOM. The attribute is removed 1 animation frame later. Enables "starting styles" upon insertion for conditional rendering. +- `[data-exiting]` - the popup is in the process of being removed from the DOM, but is still mounted. + ### Instant animation Animations can be removed under certain conditions using the `data-instant` attribute on `Popover.Popup`. This attribute can be used unconditionally, but it also has different values for granular checks: diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js index e0dc130eeb..9b8662417c 100644 --- a/docs/data/base/pagesApi.js +++ b/docs/data/base/pagesApi.js @@ -82,18 +82,18 @@ module.exports = [ pathname: '/base-ui/react-popover/components-api/#popover-backdrop', title: 'PopoverBackdrop', }, - { - pathname: '/base-ui/react-popover/components-api/#popover-group', - title: 'PopoverGroup', - }, { pathname: '/base-ui/react-popover/components-api/#popover-popup', title: 'PopoverPopup', }, { - pathname: '/base-ui/react-popover/components-api/#popover-popup-root', + pathname: '/base-ui/react-popover/components-api/#popover-positioner', title: 'PopoverPositioner', }, + { + pathname: '/base-ui/react-popover/components-api/#popover-provider', + title: 'PopoverProvider', + }, { pathname: '/base-ui/react-popover/components-api/#popover-root', title: 'PopoverRoot', @@ -172,7 +172,7 @@ module.exports = [ title: 'useOptionContextStabilizer', }, { - pathname: '/base-ui/react-popover/hooks-api/#use-popover-popup', + pathname: '/base-ui/react-popover/hooks-api/#use-popover-positioner', title: 'usePopoverPositioner', }, { diff --git a/docs/pages/base-ui/api/popover-popup-root.json b/docs/pages/base-ui/api/popover-popup-root.json deleted file mode 100644 index cb32dfc34c..0000000000 --- a/docs/pages/base-ui/api/popover-popup-root.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "props": {}, - "name": "PopoverPositioner", - "imports": [ - "import * as Popover from '@base_ui/react/Popover';\nconst PopoverPositioner = Popover.PopupRoot;" - ], - "classes": [], - "muiName": "PopoverPositioner", - "filename": "/packages/mui-base/src/Popover/PopupRoot/PopoverPositioner.tsx", - "inheritance": null, - "demos": "", - "cssComponent": false -} diff --git a/docs/pages/base-ui/api/popover-popup.json b/docs/pages/base-ui/api/popover-popup.json index 0f8795a3d7..74937e72b3 100644 --- a/docs/pages/base-ui/api/popover-popup.json +++ b/docs/pages/base-ui/api/popover-popup.json @@ -1,44 +1,7 @@ { "props": { - "alignment": { - "type": { - "name": "enum", - "description": "'center'
| 'end'
| 'start'" - }, - "default": "'center'" - }, - "alignmentOffset": { "type": { "name": "number" }, "default": "0" }, - "anchor": { "type": { "name": "any" } }, - "arrowPadding": { "type": { "name": "number" }, "default": "5" }, "className": { "type": { "name": "union", "description": "func
| string" } }, - "collisionBoundary": { "type": { "name": "any" }, "default": "'clippingAncestors'" }, - "collisionPadding": { - "type": { - "name": "union", - "description": "number
| { bottom?: number, left?: number, right?: number, top?: number }" - }, - "default": "5" - }, - "container": { "type": { "name": "any" } }, - "followCursorAxis": { - "type": { - "name": "enum", - "description": "'both'
| 'none'
| 'x'
| 'y'" - }, - "default": "'none'" - }, - "hideWhenDetached": { "type": { "name": "bool" }, "default": "false" }, - "keepMounted": { "type": { "name": "bool" }, "default": "false" }, - "render": { "type": { "name": "union", "description": "element
| func" } }, - "side": { - "type": { - "name": "enum", - "description": "'bottom'
| 'left'
| 'right'
| 'top'" - }, - "default": "'bottom'" - }, - "sideOffset": { "type": { "name": "number" }, "default": "0" }, - "sticky": { "type": { "name": "bool" }, "default": "false" } + "render": { "type": { "name": "union", "description": "element
| func" } } }, "name": "PopoverPopup", "imports": [ diff --git a/docs/pages/base-ui/api/popover-positioner.json b/docs/pages/base-ui/api/popover-positioner.json new file mode 100644 index 0000000000..a890e3e2d9 --- /dev/null +++ b/docs/pages/base-ui/api/popover-positioner.json @@ -0,0 +1,67 @@ +{ + "props": { + "alignment": { + "type": { + "name": "enum", + "description": "'center'
| 'end'
| 'start'" + }, + "default": "'center'" + }, + "alignmentOffset": { "type": { "name": "number" }, "default": "0" }, + "anchor": { + "type": { + "name": "union", + "description": "function (props, propName) {\n if (props[propName] == null) {\n return new Error(\"Prop '\" + propName + \"' is required but wasn't specified\");\n } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {\n return new Error(\"Expected prop '\" + propName + \"' to be of type Element\");\n }\n}
| func
| { contextElement?: function (props, propName) {\n if (props[propName] == null) {\n return null;\n } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {\n return new Error(\"Expected prop '\" + propName + \"' to be of type Element\");\n }\n}, getBoundingClientRect: func }
| { current?: function (props, propName) {\n if (props[propName] == null) {\n return null;\n } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {\n return new Error(\"Expected prop '\" + propName + \"' to be of type Element\");\n }\n} }" + } + }, + "arrowPadding": { "type": { "name": "number" }, "default": "5" }, + "className": { "type": { "name": "union", "description": "func
| string" } }, + "collisionBoundary": { + "type": { + "name": "union", + "description": "'clippingAncestors'
| Array<function (props, propName) {\n if (props[propName] == null) {\n return new Error(\"Prop '\" + propName + \"' is required but wasn't specified\");\n } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {\n return new Error(\"Expected prop '\" + propName + \"' to be of type Element\");\n }\n}>
| function (props, propName) {\n if (props[propName] == null) {\n return new Error(\"Prop '\" + propName + \"' is required but wasn't specified\");\n } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) {\n return new Error(\"Expected prop '\" + propName + \"' to be of type Element\");\n }\n}
| { height: number, width: number, x: number, y: number }" + }, + "default": "'clippingAncestors'" + }, + "collisionPadding": { + "type": { + "name": "union", + "description": "number
| { bottom?: number, left?: number, right?: number, top?: number }" + }, + "default": "5" + }, + "container": { "type": { "name": "any" } }, + "followCursorAxis": { + "type": { + "name": "enum", + "description": "'both'
| 'none'
| 'x'
| 'y'" + }, + "default": "'none'" + }, + "hideWhenDetached": { "type": { "name": "bool" }, "default": "false" }, + "keepMounted": { "type": { "name": "bool" }, "default": "false" }, + "render": { "type": { "name": "union", "description": "element
| func" } }, + "side": { + "type": { + "name": "enum", + "description": "'bottom'
| 'left'
| 'right'
| 'top'" + }, + "default": "'bottom'" + }, + "sideOffset": { "type": { "name": "number" }, "default": "0" }, + "sticky": { "type": { "name": "bool" }, "default": "false" } + }, + "name": "PopoverPositioner", + "imports": [ + "import * as Popover from '@base_ui/react/Popover';\nconst PopoverPositioner = Popover.Positioner;" + ], + "classes": [], + "spread": true, + "themeDefaultProps": true, + "muiName": "PopoverPositioner", + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/mui-base/src/Popover/Positioner/PopoverPositioner.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/base-ui/api/popover-group.json b/docs/pages/base-ui/api/popover-provider.json similarity index 71% rename from docs/pages/base-ui/api/popover-group.json rename to docs/pages/base-ui/api/popover-provider.json index 02befde54d..ee59ff1874 100644 --- a/docs/pages/base-ui/api/popover-group.json +++ b/docs/pages/base-ui/api/popover-provider.json @@ -4,13 +4,13 @@ "delay": { "type": { "name": "number" }, "default": "0" }, "timeout": { "type": { "name": "number" }, "default": "400" } }, - "name": "PopoverGroup", + "name": "PopoverProvider", "imports": [ - "import * as Popover from '@base_ui/react/Popover';\nconst PopoverGroup = Popover.Group;" + "import * as Popover from '@base_ui/react/Popover';\nconst PopoverProvider = Popover.Provider;" ], "classes": [], - "muiName": "PopoverGroup", - "filename": "/packages/mui-base/src/Popover/Group/PopoverGroup.tsx", + "muiName": "PopoverProvider", + "filename": "/packages/mui-base/src/Popover/Provider/PopoverProvider.tsx", "inheritance": null, "demos": "", "cssComponent": false diff --git a/docs/pages/base-ui/api/popover-trigger.json b/docs/pages/base-ui/api/popover-trigger.json index ba28b8570d..48b6895388 100644 --- a/docs/pages/base-ui/api/popover-trigger.json +++ b/docs/pages/base-ui/api/popover-trigger.json @@ -1,13 +1,17 @@ { - "props": {}, + "props": { + "className": { "type": { "name": "union", "description": "func
| string" } }, + "render": { "type": { "name": "union", "description": "element
| func" } } + }, "name": "PopoverTrigger", "imports": [ "import * as Popover from '@base_ui/react/Popover';\nconst PopoverTrigger = Popover.Trigger;" ], "classes": [], "spread": true, - "themeDefaultProps": null, + "themeDefaultProps": true, "muiName": "PopoverTrigger", + "forwardsRefTo": "HTMLButtonElement", "filename": "/packages/mui-base/src/Popover/Trigger/PopoverTrigger.tsx", "inheritance": null, "demos": "", diff --git a/docs/pages/base-ui/api/use-popover-popup.json b/docs/pages/base-ui/api/use-popover-positioner.json similarity index 77% rename from docs/pages/base-ui/api/use-popover-popup.json rename to docs/pages/base-ui/api/use-popover-positioner.json index 84fdb47fc6..f460cf6d23 100644 --- a/docs/pages/base-ui/api/use-popover-popup.json +++ b/docs/pages/base-ui/api/use-popover-positioner.json @@ -1,13 +1,5 @@ { "parameters": { - "onOpenChange": { - "type": { - "name": "(isOpen: boolean, event?: Event, reason?: OpenChangeReason) => void", - "description": "(isOpen: boolean, event?: Event, reason?: OpenChangeReason) => void" - }, - "required": true - }, - "open": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, "alignment": { "type": { "name": "'start' | 'end' | 'center'", @@ -23,7 +15,6 @@ } }, "arrowPadding": { "type": { "name": "number", "description": "number" }, "default": "5" }, - "closeDelay": { "type": { "name": "number", "description": "number" }, "default": "0" }, "collisionBoundary": { "type": { "name": "Boundary", "description": "Boundary" }, "default": "'clippingAncestors'" @@ -35,13 +26,6 @@ "description": "HTMLElement | null | React.MutableRefObject<HTMLElement | null>" } }, - "delay": { "type": { "name": "number", "description": "number" }, "default": "100" }, - "delayType": { - "type": { - "name": "'rest' | 'hover'", - "description": "'rest' | 'hover'" - } - }, "followCursorAxis": { "type": { "name": "'none' | 'both' | 'x' | 'y'", @@ -53,15 +37,9 @@ "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, - "keepMounted": { "type": { "name": "boolean", "description": "boolean" } }, - "openOnHover": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, - "side": { - "type": { - "name": "'top' | 'right' | 'bottom' | 'left'", - "description": "'top' | 'right' | 'bottom' | 'left'" - }, - "default": "'bottom'" - }, + "keepMounted": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, + "open": { "type": { "name": "boolean", "description": "boolean" } }, + "side": { "type": { "name": "Side", "description": "Side" }, "default": "'bottom'" }, "sideOffset": { "type": { "name": "number", "description": "number" }, "default": "0" }, "sticky": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" } }, @@ -91,7 +69,7 @@ }, "required": true }, - "getPopupProps": { + "getPositionerProps": { "type": { "name": "(externalProps?: GenericHTMLProps) => GenericHTMLProps", "description": "(externalProps?: GenericHTMLProps) => GenericHTMLProps" @@ -112,7 +90,7 @@ } }, "name": "usePopoverPositioner", - "filename": "/packages/mui-base/src/Popover/Popup/usePopoverPositioner.tsx", + "filename": "/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx", "imports": ["import { usePopoverPositioner } from '@base_ui/react/Popover';"], "demos": "" } diff --git a/docs/pages/base-ui/api/use-popover-root.json b/docs/pages/base-ui/api/use-popover-root.json index 14a87480e4..0ec2fbdd49 100644 --- a/docs/pages/base-ui/api/use-popover-root.json +++ b/docs/pages/base-ui/api/use-popover-root.json @@ -65,10 +65,7 @@ "required": true }, "transitionStatus": { - "type": { - "name": "TransitionStatus | undefined", - "description": "TransitionStatus | undefined" - }, + "type": { "name": "TransitionStatus", "description": "TransitionStatus" }, "required": true } }, diff --git a/docs/pages/base-ui/react-popover/[docsTab]/index.js b/docs/pages/base-ui/react-popover/[docsTab]/index.js index 3e40f358ae..5827e7c821 100644 --- a/docs/pages/base-ui/react-popover/[docsTab]/index.js +++ b/docs/pages/base-ui/react-popover/[docsTab]/index.js @@ -5,12 +5,12 @@ import * as pageProps from 'docs-base/data/base/components/popover/popover.md?@m import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; import PopoverArrowApiJsonPageContent from '../../api/popover-arrow.json'; import PopoverBackdropApiJsonPageContent from '../../api/popover-backdrop.json'; -import PopoverGroupApiJsonPageContent from '../../api/popover-group.json'; import PopoverPopupApiJsonPageContent from '../../api/popover-popup.json'; -import PopoverPositionerApiJsonPageContent from '../../api/popover-popup-root.json'; +import PopoverPositionerApiJsonPageContent from '../../api/popover-positioner.json'; +import PopoverProviderApiJsonPageContent from '../../api/popover-provider.json'; import PopoverRootApiJsonPageContent from '../../api/popover-root.json'; import PopoverTriggerApiJsonPageContent from '../../api/popover-trigger.json'; -import usePopoverPositionerApiJsonPageContent from '../../api/use-popover-popup.json'; +import usePopoverPositionerApiJsonPageContent from '../../api/use-popover-positioner.json'; import usePopoverRootApiJsonPageContent from '../../api/use-popover-root.json'; export default function Page(props) { @@ -44,13 +44,6 @@ export const getStaticProps = () => { ); const PopoverBackdropApiDescriptions = mapApiPageTranslations(PopoverBackdropApiReq); - const PopoverGroupApiReq = require.context( - 'docs-base/translations/api-docs/popover-group', - false, - /\.\/popover-group.*.json$/, - ); - const PopoverGroupApiDescriptions = mapApiPageTranslations(PopoverGroupApiReq); - const PopoverPopupApiReq = require.context( 'docs-base/translations/api-docs/popover-popup', false, @@ -59,12 +52,19 @@ export const getStaticProps = () => { const PopoverPopupApiDescriptions = mapApiPageTranslations(PopoverPopupApiReq); const PopoverPositionerApiReq = require.context( - 'docs-base/translations/api-docs/popover-popup-root', + 'docs-base/translations/api-docs/popover-positioner', false, - /\.\/popover-popup-root.*.json$/, + /\.\/popover-positioner.*.json$/, ); const PopoverPositionerApiDescriptions = mapApiPageTranslations(PopoverPositionerApiReq); + const PopoverProviderApiReq = require.context( + 'docs-base/translations/api-docs/popover-provider', + false, + /\.\/popover-provider.*.json$/, + ); + const PopoverProviderApiDescriptions = mapApiPageTranslations(PopoverProviderApiReq); + const PopoverRootApiReq = require.context( 'docs-base/translations/api-docs/popover-root', false, @@ -80,9 +80,9 @@ export const getStaticProps = () => { const PopoverTriggerApiDescriptions = mapApiPageTranslations(PopoverTriggerApiReq); const usePopoverPositionerApiReq = require.context( - 'docs-base/translations/api-docs/use-popover-popup', + 'docs-base/translations/api-docs/use-popover-positioner', false, - /\.\/use-popover-popup.*.json$/, + /\.\/use-popover-positioner.*.json$/, ); const usePopoverPositionerApiDescriptions = mapApiPageTranslations(usePopoverPositionerApiReq); @@ -98,18 +98,18 @@ export const getStaticProps = () => { componentsApiDescriptions: { PopoverArrow: PopoverArrowApiDescriptions, PopoverBackdrop: PopoverBackdropApiDescriptions, - PopoverGroup: PopoverGroupApiDescriptions, PopoverPopup: PopoverPopupApiDescriptions, PopoverPositioner: PopoverPositionerApiDescriptions, + PopoverProvider: PopoverProviderApiDescriptions, PopoverRoot: PopoverRootApiDescriptions, PopoverTrigger: PopoverTriggerApiDescriptions, }, componentsApiPageContents: { PopoverArrow: PopoverArrowApiJsonPageContent, PopoverBackdrop: PopoverBackdropApiJsonPageContent, - PopoverGroup: PopoverGroupApiJsonPageContent, PopoverPopup: PopoverPopupApiJsonPageContent, PopoverPositioner: PopoverPositionerApiJsonPageContent, + PopoverProvider: PopoverProviderApiJsonPageContent, PopoverRoot: PopoverRootApiJsonPageContent, PopoverTrigger: PopoverTriggerApiJsonPageContent, }, diff --git a/docs/translations/api-docs/popover-group/popover-group.json b/docs/translations/api-docs/popover-group/popover-group.json deleted file mode 100644 index 518e92975a..0000000000 --- a/docs/translations/api-docs/popover-group/popover-group.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "componentDescription": "Groups popovers' delays together so that once one of the popovers opens, subsequent popovers will\nnot open with a delay.", - "propDescriptions": { - "closeDelay": { - "description": "The delay in milliseconds until the popover content is closed." - }, - "delay": { - "description": "The delay in milliseconds until popovers within the group are open." - }, - "timeout": { - "description": "The timeout in milliseconds until the grouping logic is no longer active after the last popover in the group has closed." - } - }, - "classDescriptions": {} -} diff --git a/docs/translations/api-docs/popover-popup-root/popover-popup-root.json b/docs/translations/api-docs/popover-popup-root/popover-popup-root.json deleted file mode 100644 index f93d4cbd8c..0000000000 --- a/docs/translations/api-docs/popover-popup-root/popover-popup-root.json +++ /dev/null @@ -1 +0,0 @@ -{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} } diff --git a/docs/translations/api-docs/popover-popup/popover-popup.json b/docs/translations/api-docs/popover-popup/popover-popup.json index aace2979db..8e37037584 100644 --- a/docs/translations/api-docs/popover-popup/popover-popup.json +++ b/docs/translations/api-docs/popover-popup/popover-popup.json @@ -1,45 +1,10 @@ { "componentDescription": "The popover popup element.", "propDescriptions": { - "alignment": { - "description": "The alignment of the popover element to the anchor element along its cross axis." - }, - "alignmentOffset": { - "description": "The offset of the popover element along its alignment axis." - }, - "anchor": { - "description": "The anchor element to which the popover content will be placed at." - }, - "arrowPadding": { - "description": "Determines the padding between the arrow and the popover content. Useful when the popover has rounded corners via border-radius." - }, "className": { "description": "Class names applied to the element or a function that returns them based on the component's state." }, - "collisionBoundary": { - "description": "The boundary that the popover element should be constrained to." - }, - "collisionPadding": { "description": "The padding of the collision boundary." }, - "container": { - "description": "The container element to which the popover content will be appended to." - }, - "followCursorAxis": { - "description": "Determines which axis the popover should follow the cursor on." - }, - "hideWhenDetached": { - "description": "If true, the popover will be hidden if it is detached from its anchor element due to differing clipping contexts." - }, - "keepMounted": { - "description": "If true, the popover content will be kept mounted in the DOM." - }, - "render": { "description": "A function to customize rendering of the component." }, - "side": { - "description": "The side of the anchor element that the popover element should align to." - }, - "sideOffset": { "description": "The gap between the anchor element and the popover element." }, - "sticky": { - "description": "If true, allow the popover to remain in stuck view while the anchor element is scrolled out of view." - } + "render": { "description": "A function to customize rendering of the component." } }, "classDescriptions": {} } diff --git a/docs/translations/api-docs/popover-positioner/popover-positioner.json b/docs/translations/api-docs/popover-positioner/popover-positioner.json new file mode 100644 index 0000000000..3ca9149aca --- /dev/null +++ b/docs/translations/api-docs/popover-positioner/popover-positioner.json @@ -0,0 +1,43 @@ +{ + "componentDescription": "The popover positioner element.", + "propDescriptions": { + "alignment": { + "description": "The alignment of the popover element to the anchor element along its cross axis." + }, + "alignmentOffset": { + "description": "The offset of the popover element along its alignment axis." + }, + "anchor": { "description": "The anchor element to which the popover popup will be placed at." }, + "arrowPadding": { + "description": "Determines the padding between the arrow and the popover popup's edges. Useful when the popover popup has rounded corners via border-radius." + }, + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "collisionBoundary": { + "description": "The boundary that the popover element should be constrained to." + }, + "collisionPadding": { "description": "The padding of the collision boundary." }, + "container": { + "description": "The container element to which the popover popup will be appended to." + }, + "followCursorAxis": { + "description": "Determines which axis the popover should follow the cursor on." + }, + "hideWhenDetached": { + "description": "If true, the popover will be hidden if it is detached from its anchor element due to differing clipping contexts." + }, + "keepMounted": { + "description": "If true, tooltip stays mounted in the DOM when closed." + }, + "render": { "description": "A function to customize rendering of the component." }, + "side": { + "description": "The side of the anchor element that the popover element should align to." + }, + "sideOffset": { "description": "The gap between the anchor element and the popover element." }, + "sticky": { + "description": "If true, allow the popover to remain in stuck view while the anchor element is scrolled out of view." + } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/popover-provider/popover-provider.json b/docs/translations/api-docs/popover-provider/popover-provider.json new file mode 100644 index 0000000000..8fb3385a83 --- /dev/null +++ b/docs/translations/api-docs/popover-provider/popover-provider.json @@ -0,0 +1,11 @@ +{ + "componentDescription": "Provides a shared delay for popovers so that once a popover is shown, the rest of the popovers in\nthe group will not wait for the delay before showing.", + "propDescriptions": { + "closeDelay": { "description": "The delay in milliseconds until the popover popup is closed." }, + "delay": { "description": "The delay in milliseconds until the popover popup is opened." }, + "timeout": { + "description": "The timeout in milliseconds until the grouping logic is no longer active after the last popover in the group has closed." + } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/popover-root/popover-root.json b/docs/translations/api-docs/popover-root/popover-root.json index f8f6fc72de..110598a5ca 100644 --- a/docs/translations/api-docs/popover-root/popover-root.json +++ b/docs/translations/api-docs/popover-root/popover-root.json @@ -1,27 +1,25 @@ { "componentDescription": "The foundation for building custom-styled popovers.", "propDescriptions": { - "closeDelay": { - "description": "The delay in milliseconds until the popover content is closed." - }, + "closeDelay": { "description": "The delay in milliseconds until the popover popup is closed." }, "defaultOpen": { "description": "Specifies whether the popover is open initially when uncontrolled." }, - "delay": { "description": "The delay in milliseconds until the popover content is opened." }, + "delay": { "description": "The delay in milliseconds until the popover popup is opened." }, "delayType": { - "description": "The delay type to use. rest means the delay represents how long the user's cursor must rest on the trigger before the popover content is opened. hover means the delay represents how long to wait once the user's cursor has entered the trigger." + "description": "The delay type to use. rest means the delay represents how long the user's cursor must rest on the trigger before the popover popup is opened. hover means the delay represents how long to wait as soon as the user's cursor has entered the trigger." }, "followCursorAxis": { "description": "Determines which axis the tooltip should follow the cursor on." }, "onOpenChange": { - "description": "Callback fired when the popover content is requested to be opened or closed. Use when controlled." + "description": "Callback fired when the popover popup is requested to be opened or closed. Use when controlled." }, "open": { - "description": "If true, the popover content is open. Use when controlled." + "description": "If true, the popover popup is open. Use when controlled." }, "openOnHover": { - "description": "If true, the popover content opens when the trigger is hovered." + "description": "If true, the popover popup opens when the trigger is hovered." } }, "classDescriptions": {} diff --git a/docs/translations/api-docs/popover-trigger/popover-trigger.json b/docs/translations/api-docs/popover-trigger/popover-trigger.json index c9ead1764a..763409f066 100644 --- a/docs/translations/api-docs/popover-trigger/popover-trigger.json +++ b/docs/translations/api-docs/popover-trigger/popover-trigger.json @@ -1,5 +1,10 @@ { - "componentDescription": "Provides props for its child element to trigger the popover, anchoring it to the child element.", - "propDescriptions": {}, + "componentDescription": "Renders a trigger element that will open the popover.", + "propDescriptions": { + "className": { + "description": "Class names applied to the element or a function that returns them based on the component's state." + }, + "render": { "description": "A function to customize rendering of the component." } + }, "classDescriptions": {} } diff --git a/docs/translations/api-docs/use-popover-popup/use-popover-popup.json b/docs/translations/api-docs/use-popover-positioner/use-popover-positioner.json similarity index 67% rename from docs/translations/api-docs/use-popover-popup/use-popover-popup.json rename to docs/translations/api-docs/use-popover-positioner/use-popover-positioner.json index 349a803747..1f3f6356f7 100644 --- a/docs/translations/api-docs/use-popover-popup/use-popover-popup.json +++ b/docs/translations/api-docs/use-popover-positioner/use-popover-positioner.json @@ -7,21 +7,17 @@ "alignmentOffset": { "description": "The offset of the popover element along its alignment axis." }, - "anchor": { "description": "The anchor element of the popover." }, + "anchor": { "description": "The anchor element to which the popover popup will be placed at." }, "arrowPadding": { - "description": "Determines the padding between the arrow and the popover content. Useful when the popover has rounded corners via border-radius." - }, - "closeDelay": { - "description": "The hover delay in milliseconds before the popover closes after the trigger element is unhovered." + "description": "Determines the padding between the arrow and the popover popup's edges. Useful when the popover popup has rounded corners via border-radius." }, "collisionBoundary": { "description": "The boundary that the popover element should be constrained to." }, "collisionPadding": { "description": "The padding of the collision boundary." }, - "delay": { - "description": "The hover delay in milliseconds before the popover opens after the trigger element is hovered." + "container": { + "description": "The container element to which the popover popup will be appended to." }, - "delayType": { "description": "The type of hover open delay." }, "followCursorAxis": { "description": "Determines which axis the popover should follow the cursor on." }, @@ -29,15 +25,9 @@ "description": "If true, the popover will be hidden if it is detached from its anchor element due to differing clipping contexts." }, "keepMounted": { - "description": "If true, the popover will be mounted, including CSS transitions or animations." - }, - "onOpenChange": { - "description": "Callback fired when the popover is requested to be opened or closed." - }, - "open": { "description": "If true, the popover will be open." }, - "openOnHover": { - "description": "If true, the popover content opens when the trigger is hovered." + "description": "If true, tooltip stays mounted in the DOM when closed." }, + "open": { "description": "If true, the popover is open." }, "side": { "description": "The side of the anchor element that the popover element should align to." }, @@ -51,7 +41,7 @@ "arrowRef": { "description": "The ref of the popover arrow element." }, "arrowUncentered": { "description": "Determines if the arrow cannot be centered." }, "getArrowProps": { "description": "Props to spread on the popover arrow element." }, - "getPopupProps": { "description": "Props to spread on the popover content element." }, + "getPositionerProps": { "description": "Props to spread on the popover positioner element." }, "mounted": { "description": "Whether the popover is mounted, including CSS transitions or animations." }, diff --git a/docs/translations/api-docs/use-popover-root/use-popover-root.json b/docs/translations/api-docs/use-popover-root/use-popover-root.json index e642ed0afb..348dd21f74 100644 --- a/docs/translations/api-docs/use-popover-root/use-popover-root.json +++ b/docs/translations/api-docs/use-popover-root/use-popover-root.json @@ -1,15 +1,13 @@ { "hookDescription": "Manages the root state for a popover.", "parametersDescriptions": { - "closeDelay": { - "description": "The delay in milliseconds until the popover content is closed." - }, + "closeDelay": { "description": "The delay in milliseconds until the popover popup is closed." }, "defaultOpen": { "description": "Specifies whether the popover is open initially when uncontrolled." }, - "delay": { "description": "The delay in milliseconds until the popover content is opened." }, + "delay": { "description": "The delay in milliseconds until the popover popup is opened." }, "delayType": { - "description": "The delay type to use. rest means the delay represents how long the user's cursor must rest on the trigger before the popover content is opened. hover means the delay represents how long to wait once the user's cursor has entered the trigger." + "description": "The delay type to use. rest means the delay represents how long the user's cursor must rest on the trigger before the popover popup is opened. hover means the delay represents how long to wait as soon as the user's cursor has entered the trigger." }, "followCursorAxis": { "description": "Determines which axis the tooltip should follow the cursor on." @@ -18,15 +16,15 @@ "description": "If true, tooltip stays mounted in the DOM when closed." }, "onOpenChange": { - "description": "Callback fired when the popover content is requested to be opened or closed. Use when controlled." + "description": "Callback fired when the popover popup is requested to be opened or closed. Use when controlled." }, "open": { - "description": "If true, the popover content is open. Use when controlled." + "description": "If true, the popover popup is open. Use when controlled." }, "openOnHover": { - "description": "If true, the popover content opens when the trigger is hovered." + "description": "If true, the popover popup opens when the trigger is hovered." }, - "popupEl": { "description": "The element that contains the popover content." }, + "popupEl": { "description": "The element that contains the popover popup." }, "triggerEl": { "description": "The element that triggers the popover." } }, "returnValueDescriptions": {} diff --git a/docs/translations/translations.json b/docs/translations/translations.json index 2bbe0b273d..c33ee040bb 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -223,10 +223,10 @@ "/base-ui/react-checkbox": "Checkbox", "/base-ui/react-number-field": "Number Field", "/base-ui/react-switch": "Switch", - "navigation": "Navigation", - "/base-ui/react-tabs": "Tabs", "data-display": "Data display", "/base-ui/react-popover": "Popover", + "navigation": "Navigation", + "/base-ui/react-tabs": "Tabs", "/base-ui/guides": "How-to guides", "/base-ui/guides/next-js-app-router": "Next.js App Router" } diff --git a/packages/mui-base/package.json b/packages/mui-base/package.json index 55072d3394..d41b8f8319 100644 --- a/packages/mui-base/package.json +++ b/packages/mui-base/package.json @@ -40,13 +40,8 @@ }, "dependencies": { "@babel/runtime": "^7.24.5", -<<<<<<< HEAD - "@floating-ui/react": "^0.26.13", + "@floating-ui/react": "^0.26.16", "@floating-ui/react-dom": "^2.1.0", -======= - "@floating-ui/react": "^0.26.15", - "@floating-ui/react-dom": "^2.0.9", ->>>>>>> ef816f13 (Refactor to use root context) "@floating-ui/utils": "^0.2.2", "@mui/types": "^7.2.14", "@mui/utils": "^5.15.14", diff --git a/packages/mui-base/src/Popover/Arrow/PopoverArrow.test.tsx b/packages/mui-base/src/Popover/Arrow/PopoverArrow.test.tsx index e85d3cfecc..f459b4ed6d 100644 --- a/packages/mui-base/src/Popover/Arrow/PopoverArrow.test.tsx +++ b/packages/mui-base/src/Popover/Arrow/PopoverArrow.test.tsx @@ -11,7 +11,9 @@ describe('', () => { render(node) { return render( - {node} + + {node} + , ); }, diff --git a/packages/mui-base/src/Popover/Arrow/PopoverArrow.tsx b/packages/mui-base/src/Popover/Arrow/PopoverArrow.tsx index 4305e7186e..f9a414b3c2 100644 --- a/packages/mui-base/src/Popover/Arrow/PopoverArrow.tsx +++ b/packages/mui-base/src/Popover/Arrow/PopoverArrow.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import type { PopoverArrowOwnerState, PopoverArrowProps } from './PopoverArrow.types'; -import { resolveClassName } from '../../utils/resolveClassName'; -import { usePopoverPositionerContext } from '../Popup/PopoverPopupContext'; -import { evaluateRenderProp } from '../../utils/evaluateRenderProp'; -import { useRenderPropForkRef } from '../../utils/useRenderPropForkRef'; -import { useStyleHooks } from './useStyleHooks'; +import { usePopoverPositionerContext } from '../Positioner/PopoverPositionerContext'; +import { popoverArrowStyleHookMapping } from './styleHooks'; +import { usePopoverRootContext } from '../Root/PopoverRootContext'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useForkRef } from '../../utils/useForkRef'; /** * The tooltip arrow caret element. @@ -23,10 +23,10 @@ const PopoverArrow = React.forwardRef(function PopoverArrow( props: PopoverArrowProps, forwardedRef: React.ForwardedRef, ) { - const { render: renderProp, hideWhenUncentered = false, className, ...otherProps } = props; - const render = renderProp ??
; + const { className, render, hideWhenUncentered = false, ...otherProps } = props; - const { open, arrowRef, side, alignment, arrowUncentered, getArrowProps } = + const { open } = usePopoverRootContext(); + const { arrowRef, side, alignment, arrowUncentered, getArrowProps } = usePopoverPositionerContext(); const ownerState: PopoverArrowOwnerState = React.useMemo( @@ -34,26 +34,30 @@ const PopoverArrow = React.forwardRef(function PopoverArrow( open, side, alignment, + arrowUncentered, }), - [open, side, alignment], + [open, side, alignment, arrowUncentered], ); - const mergedRef = useRenderPropForkRef(render, arrowRef, forwardedRef); + const mergedRef = useForkRef(arrowRef, forwardedRef); - const styleHooks = useStyleHooks(ownerState); - - const arrowProps = getArrowProps({ + const { renderElement } = useComponentRenderer({ + propGetter: getArrowProps, + render: render ?? 'div', + className, + ownerState, ref: mergedRef, - className: resolveClassName(className, ownerState), - ...styleHooks, - ...otherProps, - style: { - ...(hideWhenUncentered && arrowUncentered && { visibility: 'hidden' }), - ...otherProps.style, + extraProps: { + ...otherProps, + style: { + ...(hideWhenUncentered && arrowUncentered && { visibility: 'hidden' }), + ...otherProps.style, + }, }, + customStyleHookMapping: popoverArrowStyleHookMapping, }); - return evaluateRenderProp(render, arrowProps, ownerState); + return renderElement(); }); PopoverArrow.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-base/src/Popover/Arrow/PopoverArrow.types.ts b/packages/mui-base/src/Popover/Arrow/PopoverArrow.types.ts index b9693c5dd5..abcf62eb4a 100644 --- a/packages/mui-base/src/Popover/Arrow/PopoverArrow.types.ts +++ b/packages/mui-base/src/Popover/Arrow/PopoverArrow.types.ts @@ -5,6 +5,7 @@ export type PopoverArrowOwnerState = { open: boolean; side: Side; alignment: 'start' | 'end' | 'center'; + arrowUncentered: boolean; }; export interface PopoverArrowProps extends BaseUIComponentProps<'div', PopoverArrowOwnerState> { diff --git a/packages/mui-base/src/Popover/Arrow/styleHooks.ts b/packages/mui-base/src/Popover/Arrow/styleHooks.ts new file mode 100644 index 0000000000..10e36860f5 --- /dev/null +++ b/packages/mui-base/src/Popover/Arrow/styleHooks.ts @@ -0,0 +1,10 @@ +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; +import type { PopoverArrowOwnerState } from './PopoverArrow.types'; + +export const popoverArrowStyleHookMapping: CustomStyleHookMapping = { + open(value) { + return { + 'data-state': value ? 'open' : 'closed', + }; + }, +}; diff --git a/packages/mui-base/src/Popover/Arrow/useStyleHooks.ts b/packages/mui-base/src/Popover/Arrow/useStyleHooks.ts deleted file mode 100644 index e03b4586d0..0000000000 --- a/packages/mui-base/src/Popover/Arrow/useStyleHooks.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import type { PopoverArrowOwnerState } from './PopoverArrow.types'; -import { getStyleHookProps } from '../../utils/getStyleHookProps'; - -/** - * @ignore - internal hook. - */ -export function useStyleHooks(ownerState: PopoverArrowOwnerState) { - return React.useMemo(() => { - return getStyleHookProps(ownerState, { - open(value) { - return { - 'data-state': value ? 'open' : 'closed', - }; - }, - }); - }, [ownerState]); -} diff --git a/packages/mui-base/src/Popover/Backdrop/PopoverBackdrop.tsx b/packages/mui-base/src/Popover/Backdrop/PopoverBackdrop.tsx index 39cfe2d85f..9ed0c7c879 100644 --- a/packages/mui-base/src/Popover/Backdrop/PopoverBackdrop.tsx +++ b/packages/mui-base/src/Popover/Backdrop/PopoverBackdrop.tsx @@ -3,14 +3,8 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { FloatingOverlay, FloatingPortal } from '@floating-ui/react'; import type { PopoverBackdropProps } from './PopoverBackdrop.types'; -import { useRenderPropForkRef } from '../../utils/useRenderPropForkRef'; import { usePopoverRootContext } from '../Root/PopoverRootContext'; -import { resolveClassName } from '../../utils/resolveClassName'; -import { evaluateRenderProp } from '../../utils/evaluateRenderProp'; - -function defaultRender(props: React.ComponentPropsWithRef<'div'>) { - return ; -} +import { useComponentRenderer } from '../../utils/useComponentRenderer'; /** * Renders a backdrop for the popover. @@ -27,31 +21,32 @@ const PopoverBackdrop = React.forwardRef(function PopoverBackdrop( props: PopoverBackdropProps, forwardedRef: React.ForwardedRef, ) { - const { className, render: renderProp, keepMounted = false, ...otherProps } = props; - const render = renderProp ?? defaultRender; + const { className, render, keepMounted = false, container, ...otherProps } = props; const { open, mounted } = usePopoverRootContext(); - const mergedRef = useRenderPropForkRef(render, forwardedRef); - const ownerState = React.useMemo(() => ({ open }), [open]); + const { renderElement } = useComponentRenderer({ + render: render ?? , + className, + ownerState, + ref: forwardedRef, + extraProps: { + ...otherProps, + style: { + zIndex: 2147483647, // max z-index + ...otherProps.style, + }, + }, + }); + const shouldRender = keepMounted || mounted; if (!shouldRender) { return null; } - const backdropProps = { - ref: mergedRef, - className: resolveClassName(className, ownerState), - ...otherProps, - style: { - zIndex: 2147483647, // max z-index - ...otherProps.style, - }, - }; - - return {evaluateRenderProp(render, backdropProps, ownerState)}; + return {renderElement()}; }); PopoverBackdrop.propTypes /* remove-proptypes */ = { diff --git a/packages/mui-base/src/Popover/Popup/PopoverPopup.test.tsx b/packages/mui-base/src/Popover/Popup/PopoverPopup.test.tsx index f5eab4db15..7ea62d2d50 100644 --- a/packages/mui-base/src/Popover/Popup/PopoverPopup.test.tsx +++ b/packages/mui-base/src/Popover/Popup/PopoverPopup.test.tsx @@ -11,7 +11,11 @@ describe('', () => { inheritComponent: 'div', refInstanceof: window.HTMLDivElement, render(node) { - return render({node}); + return render( + + {node} + , + ); }, skip: ['reactTestRenderer'], })); @@ -19,7 +23,9 @@ describe('', () => { it('should render the children', async () => { render( - Content + + Content + , ); diff --git a/packages/mui-base/src/Popover/Popup/PopoverPopup.tsx b/packages/mui-base/src/Popover/Popup/PopoverPopup.tsx index 53678f12d7..964572ee1b 100644 --- a/packages/mui-base/src/Popover/Popup/PopoverPopup.tsx +++ b/packages/mui-base/src/Popover/Popup/PopoverPopup.tsx @@ -1,13 +1,11 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { resolveClassName } from '../../utils/resolveClassName'; import { usePopoverRootContext } from '../Root/PopoverRootContext'; -import { PopoverPopupContext } from './PopoverPopupContext'; -import { evaluateRenderProp } from '../../utils/evaluateRenderProp'; -import { useRenderPropForkRef } from '../../utils/useRenderPropForkRef'; import { PopoverPopupOwnerState, PopoverPopupProps } from './PopoverPopup.types'; -import { usePopoverPositionerStyleHooks } from './useStyleHooks'; +import { popoverPopupStyleHookMapping } from './styleHooks'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { usePopoverPositionerContext } from '../Positioner/PopoverPositionerContext'; /** * The popover popup element. @@ -24,53 +22,38 @@ const PopoverPopup = React.forwardRef(function PopoverPopup( props: PopoverPopupProps, forwardedRef: React.ForwardedRef, ) { - const { className, render: renderProp, ...otherProps } = props; - const render = renderProp ??
; + const { className, render, ...otherProps } = props; const { open, transitionStatus } = usePopoverRootContext(); + const { side, alignment } = usePopoverPositionerContext(); const ownerState: PopoverPopupOwnerState = React.useMemo( () => ({ open, - side: popover.side, - alignment: popover.alignment, - status: transitionStatus, + side, + alignment, + entering: transitionStatus === 'entering', + exiting: transitionStatus === 'exiting', }), - [open, transitionStatus, popover.side, popover.alignment], + [open, transitionStatus, side, alignment], ); - const contextValue = React.useMemo( - () => ({ - ...ownerState, - arrowRef: popover.arrowRef, - arrowUncentered: popover.arrowUncentered, - getArrowProps: popover.getArrowProps, - }), - [ownerState, popover.arrowRef, popover.arrowUncentered, popover.getArrowProps], - ); - - const mergedRef = useRenderPropForkRef(render, forwardedRef); - const styleHooks = usePopoverPositionerStyleHooks(ownerState); - - // The content element needs to be a child of a wrapper floating element in order to avoid - // conflicts with CSS transitions and the positioning transform. - const popupProps = { - ref: mergedRef, - className: resolveClassName(className, ownerState), - ...styleHooks, - ...otherProps, - style: { - // must be relative to the content element. - position: 'relative', - ...otherProps.style, + const { renderElement } = useComponentRenderer({ + ref: forwardedRef, + render: render ?? 'div', + className, + ownerState, + customStyleHookMapping: popoverPopupStyleHookMapping, + extraProps: { + style: { + position: 'relative', + ...otherProps.style, + }, + ...otherProps, }, - } as const; + }); - return ( - - {evaluateRenderProp(render, popupProps, ownerState)} - - ); + return renderElement(); }); PopoverPopup.propTypes /* remove-proptypes */ = { @@ -78,26 +61,6 @@ PopoverPopup.propTypes /* remove-proptypes */ = { // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ - /** - * The alignment of the popover element to the anchor element along its cross axis. - * @default 'center' - */ - alignment: PropTypes.oneOf(['center', 'end', 'start']), - /** - * The offset of the popover element along its alignment axis. - * @default 0 - */ - alignmentOffset: PropTypes.number, - /** - * The anchor element to which the popover content will be placed at. - */ - anchor: PropTypes /* @typescript-to-proptypes-ignore */.any, - /** - * Determines the padding between the arrow and the popover content. Useful when the popover - * has rounded corners via `border-radius`. - * @default 5 - */ - arrowPadding: PropTypes.number, /** * @ignore */ @@ -106,64 +69,10 @@ PopoverPopup.propTypes /* remove-proptypes */ = { * Class names applied to the element or a function that returns them based on the component's state. */ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - /** - * The boundary that the popover element should be constrained to. - * @default 'clippingAncestors' - */ - collisionBoundary: PropTypes /* @typescript-to-proptypes-ignore */.any, - /** - * The padding of the collision boundary. - * @default 5 - */ - collisionPadding: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.shape({ - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - top: PropTypes.number, - }), - ]), - /** - * The container element to which the popover content will be appended to. - */ - container: PropTypes /* @typescript-to-proptypes-ignore */.any, - /** - * Determines which axis the popover should follow the cursor on. - * @default 'none' - */ - followCursorAxis: PropTypes.oneOf(['both', 'none', 'x', 'y']), - /** - * If `true`, the popover will be hidden if it is detached from its anchor element due to - * differing clipping contexts. - * @default false - */ - hideWhenDetached: PropTypes.bool, - /** - * If `true`, the popover content will be kept mounted in the DOM. - * @default false - */ - keepMounted: PropTypes.bool, /** * A function to customize rendering of the component. */ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), - /** - * The side of the anchor element that the popover element should align to. - * @default 'bottom' - */ - side: PropTypes.oneOf(['bottom', 'left', 'right', 'top']), - /** - * The gap between the anchor element and the popover element. - * @default 0 - */ - sideOffset: PropTypes.number, - /** - * If `true`, allow the popover to remain in stuck view while the anchor element is scrolled out - * of view. - * @default false - */ - sticky: PropTypes.bool, /** * @ignore */ diff --git a/packages/mui-base/src/Popover/Popup/PopoverPopup.types.ts b/packages/mui-base/src/Popover/Popup/PopoverPopup.types.ts index 7bef7edef8..e55c607d6c 100644 --- a/packages/mui-base/src/Popover/Popup/PopoverPopup.types.ts +++ b/packages/mui-base/src/Popover/Popup/PopoverPopup.types.ts @@ -1,19 +1,12 @@ import type { Side } from '@floating-ui/react'; -import type { BaseUIComponentProps, GenericHTMLProps } from '../../utils/BaseUI.types'; - -export interface PopoverPopupContextValue { - open: boolean; - side: Side; - alignment: 'start' | 'end' | 'center'; - arrowRef: React.MutableRefObject; - arrowUncentered: boolean; - getArrowProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; -} +import type { BaseUIComponentProps } from '../../utils/BaseUI.types'; export type PopoverPopupOwnerState = { open: boolean; side: Side; alignment: 'start' | 'end' | 'center'; + entering: boolean; + exiting: boolean; }; export interface PopoverPopupProps extends BaseUIComponentProps<'div', PopoverPopupOwnerState> {} diff --git a/packages/mui-base/src/Popover/Popup/PopoverPopupContext.ts b/packages/mui-base/src/Popover/Popup/PopoverPopupContext.ts deleted file mode 100644 index f86e076101..0000000000 --- a/packages/mui-base/src/Popover/Popup/PopoverPopupContext.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; -import type { PopoverPopupContextValue } from './PopoverPopup.types'; - -export const PopoverPopupContext = React.createContext(null); - -export function usePopoverPositionerContext() { - const context = React.useContext(PopoverPopupContext); - if (context === null) { - throw new Error(' must be used within the component'); - } - return context; -} diff --git a/packages/mui-base/src/Popover/Popup/styleHooks.ts b/packages/mui-base/src/Popover/Popup/styleHooks.ts new file mode 100644 index 0000000000..d6f2906288 --- /dev/null +++ b/packages/mui-base/src/Popover/Popup/styleHooks.ts @@ -0,0 +1,16 @@ +import type { PopoverPopupOwnerState } from './PopoverPopup.types'; +import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps'; + +export const popoverPopupStyleHookMapping: CustomStyleHookMapping = { + entering(value) { + return value ? { 'data-entering': '' } : null; + }, + exiting(value) { + return value ? { 'data-exiting': '' } : null; + }, + open(value) { + return { + 'data-state': value ? 'open' : 'closed', + }; + }, +}; diff --git a/packages/mui-base/src/Popover/Popup/useStyleHooks.ts b/packages/mui-base/src/Popover/Popup/useStyleHooks.ts deleted file mode 100644 index a24051d183..0000000000 --- a/packages/mui-base/src/Popover/Popup/useStyleHooks.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { getStyleHookProps } from '../../utils/getStyleHookProps'; -import type { PopoverPopupOwnerState } from './PopoverPopup.types'; - -/** - * @ignore - internal hook. - */ -export function usePopoverPositionerStyleHooks(ownerState: PopoverPopupOwnerState) { - return React.useMemo(() => { - return getStyleHookProps(ownerState, { - open(value) { - return { - 'data-state': value ? 'open' : 'closed', - }; - }, - }); - }, [ownerState]); -} diff --git a/packages/mui-base/src/Popover/Positioner/PopoverPositioner.test.tsx b/packages/mui-base/src/Popover/Positioner/PopoverPositioner.test.tsx index e69de29bb2..471c03071e 100644 --- a/packages/mui-base/src/Popover/Positioner/PopoverPositioner.test.tsx +++ b/packages/mui-base/src/Popover/Positioner/PopoverPositioner.test.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import * as Popover from '@base_ui/react/Popover'; +import { createRenderer } from '@mui/internal-test-utils'; +import { describeConformance } from '../../../test/describeConformance'; + +describe('', () => { + const { render } = createRenderer(); + + describeConformance(, () => ({ + inheritComponent: 'div', + refInstanceof: window.HTMLDivElement, + render(node) { + return render({node}); + }, + skip: ['reactTestRenderer'], + })); +}); diff --git a/packages/mui-base/src/Popover/Positioner/PopoverPositioner.tsx b/packages/mui-base/src/Popover/Positioner/PopoverPositioner.tsx index 6bdc31b531..e348004060 100644 --- a/packages/mui-base/src/Popover/Positioner/PopoverPositioner.tsx +++ b/packages/mui-base/src/Popover/Positioner/PopoverPositioner.tsx @@ -1,10 +1,28 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { FloatingFocusManager, FloatingPortal } from '@floating-ui/react'; -import type { PopoverPositionerProps } from './PopoverPositioner.types'; -import { usePopoverPositioner } from './usePopoverPositioner'; +import type { + PopoverPositionerContextValue, + PopoverPositionerOwnerState, + PopoverPositionerProps, +} from './PopoverPositioner.types'; +import { useComponentRenderer } from '../../utils/useComponentRenderer'; +import { useForkRef } from '../../utils/useForkRef'; import { usePopoverRootContext } from '../Root/PopoverRootContext'; +import { usePopoverPositioner } from './usePopoverPositioner'; +import { PopoverPositionerContext } from './PopoverPositionerContext'; +/** + * The popover positioner element. + * + * Demos: + * + * - [Popover](https://mui.com/base-ui/react-popover/) + * + * API: + * + * - [PopoverPositioner API](https://mui.com/base-ui/react-popover/components-api/#popover-positioner) + */ const PopoverPositioner = React.forwardRef(function PopoverPositioner( props: PopoverPositionerProps, forwardedRef: React.ForwardedRef, @@ -12,7 +30,7 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner( const { anchor, className, - render: renderProp, + render, side = 'bottom', alignment = 'center', sideOffset = 0, @@ -47,14 +65,56 @@ const PopoverPositioner = React.forwardRef(function PopoverPositioner( followCursorAxis, }); + const ownerState: PopoverPositionerOwnerState = React.useMemo( + () => ({ + open, + side: positioner.side, + alignment: positioner.alignment, + }), + [open, positioner.side, positioner.alignment], + ); + + const contextValue: PopoverPositionerContextValue = React.useMemo( + () => ({ + side: positioner.side, + alignment: positioner.alignment, + arrowRef: positioner.arrowRef, + arrowUncentered: positioner.arrowUncentered, + getArrowProps: positioner.getArrowProps, + }), + [ + positioner.side, + positioner.alignment, + positioner.arrowRef, + positioner.arrowUncentered, + positioner.getArrowProps, + ], + ); + + const mergedRef = useForkRef(forwardedRef, setPopupEl); + + const { renderElement } = useComponentRenderer({ + propGetter: positioner.getPositionerProps, + render: render ?? 'div', + className, + ownerState, + ref: mergedRef, + extraProps: otherProps, + }); + + const shouldRender = keepMounted || positioner.mounted; + if (!shouldRender) { + return null; + } + return ( - - -
- {props.children} -
-
-
+ + + + {renderElement()} + + + ); }); @@ -64,17 +124,141 @@ PopoverPositioner.propTypes /* remove-proptypes */ = { // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ /** - * @ignore + * The alignment of the popover element to the anchor element along its cross axis. + * @default 'center' + */ + alignment: PropTypes.oneOf(['center', 'end', 'start']), + /** + * The offset of the popover element along its alignment axis. + * @default 0 + */ + alignmentOffset: PropTypes.number, + /** + * The anchor element to which the popover popup will be placed at. + */ + anchor: PropTypes.oneOfType([ + function (props, propName) { + if (props[propName] == null) { + return new Error("Prop '" + propName + "' is required but wasn't specified"); + } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { + return new Error("Expected prop '" + propName + "' to be of type Element"); + } + }, + PropTypes.func, + PropTypes.shape({ + contextElement: function (props, propName) { + if (props[propName] == null) { + return null; + } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { + return new Error("Expected prop '" + propName + "' to be of type Element"); + } + }, + getBoundingClientRect: PropTypes.func.isRequired, + }), + PropTypes.shape({ + current: function (props, propName) { + if (props[propName] == null) { + return null; + } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { + return new Error("Expected prop '" + propName + "' to be of type Element"); + } + }, + }), + ]), + /** + * Determines the padding between the arrow and the popover popup's edges. Useful when the popover + * popup has rounded corners via `border-radius`. + * @default 5 */ - children: PropTypes.element.isRequired, + arrowPadding: PropTypes.number, /** * @ignore */ + children: PropTypes.node, + /** + * Class names applied to the element or a function that returns them based on the component's state. + */ + className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + /** + * The boundary that the popover element should be constrained to. + * @default 'clippingAncestors' + */ + collisionBoundary: PropTypes.oneOfType([ + PropTypes.oneOf(['clippingAncestors']), + PropTypes.arrayOf(function (props, propName) { + if (props[propName] == null) { + return new Error("Prop '" + propName + "' is required but wasn't specified"); + } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { + return new Error("Expected prop '" + propName + "' to be of type Element"); + } + }), + function (props, propName) { + if (props[propName] == null) { + return new Error("Prop '" + propName + "' is required but wasn't specified"); + } else if (typeof props[propName] !== 'object' || props[propName].nodeType !== 1) { + return new Error("Expected prop '" + propName + "' to be of type Element"); + } + }, + PropTypes.shape({ + height: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), + ]), + /** + * The padding of the collision boundary. + * @default 5 + */ + collisionPadding: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.shape({ + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + top: PropTypes.number, + }), + ]), + /** + * The container element to which the popover popup will be appended to. + */ container: PropTypes /* @typescript-to-proptypes-ignore */.any, /** - * @ignore + * Determines which axis the popover should follow the cursor on. + * @default 'none' + */ + followCursorAxis: PropTypes.oneOf(['both', 'none', 'x', 'y']), + /** + * If `true`, the popover will be hidden if it is detached from its anchor element due to + * differing clipping contexts. + * @default false + */ + hideWhenDetached: PropTypes.bool, + /** + * If `true`, tooltip stays mounted in the DOM when closed. + * @default false + */ + keepMounted: PropTypes.bool, + /** + * A function to customize rendering of the component. + */ + render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + /** + * The side of the anchor element that the popover element should align to. + * @default 'bottom' + */ + side: PropTypes.oneOf(['bottom', 'left', 'right', 'top']), + /** + * The gap between the anchor element and the popover element. + * @default 0 + */ + sideOffset: PropTypes.number, + /** + * If `true`, allow the popover to remain in stuck view while the anchor element is scrolled out + * of view. + * @default false */ - unstable_floatingContext: PropTypes /* @typescript-to-proptypes-ignore */.any, + sticky: PropTypes.bool, } as any; export { PopoverPositioner }; diff --git a/packages/mui-base/src/Popover/Positioner/PopoverPositioner.types.ts b/packages/mui-base/src/Popover/Positioner/PopoverPositioner.types.ts index b5c7ebb341..f6502a8cd1 100644 --- a/packages/mui-base/src/Popover/Positioner/PopoverPositioner.types.ts +++ b/packages/mui-base/src/Popover/Positioner/PopoverPositioner.types.ts @@ -1,7 +1,15 @@ -import type { FloatingContext, Side } from '@floating-ui/react'; -import type { BaseUIComponentProps } from '../../utils/BaseUI.types'; +import type { Side } from '@floating-ui/react'; +import type { BaseUIComponentProps, GenericHTMLProps } from '../../utils/BaseUI.types'; import type { PopoverPositionerParameters } from './usePopoverPositioner.types'; +export interface PopoverPositionerContextValue { + side: Side; + alignment: 'start' | 'end' | 'center'; + arrowRef: React.MutableRefObject; + arrowUncentered: boolean; + getArrowProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; +} + export type PopoverPositionerOwnerState = { open: boolean; side: Side; @@ -10,7 +18,4 @@ export type PopoverPositionerOwnerState = { export interface PopoverPositionerProps extends PopoverPositionerParameters, - BaseUIComponentProps<'div', PopoverPositionerOwnerState> { - children: React.ReactElement; - unstable_floatingContext: FloatingContext; -} + BaseUIComponentProps<'div', PopoverPositionerOwnerState> {} diff --git a/packages/mui-base/src/Popover/Positioner/PopoverPositionerContext.ts b/packages/mui-base/src/Popover/Positioner/PopoverPositionerContext.ts new file mode 100644 index 0000000000..a00f473694 --- /dev/null +++ b/packages/mui-base/src/Popover/Positioner/PopoverPositionerContext.ts @@ -0,0 +1,16 @@ +import * as React from 'react'; +import type { PopoverPositionerContextValue } from './PopoverPositioner.types'; + +export const PopoverPositionerContext = React.createContext( + null, +); + +export function usePopoverPositionerContext() { + const context = React.useContext(PopoverPositionerContext); + if (context === null) { + throw new Error( + ' and must be used within the component', + ); + } + return context; +} diff --git a/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx b/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx index f80d5a8611..c20e8ae6d0 100644 --- a/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx +++ b/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.tsx @@ -32,7 +32,7 @@ import { usePopoverRootContext } from '../Root/PopoverRootContext'; * * API: * - * - [usePopoverPositioner API](https://mui.com/base-ui/react-popover/hooks-api/#use-popover-popup) + * - [usePopoverPositioner API](https://mui.com/base-ui/react-popover/hooks-api/#use-popover-positioner) */ export function usePopoverPositioner( params: UsePopoverPositionerParameters, @@ -52,7 +52,7 @@ export function usePopoverPositioner( arrowPadding = 5, } = params; - const { mounted, setMounted, rootContext, getRootPopupProps } = usePopoverRootContext(); + const { mounted, rootContext, getRootPopupProps } = usePopoverRootContext(); // Using a ref assumes that the arrow element is always present in the DOM for the lifetime of the // tooltip. If this assumption ends up being false, we can switch to state to manage the arrow's @@ -112,12 +112,15 @@ export function usePopoverPositioner( }); }, }), - arrow(() => ({ - // `transform-origin` calculations rely on an element existing. If the arrow hasn't been set, - // we'll create a fake element. - element: arrowRef.current || document.createElement('div'), - padding: arrowPadding, - })), + arrow( + () => ({ + // `transform-origin` calculations rely on an element existing. If the arrow hasn't been set, + // we'll create a fake element. + element: arrowRef.current || document.createElement('div'), + padding: arrowPadding, + }), + [arrowPadding], + ), hideWhenDetached && hide(), { name: 'transformOrigin', @@ -184,37 +187,27 @@ export function usePopoverPositioner( const renderedSide = getSide(renderedPlacement); const renderedAlignment = getAlignment(renderedPlacement) || 'center'; const isHidden = hideWhenDetached && middlewareData.hide?.referenceHidden; - // TODO: While in the instant phase, if the tooltip is closing and no other tooltip is opening, - // the `instantType` should be `undefined`. This ensures the close animation will play. This may - // need an internal fix in Floating UI. - const getPopupProps: UsePopoverPositionerReturnValue['getPopupProps'] = React.useCallback( - (externalProps = {}) => { - function handleTransitionOrAnimationEnd({ target }: React.SyntheticEvent) { - const popupElement = refs.floating.current?.firstElementChild; - if (target === popupElement) { - setMounted((prevMounted) => (prevMounted ? false : prevMounted)); - } - } - - return mergeReactProps( - externalProps, - getRootPopupProps({ - style: { - ...floatingStyles, - maxWidth: 'var(--available-width)', - maxHeight: 'var(--available-height)', - visibility: isHidden ? 'hidden' : undefined, - pointerEvents: isHidden || followCursorAxis === 'both' ? 'none' : undefined, - zIndex: 2147483647, // max z-index - }, - onTransitionEnd: handleTransitionOrAnimationEnd, - onAnimationEnd: handleTransitionOrAnimationEnd, - }), - ); - }, - [getRootPopupProps, floatingStyles, isHidden, followCursorAxis, setMounted, refs], - ); + const getPositionerProps: UsePopoverPositionerReturnValue['getPositionerProps'] = + React.useCallback( + (externalProps = {}) => { + return mergeReactProps( + externalProps, + getRootPopupProps({ + role: 'presentation', + style: { + ...floatingStyles, + maxWidth: 'var(--available-width)', + maxHeight: 'var(--available-height)', + visibility: isHidden ? 'hidden' : undefined, + pointerEvents: isHidden || followCursorAxis === 'both' ? 'none' : undefined, + zIndex: 2147483647, // max z-index + }, + }), + ); + }, + [getRootPopupProps, floatingStyles, isHidden, followCursorAxis], + ); const getArrowProps: UsePopoverPositionerReturnValue['getArrowProps'] = React.useCallback( (externalProps = {}) => { @@ -234,7 +227,7 @@ export function usePopoverPositioner( return React.useMemo( () => ({ mounted, - getPopupProps, + getPositionerProps, getArrowProps, arrowRef, arrowUncentered, @@ -244,7 +237,7 @@ export function usePopoverPositioner( }), [ mounted, - getPopupProps, + getPositionerProps, getArrowProps, arrowUncentered, renderedSide, diff --git a/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.types.ts b/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.types.ts index b991a83dda..0e684d55b0 100644 --- a/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.types.ts +++ b/packages/mui-base/src/Popover/Positioner/usePopoverPositioner.types.ts @@ -8,7 +8,7 @@ export interface PopoverPositionerParameters { */ open?: boolean; /** - * The anchor element to which the popover content will be placed at. + * The anchor element to which the popover popup will be placed at. */ anchor?: | Element @@ -68,8 +68,8 @@ export interface PopoverPositionerParameters { */ followCursorAxis?: 'none' | 'both' | 'x' | 'y'; /** - * Determines the padding between the arrow and the popover content. Useful when the popover - * has rounded corners via `border-radius`. + * Determines the padding between the arrow and the popover popup's edges. Useful when the popover + * popup has rounded corners via `border-radius`. * @default 5 */ arrowPadding?: number; @@ -84,9 +84,9 @@ export interface UsePopoverPositionerParameters extends PopoverPositionerParamet export interface UsePopoverPositionerReturnValue { /** - * Props to spread on the popover content element. + * Props to spread on the popover positioner element. */ - getPopupProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; + getPositionerProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps; /** * Props to spread on the popover arrow element. */ diff --git a/packages/mui-base/src/Popover/Group/PopoverGroup.tsx b/packages/mui-base/src/Popover/Provider/PopoverProvider.tsx similarity index 75% rename from packages/mui-base/src/Popover/Group/PopoverGroup.tsx rename to packages/mui-base/src/Popover/Provider/PopoverProvider.tsx index dadc594c02..b1535b4328 100644 --- a/packages/mui-base/src/Popover/Group/PopoverGroup.tsx +++ b/packages/mui-base/src/Popover/Provider/PopoverProvider.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { FloatingDelayGroup } from '@floating-ui/react'; -import type { PopoverGroupProps } from './PopoverGroup.types'; +import type { PopoverProviderProps } from './PopoverProvider.types'; /** - * Groups popovers' delays together so that once one of the popovers opens, subsequent popovers will - * not open with a delay. + * Provides a shared delay for popovers so that once a popover is shown, the rest of the popovers in + * the group will not wait for the delay before showing. * * Demos: * @@ -16,7 +16,7 @@ import type { PopoverGroupProps } from './PopoverGroup.types'; * * - [PopoverGroup API](https://mui.com/base-ui/react-popover/components-api/#popover-group) */ -function PopoverGroup(props: PopoverGroupProps) { +function PopoverProvider(props: PopoverProviderProps) { const { delay = 0, closeDelay = 0, timeout = 400 } = props; return ( @@ -25,7 +25,7 @@ function PopoverGroup(props: PopoverGroupProps) { ); } -PopoverGroup.propTypes /* remove-proptypes */ = { +PopoverProvider.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ @@ -35,12 +35,12 @@ PopoverGroup.propTypes /* remove-proptypes */ = { */ children: PropTypes.node, /** - * The delay in milliseconds until the popover content is closed. + * The delay in milliseconds until the popover popup is closed. * @default 0 */ closeDelay: PropTypes.number, /** - * The delay in milliseconds until popovers within the group are open. + * The delay in milliseconds until the popover popup is opened. * @default 0 */ delay: PropTypes.number, @@ -52,4 +52,4 @@ PopoverGroup.propTypes /* remove-proptypes */ = { timeout: PropTypes.number, } as any; -export { PopoverGroup }; +export { PopoverProvider }; diff --git a/packages/mui-base/src/Popover/Group/PopoverGroup.types.ts b/packages/mui-base/src/Popover/Provider/PopoverProvider.types.ts similarity index 63% rename from packages/mui-base/src/Popover/Group/PopoverGroup.types.ts rename to packages/mui-base/src/Popover/Provider/PopoverProvider.types.ts index 52be77525e..da5e11e2e6 100644 --- a/packages/mui-base/src/Popover/Group/PopoverGroup.types.ts +++ b/packages/mui-base/src/Popover/Provider/PopoverProvider.types.ts @@ -1,12 +1,12 @@ -export interface PopoverGroupProps { +export interface PopoverProviderProps { children?: React.ReactNode; /** - * The delay in milliseconds until popovers within the group are open. + * The delay in milliseconds until the popover popup is opened. * @default 0 */ delay?: number; /** - * The delay in milliseconds until the popover content is closed. + * The delay in milliseconds until the popover popup is closed. * @default 0 */ closeDelay?: number; diff --git a/packages/mui-base/src/Popover/Root/PopoverRoot.test.tsx b/packages/mui-base/src/Popover/Root/PopoverRoot.test.tsx index 46f8419df3..4cf132f944 100644 --- a/packages/mui-base/src/Popover/Root/PopoverRoot.test.tsx +++ b/packages/mui-base/src/Popover/Root/PopoverRoot.test.tsx @@ -12,9 +12,7 @@ describe('', () => { it('should render the children', () => { render( - -
Content
-
+ Content
, ); @@ -25,10 +23,10 @@ describe('', () => { it('should open when the anchor is clicked', async () => { render( - -