Skip to content

Commit 275469d

Browse files
committed
Add controlled popover
1 parent d3264dd commit 275469d

File tree

3 files changed

+105
-21
lines changed

3 files changed

+105
-21
lines changed

src/components/Popover/Popover.jsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,40 @@ export const Popover = React.forwardRef((props, ref) => {
1212
const {
1313
placement,
1414
children,
15+
popoverTargetId,
1516
portalId,
1617
...restProps
1718
} = props;
1819

1920
const PopoverEl = (
20-
<div
21-
{...transferProps(restProps)}
22-
className={classNames(
23-
styles.root,
24-
ref && styles.isRootControlled,
25-
getRootSideClassName(placement, styles),
26-
getRootAlignmentClassName(placement, styles),
21+
<>
22+
{/**
23+
* This hack is needed because the default behavior of the Popover API is to place the popover into a
24+
* top-layer. It is currently not possible to position an element in the top-layer relative to a normal element.
25+
* This will create a hidden browser popover, then with CSS it will open and close the RUI popover.
26+
*/}
27+
{!!popoverTargetId && (
28+
<div
29+
className={styles.helper}
30+
id={popoverTargetId}
31+
popover="auto"
32+
/>
2733
)}
28-
ref={ref}
29-
>
30-
{children}
31-
<span className={styles.arrow} />
32-
</div>
34+
<div
35+
{...transferProps(restProps)}
36+
className={classNames(
37+
styles.root,
38+
ref && styles.isRootControlled,
39+
popoverTargetId && styles.controlledPopover,
40+
getRootSideClassName(placement, styles),
41+
getRootAlignmentClassName(placement, styles),
42+
)}
43+
ref={ref}
44+
>
45+
{children}
46+
<span className={styles.arrow} />
47+
</div>
48+
</>
3349
);
3450

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

4258
Popover.defaultProps = {
4359
placement: 'bottom',
60+
popoverTargetId: null,
4461
portalId: null,
4562
};
4663

@@ -67,6 +84,12 @@ Popover.propTypes = {
6784
'left-start',
6885
'left-end',
6986
]),
87+
/**
88+
* If set, the popover will become controlled, meaning it will be hidden by default and will need a trigger to open.
89+
* This sets the ID of the internal helper element for the popover.
90+
* Assign the same ID to `popovertarget` of a trigger to make it open and close.
91+
*/
92+
popoverTargetId: PropTypes.string,
7093
/**
7194
* If set, popover is rendered in the React Portal with that ID.
7295
*/

src/components/Popover/Popover.module.scss

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
// 1. Reset positioning for controlled variant.
2-
// 2. Shift Popover so there is space for the arrow between Popover and reference element.
3-
// 3. Add top offset in case it's not defined by external library.
1+
// 1. Hide the popover by default. This is needed because the popover is
2+
// controlled via CSS with the help of the helper popover. The popover can't
3+
// be displayed directly, because relative positioning doesn't work with
4+
// elements on the top-layer, so this CSS hack is needed.
5+
// 2. Hide the popover helper element.
6+
// 3. If the popover helper is open, show the actual popover.
7+
// 4. Reset positioning for controlled variant.
8+
// 5. Shift Popover so there is space for the arrow between Popover and reference element.
9+
// 6. Add top offset in case it's not defined by external library.
410

511
@use "theme";
612

@@ -49,6 +55,28 @@
4955
}
5056
}
5157

58+
// Controlled popover
59+
.controlledPopover {
60+
display: none; // 1.
61+
}
62+
63+
.helper {
64+
position: fixed; // 2.
65+
inset: unset;
66+
top: 0;
67+
right: 0;
68+
width: auto;
69+
height: auto;
70+
padding: 0;
71+
border: none;
72+
background: transparent;
73+
pointer-events: none;
74+
}
75+
76+
.helper:popover-open ~ .controlledPopover {
77+
display: block; // 3.
78+
}
79+
5280
// Sides
5381
.isRootAtTop {
5482
bottom: calc(100% + #{theme.$arrow-gap} - #{theme.$arrow-safe-rendering-overlap});
@@ -212,27 +240,27 @@
212240
.isRootControlled.isRootAtBottom,
213241
.isRootControlled.isRootAtLeft,
214242
.isRootControlled.isRootAtRight {
215-
inset: unset; // 1.
243+
inset: unset; // 4.
216244
}
217245

218246
.isRootControlled.isRootAtTop {
219-
transform: translate(0, calc(-1 * #{theme.$arrow-height})); // 2.
247+
transform: translate(0, calc(-1 * #{theme.$arrow-height})); // 5.
220248
}
221249

222250
.isRootControlled.isRootAtBottom {
223-
transform: translate(0, #{theme.$arrow-height}); // 2.
251+
transform: translate(0, #{theme.$arrow-height}); // 5.
224252
}
225253

226254
.isRootControlled.isRootAtLeft {
227-
transform: translate(calc(-1 * #{theme.$arrow-height}), 0); // 2.
255+
transform: translate(calc(-1 * #{theme.$arrow-height}), 0); // 5.
228256
}
229257

230258
.isRootControlled.isRootAtRight {
231-
transform: translate(#{theme.$arrow-height}, 0); // 2.
259+
transform: translate(#{theme.$arrow-height}, 0); // 5.
232260
}
233261

234262
.isRootControlled.isRootAtLeft.isRootAtStart,
235263
.isRootControlled.isRootAtRight.isRootAtStart {
236-
top: 0; // 3.
264+
top: 0; // 6.
237265
}
238266
}

src/components/Popover/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,39 @@ React.createElement(() => {
284284
});
285285
```
286286

287+
## Controlled Popover
288+
289+
Popover API can be used to control visibility of Popover component. You need to
290+
set `id` on the trigger element and matching `popoverTargetId` attribute on the
291+
Popover component. This leverages the browser's Popover API to control the
292+
popover, automatically closing it when the trigger or the backdrop is pressed.
293+
294+
```docoff-react-preview
295+
React.createElement(() => {
296+
// All inline styles in this example are for demonstration purposes only.
297+
return (
298+
<div
299+
style={{
300+
display: 'grid',
301+
placeContent: 'center',
302+
minWidth: '20rem',
303+
minHeight: '10rem',
304+
}}
305+
>
306+
<PopoverWrapper>
307+
<Button
308+
label="Want to see a popover? Click me!"
309+
popovertarget="my-popover-helper"
310+
/>
311+
<Popover id="my-popover" popoverTargetId="my-popover-helper">
312+
Hello there!
313+
</Popover>
314+
</PopoverWrapper>
315+
</div>
316+
);
317+
});
318+
```
319+
287320
## Forwarding HTML Attributes
288321

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

0 commit comments

Comments
 (0)