Skip to content

Commit 956c65b

Browse files
committed
Allow to customize initial Popover placement
1 parent 7109fdc commit 956c65b

File tree

4 files changed

+111
-15
lines changed

4 files changed

+111
-15
lines changed

src/components/feedback/Popover.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ function usePopoverPositioning(
4141
popoverOpen: boolean,
4242
asNativePopover: boolean,
4343
alignToRight: boolean,
44+
placement: 'above' | 'below',
4445
) {
4546
const adjustPopoverPositioning = useCallback(() => {
4647
const popoverEl = popoverRef.current!;
@@ -71,11 +72,15 @@ function usePopoverPositioning(
7172
const { height: popoverHeight, width: popoverWidth } =
7273
popoverEl.getBoundingClientRect();
7374

74-
// The popover should render above only if there's not enough space below to
75-
// fit it and there's more absolute space above than below
75+
// The popover should render in indicated placement unless there's not
76+
// enough space to fit it there, but there is in the opposite one.
7677
const shouldBeAbove =
77-
anchorElDistanceToBottom < popoverHeight &&
78-
anchorElDistanceToTop > anchorElDistanceToBottom;
78+
(placement === 'above' &&
79+
(anchorElDistanceToTop > popoverHeight ||
80+
anchorElDistanceToBottom < anchorElDistanceToTop)) ||
81+
(placement === 'below' &&
82+
anchorElDistanceToBottom < popoverHeight &&
83+
anchorElDistanceToTop > anchorElDistanceToBottom);
7984

8085
if (!asNativePopover) {
8186
// Set styles for non-popover mode
@@ -126,7 +131,7 @@ function usePopoverPositioning(
126131
: `calc(${absBodyTop + anchorElDistanceToTop + anchorElHeight}px + ${POPOVER_ANCHOR_EL_GAP})`,
127132
left: `${Math.max(POPOVER_VIEWPORT_HORIZONTAL_GAP, left)}px`,
128133
});
129-
}, [asNativePopover, anchorElementRef, popoverRef, alignToRight]);
134+
}, [asNativePopover, anchorElementRef, popoverRef, alignToRight, placement]);
130135

131136
useLayoutEffect(() => {
132137
if (!popoverOpen) {
@@ -250,6 +255,16 @@ export type PopoverProps = {
250255
*/
251256
align?: 'right' | 'left';
252257

258+
/**
259+
* Where to position the popover if there's available space: above the anchor
260+
* or below it.
261+
* Defaults to 'below'.
262+
*
263+
* If there's no space to display the popover in selected placement, an
264+
* alternative placement will be used to keep it inside the viewport.
265+
*/
266+
placement?: 'above' | 'below';
267+
253268
/**
254269
* Determines if focus should be restored when the popover is closed.
255270
* Defaults to true.
@@ -330,6 +345,7 @@ export default function Popover({
330345
open,
331346
onClose,
332347
align = 'left',
348+
placement = 'below',
333349
classes,
334350
variant = 'panel',
335351
onScroll,
@@ -345,6 +361,7 @@ export default function Popover({
345361
open,
346362
asNativePopover,
347363
align === 'right',
364+
placement,
348365
);
349366
useOnClose(popoverRef, anchorElementRef, onClose, open, asNativePopover);
350367
useRestoreFocusOnClose({

src/components/feedback/test/Popover-test.js

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,17 +188,33 @@ describe('Popover', () => {
188188
asNativePopover: false,
189189
shouldBeAbove: true,
190190
},
191-
].forEach(({ containerPaddingTop, asNativePopover, shouldBeAbove }) => {
192-
it('positions popover above or below based on available space', () => {
193-
const wrapper = createComponent(
194-
{ asNativePopover },
195-
{ paddingTop: containerPaddingTop },
196-
);
197-
togglePopover(wrapper);
198191

199-
assert.equal(popoverAppearedAbove(wrapper), shouldBeAbove);
200-
});
201-
});
192+
// placement: 'above'
193+
{
194+
containerPaddingTop: 1000,
195+
asNativePopover: true,
196+
shouldBeAbove: true,
197+
placement: 'above',
198+
},
199+
{
200+
containerPaddingTop: 0,
201+
asNativePopover: true,
202+
shouldBeAbove: false,
203+
placement: 'above',
204+
},
205+
].forEach(
206+
({ containerPaddingTop, asNativePopover, shouldBeAbove, placement }) => {
207+
it('positions popover above or below based on available space and placement', () => {
208+
const wrapper = createComponent(
209+
{ asNativePopover, placement },
210+
{ paddingTop: containerPaddingTop },
211+
);
212+
togglePopover(wrapper);
213+
214+
assert.equal(popoverAppearedAbove(wrapper), shouldBeAbove);
215+
});
216+
},
217+
);
202218

203219
[true, false].forEach(asNativePopover => {
204220
it('repositions popover if it is resized after being open', async () => {

src/pattern-library/components/patterns/feedback/PopoverPage.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,25 @@ export default function PopoverPage() {
174174
</Library.InfoItem>
175175
</Library.Info>
176176
</Library.SectionL3>
177+
<Library.SectionL3 title="placement">
178+
<Library.Info>
179+
<Library.InfoItem label="description">
180+
Whether the <code>Popover</code> should show above or below the
181+
anchor element.
182+
</Library.InfoItem>
183+
<Library.InfoItem label="type">
184+
<code>{"'above' | 'below'"}</code>
185+
</Library.InfoItem>
186+
<Library.InfoItem label="default">
187+
<code>{"'below'"}</code>
188+
</Library.InfoItem>
189+
</Library.Info>
190+
<Library.Demo
191+
title="Popover placement"
192+
exampleFile="popover-placement"
193+
withSource
194+
/>
195+
</Library.SectionL3>
177196
<Library.SectionL3 title="restoreFocusOnClose">
178197
<Library.Info>
179198
<Library.InfoItem label="description">
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useRef, useState } from 'preact/hooks';
2+
3+
import { Popover } from '../../components/feedback';
4+
import { Button } from '../../components/input';
5+
6+
function ButtonWithPopover({ placement }: { placement: 'above' | 'below' }) {
7+
const [open, setOpen] = useState(false);
8+
const buttonRef = useRef<HTMLButtonElement | null>(null);
9+
10+
return (
11+
<div>
12+
<Button
13+
variant="primary"
14+
elementRef={buttonRef}
15+
onClick={() => setOpen(prev => !prev)}
16+
>
17+
{open ? 'Close' : 'Open'} {placement}
18+
</Button>
19+
<Popover
20+
open={open}
21+
onClose={() => setOpen(false)}
22+
anchorElementRef={buttonRef}
23+
placement={placement}
24+
>
25+
<div className="p-2 flex flex-col gap-y-2 w-64">
26+
<p>This popover is displayed {placement} the button.</p>
27+
<p>
28+
It will be displayed in the opposite direction if there is no room
29+
for it {placement}.
30+
</p>
31+
</div>
32+
</Popover>
33+
</div>
34+
);
35+
}
36+
37+
export default function App() {
38+
return (
39+
<div className="relative flex justify-center gap-x-3">
40+
<ButtonWithPopover placement="above" />
41+
<ButtonWithPopover placement="below" />
42+
</div>
43+
);
44+
}

0 commit comments

Comments
 (0)