diff --git a/src/components/Popover/Popover.jsx b/src/components/Popover/Popover.jsx index d0d24768..f0f0acfd 100644 --- a/src/components/Popover/Popover.jsx +++ b/src/components/Popover/Popover.jsx @@ -12,24 +12,40 @@ export const Popover = React.forwardRef((props, ref) => { const { placement, children, + popoverTargetId, portalId, ...restProps } = props; const PopoverEl = ( -
+ {/** + * This hack is needed because the default behavior of the Popover API is to place the popover into a + * top-layer. It is currently not possible to position an element in the top-layer relative to a normal element. + * This will create a hidden browser popover, then with CSS it will open and close the RUI popover. + */} + {!!popoverTargetId && ( +
)} - ref={ref} - > - {children} - -
+
+ {children} + +
+ ); if (portalId === null) { @@ -41,6 +57,7 @@ export const Popover = React.forwardRef((props, ref) => { Popover.defaultProps = { placement: 'bottom', + popoverTargetId: null, portalId: null, }; @@ -67,6 +84,12 @@ Popover.propTypes = { 'left-start', 'left-end', ]), + /** + * If set, the popover will become controlled, meaning it will be hidden by default and will need a trigger to open. + * This sets the ID of the internal helper element for the popover. + * Assign the same ID to `popovertarget` of a trigger to make it open and close. + */ + popoverTargetId: PropTypes.string, /** * If set, popover is rendered in the React Portal with that ID. */ diff --git a/src/components/Popover/Popover.module.scss b/src/components/Popover/Popover.module.scss index 1c3d1873..b9b554be 100644 --- a/src/components/Popover/Popover.module.scss +++ b/src/components/Popover/Popover.module.scss @@ -1,6 +1,12 @@ -// 1. Reset positioning for controlled variant. -// 2. Shift Popover so there is space for the arrow between Popover and reference element. -// 3. Add top offset in case it's not defined by external library. +// 1. Hide the popover by default. This is needed because the popover is +// controlled via CSS with the help of the helper popover. The popover can't +// be displayed directly, because relative positioning doesn't work with +// elements on the top-layer, so this CSS hack is needed. +// 2. Hide the popover helper element. +// 3. If the popover helper is open, show the actual popover. +// 4. Reset positioning for controlled variant. +// 5. Shift Popover so there is space for the arrow between Popover and reference element. +// 6. Add top offset in case it's not defined by external library. @use "theme"; @@ -49,6 +55,28 @@ } } + // Controlled popover + .controlledPopover { + display: none; // 1. + } + + .helper { + position: fixed; // 2. + inset: unset; + top: 0; + right: 0; + width: auto; + height: auto; + padding: 0; + border: none; + background: transparent; + pointer-events: none; + } + + .helper:popover-open ~ .controlledPopover { + display: block; // 3. + } + // Sides .isRootAtTop { bottom: calc(100% + #{theme.$arrow-gap} - #{theme.$arrow-safe-rendering-overlap}); @@ -212,27 +240,27 @@ .isRootControlled.isRootAtBottom, .isRootControlled.isRootAtLeft, .isRootControlled.isRootAtRight { - inset: unset; // 1. + inset: unset; // 4. } .isRootControlled.isRootAtTop { - transform: translate(0, calc(-1 * #{theme.$arrow-height})); // 2. + transform: translate(0, calc(-1 * #{theme.$arrow-height})); // 5. } .isRootControlled.isRootAtBottom { - transform: translate(0, #{theme.$arrow-height}); // 2. + transform: translate(0, #{theme.$arrow-height}); // 5. } .isRootControlled.isRootAtLeft { - transform: translate(calc(-1 * #{theme.$arrow-height}), 0); // 2. + transform: translate(calc(-1 * #{theme.$arrow-height}), 0); // 5. } .isRootControlled.isRootAtRight { - transform: translate(#{theme.$arrow-height}, 0); // 2. + transform: translate(#{theme.$arrow-height}, 0); // 5. } .isRootControlled.isRootAtLeft.isRootAtStart, .isRootControlled.isRootAtRight.isRootAtStart { - top: 0; // 3. + top: 0; // 6. } } diff --git a/src/components/Popover/README.md b/src/components/Popover/README.md index dac06ce8..46f98c9a 100644 --- a/src/components/Popover/README.md +++ b/src/components/Popover/README.md @@ -284,6 +284,39 @@ React.createElement(() => { }); ``` +## Controlled Popover + +Popover API can be used to control visibility of Popover component. You need to +set `id` on the trigger element and matching `popoverTargetId` attribute on the +Popover component. This leverages the browser's Popover API to control the +popover, automatically closing it when the trigger or the backdrop is pressed. + +```docoff-react-preview +React.createElement(() => { + // All inline styles in this example are for demonstration purposes only. + return ( +
+ +
+ ); +}); +``` + ## Forwarding HTML Attributes In addition to the options below in the [component's API](#api) section, you