Skip to content

Commit 60ef230

Browse files
committed
Add PopoverWrapper for basic Popover use cases (#45)
1 parent be35583 commit 60ef230

File tree

6 files changed

+139
-13
lines changed

6 files changed

+139
-13
lines changed

src/lib/components/Popover/Popover.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
@use "theme";
66

7+
.wrapper {
8+
position: relative;
9+
}
10+
711
.root {
812
position: absolute;
913
width: theme.$width;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import PropTypes from 'prop-types';
2+
import React from 'react';
3+
import { withGlobalProps } from '../../provider';
4+
import withForwardedRef from '../withForwardedRef';
5+
import styles from './Popover.scss';
6+
7+
export const PopoverWrapper = ({
8+
children,
9+
id,
10+
tag: Tag,
11+
...restProps
12+
}) => (
13+
<Tag
14+
className={styles.wrapper}
15+
id={id}
16+
{...restProps}
17+
>
18+
{children}
19+
</Tag>
20+
);
21+
22+
PopoverWrapper.defaultProps = {
23+
id: undefined,
24+
tag: 'div',
25+
};
26+
27+
PopoverWrapper.propTypes = {
28+
/**
29+
* Popover reference and the Popover itself.
30+
*/
31+
children: PropTypes.node.isRequired,
32+
/**
33+
* ID of the root HTML element.
34+
*/
35+
id: PropTypes.string,
36+
/**
37+
* HTML tag to render. Can be any valid HTML tag of your choice, usually a
38+
* [block-level element](https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements).
39+
*/
40+
tag: PropTypes.string,
41+
};
42+
43+
export const PopoverWrapperWithContext = withForwardedRef(withGlobalProps(PopoverWrapper, 'PopoverWrapper'));
44+
45+
export default PopoverWrapperWithContext;
46+

src/lib/components/Popover/README.mdx

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { SelectField } from '../SelectField/SelectField'
2222
import { Toolbar } from '../Toolbar/Toolbar'
2323
import { ToolbarItem } from '../Toolbar/ToolbarItem'
2424
import { Popover } from './Popover'
25+
import { PopoverWrapper } from './PopoverWrapper'
2526

2627
Popover displays additional information without interrupting user flow.
2728

@@ -30,7 +31,7 @@ Popover displays additional information without interrupting user flow.
3031
To implement the Popover component, you need to import it first:
3132

3233
```js
33-
import { Popover } from '@react-ui-org/react-ui';
34+
import { Popover, PopoverWrapper } from '@react-ui-org/react-ui';
3435
```
3536

3637
And use it:
@@ -48,7 +49,7 @@ And use it:
4849
minHeight: '10rem',
4950
}}
5051
>
51-
<div style={{position: 'relative'}}>
52+
<PopoverWrapper>
5253
<Button
5354
aria-describedby={isPopoverOpen ? 'my-popover' : undefined}
5455
label="Want to see a popover? Click me!"
@@ -59,7 +60,7 @@ And use it:
5960
Hello there!
6061
</Popover>
6162
)}
62-
</div>
63+
</PopoverWrapper>
6364
</div>
6465
);
6566
}}
@@ -69,12 +70,10 @@ See [API](#api) for all available options.
6970

7071
## Placement
7172

72-
By default, Popover is placed relative to the closest parent element with
73-
`position: relative` or `position: absolute`. Available placements are: top,
74-
right, bottom, and left. Additionally, all basic placements can be aligned to
75-
the center (default, no suffix), start (e.g. `top-start`), or end (e.g.
76-
`bottom-end`). Check Popover [API](#api) for the complete list of accepted
77-
values.
73+
Available placements are: top, right, bottom, and left. Additionally, all basic
74+
placements can be aligned to the center (default, no suffix), start (e.g.
75+
`top-start`), or end (e.g. `bottom-end`). Check Popover [API](#api) for the
76+
complete list of accepted values.
7877

7978
<Playground>
8079
{() => {
@@ -114,7 +113,7 @@ values.
114113
minHeight: '15rem',
115114
}}
116115
>
117-
<div style={{ position: 'relative' }}>
116+
<PopoverWrapper>
118117
<Placeholder bordered aria-describedby="my-popover-top">
119118
Popovers
120119
<br />
@@ -132,13 +131,50 @@ values.
132131
<Popover id="my-popover-left" placement={`left${align}`}>
133132
Left side
134133
</Popover>
135-
</div>
134+
</PopoverWrapper>
136135
</div>
137136
</>
138137
);
139138
}}
140139
</Playground>
141140

