Skip to content

Commit ba0fc4a

Browse files
unekinnBarsnes
andauthored
fix(Popover): unnecesary call of onOpen and missing call of onClose (#4230)
Co-authored-by: Tobias Barsnes <tobias.barsnes@digdir.no>
1 parent 160ff38 commit ba0fc4a

File tree

4 files changed

+108
-5
lines changed

4 files changed

+108
-5
lines changed

.changeset/stupid-birds-read.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@digdir/designsystemet-react": patch
3+
---
4+
5+
**Popover**: Fix unnecesary call of `onOpen` and missing call of `onClose`
6+
7+
- Don't call `onOpen` when clicking `Popover.Trigger` when `Popover` is already open.
8+
- Call `onClose` when a controlled `Popover` is closed by clicking on `Popover.Trigger`.

packages/react/src/components/popover/popover.stories.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,13 @@ export const Controlled: StoryFn<typeof Popover> = () => {
188188

189189
return (
190190
<Popover.TriggerContext>
191-
<Popover.Trigger onClick={() => setOpen(!open)}>Slett</Popover.Trigger>
192-
<Popover open={open} onClose={() => setOpen(false)} data-color='neutral'>
191+
<Popover.Trigger>Slett</Popover.Trigger>
192+
<Popover
193+
open={open}
194+
onOpen={() => setOpen(true)}
195+
onClose={() => setOpen(false)}
196+
data-color='neutral'
197+
>
193198
<Paragraph>Er du sikker på at du vil slette?</Paragraph>
194199
<div
195200
style={{

packages/react/src/components/popover/popover.test.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,38 @@ const CompControlled = () => {
2323
);
2424
};
2525

26+
type ControlledWithTriggerProps = {
27+
isInitiallyOpen: boolean;
28+
onOpen?: () => void;
29+
onClose?: () => void;
30+
};
31+
32+
const CompControlledWithTrigger = (props: ControlledWithTriggerProps) => {
33+
const [open, setOpen] = useState(props.isInitiallyOpen);
34+
35+
return (
36+
<Popover.TriggerContext>
37+
<Popover.Trigger>
38+
Dropdown
39+
{open ? <ChevronDownIcon aria-hidden /> : <ChevronUpIcon aria-hidden />}
40+
</Popover.Trigger>
41+
<Popover
42+
open={open}
43+
onOpen={() => {
44+
props.onOpen?.();
45+
setOpen(true);
46+
}}
47+
onClose={() => {
48+
props.onClose?.();
49+
setOpen(false);
50+
}}
51+
>
52+
{contentText}
53+
</Popover>
54+
</Popover.TriggerContext>
55+
);
56+
};
57+
2658
const contentText = 'popover content';
2759

2860
const render = async (props: PopoverProps = {}) => {
@@ -41,6 +73,19 @@ const render = async (props: PopoverProps = {}) => {
4173
};
4274
};
4375

76+
const renderControlledWithTrigger = async (
77+
props: ControlledWithTriggerProps,
78+
) => {
79+
/* Flush microtasks */
80+
await act(async () => {});
81+
const user = userEvent.setup();
82+
83+
return {
84+
user,
85+
...renderRtl(<CompControlledWithTrigger {...props} />),
86+
};
87+
};
88+
4489
describe('Popover', () => {
4590
it('should render popover on trigger-click when closed', async () => {
4691
const { user } = await render();
@@ -177,4 +222,48 @@ describe('Popover', () => {
177222
await act(async () => await user.click(document.body));
178223
expect(onClose).toHaveBeenCalledTimes(1);
179224
});
225+
226+
it('should not call onOpen when the popover is open', async () => {
227+
const onOpen = vi.fn();
228+
const { user } = await render({ onOpen });
229+
const popoverTrigger = screen.getByRole('button');
230+
231+
await act(async () => await user.click(document.body));
232+
expect(onOpen).toHaveBeenCalledTimes(0);
233+
234+
await act(async () => await user.click(popoverTrigger));
235+
expect(screen.queryByText(contentText)).toBeVisible();
236+
expect(onOpen).toHaveBeenCalledTimes(1);
237+
await act(async () => await user.click(popoverTrigger));
238+
expect(onOpen).toHaveBeenCalledTimes(1);
239+
});
240+
241+
describe('with controlled state', () => {
242+
it('should not call onClose when the popover is closed', async () => {
243+
const onClose = vi.fn();
244+
const { user } = await renderControlledWithTrigger({
245+
isInitiallyOpen: false,
246+
onClose,
247+
});
248+
const popoverTrigger = screen.getByRole('button');
249+
250+
await act(async () => await user.click(popoverTrigger));
251+
expect(onClose).toHaveBeenCalledTimes(0);
252+
await act(async () => await user.click(popoverTrigger));
253+
expect(onClose).toHaveBeenCalledTimes(1);
254+
});
255+
256+
it('should not call onOpen when the popover is open', async () => {
257+
const onOpen = vi.fn();
258+
const { user } = await renderControlledWithTrigger({
259+
isInitiallyOpen: true,
260+
onOpen,
261+
});
262+
const popoverTrigger = screen.getByRole('button');
263+
264+
expect(screen.queryByText(contentText)).toBeVisible();
265+
await act(async () => await user.click(popoverTrigger));
266+
expect(onOpen).toHaveBeenCalledTimes(0);
267+
});
268+
});
180269
});

packages/react/src/components/popover/popover.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,13 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
131131

132132
if (isTrigger) {
133133
event.preventDefault(); // Prevent native Popover API
134-
setInternalOpen((open) => !open);
135-
onOpen?.();
136134
}
137-
if (isOutside && internalOpen) {
135+
if (controlledOpen && (isTrigger || isOutside)) {
138136
setInternalOpen(false);
139137
onClose?.();
138+
} else if (isTrigger) {
139+
setInternalOpen(true);
140+
onOpen?.();
140141
}
141142
};
142143

0 commit comments

Comments
 (0)