Skip to content

Commit

Permalink
Add controlled popover
Browse files Browse the repository at this point in the history
  • Loading branch information
atmelmicro committed Jan 10, 2025
1 parent d3264dd commit 684d5ab
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 21 deletions.
47 changes: 35 additions & 12 deletions src/components/Popover/Popover.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,40 @@ export const Popover = React.forwardRef((props, ref) => {
const {
placement,
children,
popoverTargetId,
portalId,
...restProps
} = props;

const PopoverEl = (
<div
{...transferProps(restProps)}
className={classNames(
styles.root,
ref && styles.isRootControlled,
getRootSideClassName(placement, styles),
getRootAlignmentClassName(placement, styles),
<>
{/**
* 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 && (
<div
className={styles.helper}
id={popoverTargetId}
popover="auto"
/>
)}
ref={ref}
>
{children}
<span className={styles.arrow} />
</div>
<div
{...transferProps(restProps)}
className={classNames(
styles.root,
ref && styles.isRootControlled,
popoverTargetId && styles.controlledPopover,
getRootSideClassName(placement, styles),
getRootAlignmentClassName(placement, styles),
)}
ref={ref}
>
{children}
<span className={styles.arrow} />
</div>
</>
);

if (portalId === null) {
Expand All @@ -41,6 +57,7 @@ export const Popover = React.forwardRef((props, ref) => {

Popover.defaultProps = {
placement: 'bottom',
popoverTargetId: null,
portalId: null,
};

Expand All @@ -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.
*/
Expand Down
46 changes: 37 additions & 9 deletions src/components/Popover/Popover.module.scss
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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});
Expand Down Expand Up @@ -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.
}
}
33 changes: 33 additions & 0 deletions src/components/Popover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
style={{
display: 'grid',
placeContent: 'center',
minWidth: '20rem',
minHeight: '10rem',
}}
>
<PopoverWrapper>
<Button
label="Want to see a popover? Click me!"
popovertarget="my-popover-helper"
/>
<Popover id="my-popover" popoverTargetId="my-popover-helper">
Hello there!
</Popover>
</PopoverWrapper>
</div>
);
});
```

## Forwarding HTML Attributes

In addition to the options below in the [component's API](#api) section, you
Expand Down

0 comments on commit 684d5ab

Please sign in to comment.