141+
## PopoverWrapper
142+
143+
PopoverWrapper is an optional wrapper to make positioning of Popover even
144+
easier.
145+
146+
By default, Popover is placed relative to the closest parent element with
147+
`position: relative` or `position: absolute`. Maybe you already have one of
148+
these in your CSS. PopoverWrapper is here for situations when you don't.
149+
150+
```jsx
151+
<PopoverWrapper>
152+
<Button
153+
aria-describedby={isPopoverOpen ? 'my-popover' : undefined}
154+
label="Want to see a popover? Click me!"
155+
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
156+
/>
157+
{isPopoverOpen && <Popover id="my-popover">Hello there!</Popover>}
158+
</PopoverWrapper>
159+
```
160+
161+
How do you know you may need PopoverWrapper?
162+
163+
- You are **not** rendering Popover in a React portal.
164+
- You are using Popover in a complex layout and it does not pop up where you
165+
need it.
166+
- You are using Floating UI with `absolute` positioning strategy (see
167+
[Advanced Positioning](#advanced-positioning) below) and your Popover keeps to
168+
be misplaced.
169+
- You have no idea what CSS `position` is and just want to get it working.
170+
171+
To sum it up, usually you will need either PopoverWrapper around your content or
172+
`position: [ relative | absolute ]` somewhere in your CSS (but you never need
173+
both!). Nevertheless, in the simplest situations, like in a single-column page
174+
layout, you may not need either of these at all.
175+
176+
Head to PopoverWrapper [API](#popoverwrapper-api) for all available options.
177+
142178
## Advanced Positioning
143179

144180
While the basic setup can be sufficient in some scenarios, dropping a Popover
@@ -277,6 +313,10 @@ get an idea of all possible cases you may need to cover.
277313

278314
<Props table of={Popover} />
279315

316+
### PopoverWrapper API
317+
318+
<Props table of={PopoverWrapper} />
319+
280320
## Theming
281321

282322
| Custom Property | Description |
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react';
2+
import {
3+
render,
4+
within,
5+
} from '@testing-library/react';
6+
import { idPropTest } from '../../../../../tests/propTests/idPropTest';
7+
import { tagPropTest } from '../../../../../tests/propTests/tagPropTest';
8+
import { PopoverWrapper } from '../PopoverWrapper';
9+
10+
const mandatoryProps = {
11+
children: <div>content text</div>,
12+
};
13+
14+
describe('rendering', () => {
15+
it.each([
16+
[
17+
{ children: <div>content text</div> },
18+
(rootElement) => expect(within(rootElement).getByText('content text')),
19+
],
20+
...idPropTest,
21+
...tagPropTest,
22+
])('renders with props: "%s"', (testedProps, assert) => {
23+
const dom = render((
24+
<PopoverWrapper
25+
{...mandatoryProps}
26+
{...testedProps}
27+
/>
28+
));
29+
30+
assert(dom.container.firstChild);
31+
});
32+
});
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export { default } from './Popover';
1+
export { default as Popover } from './Popover';
2+
export { default as PopoverWrapper } from './PopoverWrapper';

src/lib/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ export {
2929
} from './components/Media';
3030
export { default as Modal } from './components/Modal';
3131
export { default as Paper } from './components/Paper';
32-
export { default as Popover } from './components/Popover';
32+
export {
33+
Popover,
34+
PopoverWrapper,
35+
} from './components/Popover';
3336
export { default as Radio } from './components/Radio';
3437
export { default as ScrollView } from './components/ScrollView';
3538
export { default as SelectField } from './components/SelectField';

0 commit comments

Comments
 (0)