Skip to content

Commit 63a07c2

Browse files
committed
feat(ui-react): introduce Popover component
Signed-off-by: Simon Bruneaud <simon.bruneaud@ledger.fr>
1 parent 3690f69 commit 63a07c2

File tree

6 files changed

+816
-0
lines changed

6 files changed

+816
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
import * as React from 'react';
3+
import { Avatar } from '../Avatar/Avatar';
4+
import { Button } from '../Button/Button';
5+
import { Tag } from '../Tag';
6+
import {
7+
Popover,
8+
PopoverTrigger,
9+
PopoverContent,
10+
createPopoverHandle,
11+
} from './Popover';
12+
import type { PopoverProps } from './types';
13+
14+
const meta: Meta<typeof Popover> = {
15+
title: 'Containment/Popover',
16+
component: Popover,
17+
subcomponents: {
18+
PopoverTrigger,
19+
PopoverContent,
20+
},
21+
parameters: {
22+
layout: 'centered',
23+
backgrounds: {
24+
default: 'light',
25+
},
26+
},
27+
};
28+
29+
export default meta;
30+
31+
type Story = StoryObj<typeof Popover>;
32+
33+
const DefaultContent = () => {
34+
return (
35+
<div className='flex flex-col gap-24'>
36+
<div className='flex items-center gap-12'>
37+
<Avatar
38+
size='lg'
39+
src='https://plus.unsplash.com/premium_photo-1689551670902-19b441a6afde?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'
40+
/>
41+
<div>
42+
<div className='heading-4-semi-bold text-base'>John Doe</div>
43+
<Tag label='Status: Active' appearance='success' />
44+
</div>
45+
</div>
46+
47+
<div>
48+
<p className='heading-4-semi-bold text-base'>Notifications</p>
49+
<p className='body-2 text-muted'>
50+
You have 10 notifications in your account
51+
</p>
52+
</div>
53+
<div className='flex gap-12'>
54+
<Button size='sm' appearance='gray'>
55+
View all
56+
</Button>
57+
<Button size='sm' appearance='gray'>
58+
Settings
59+
</Button>
60+
</div>
61+
</div>
62+
);
63+
};
64+
65+
export const Base: Story = {
66+
render: (args: PopoverProps) => (
67+
<Popover {...args}>
68+
<PopoverTrigger>
69+
<Button appearance='gray'>Open Popover</Button>
70+
</PopoverTrigger>
71+
<PopoverContent>
72+
<DefaultContent />
73+
</PopoverContent>
74+
</Popover>
75+
),
76+
};
77+
78+
export const WidthShowcase: Story = {
79+
render: () => (
80+
<div className='flex items-center gap-16'>
81+
<Popover>
82+
<PopoverTrigger>
83+
<Button appearance='gray'>Hug (default)</Button>
84+
</PopoverTrigger>
85+
<PopoverContent width='hug'>
86+
<p className='body-2 text-base'>
87+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
88+
quos.
89+
</p>
90+
</PopoverContent>
91+
</Popover>
92+
93+
<Popover>
94+
<PopoverTrigger>
95+
<Button appearance='gray'>Hug (with custom width w-256)</Button>
96+
</PopoverTrigger>
97+
<PopoverContent width='hug' className='w-256'>
98+
<p className='body-2 text-base'>
99+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
100+
quos.
101+
</p>
102+
</PopoverContent>
103+
</Popover>
104+
105+
<Popover>
106+
<PopoverTrigger>
107+
<Button appearance='gray'>Fixed (max-w 400px)</Button>
108+
</PopoverTrigger>
109+
<PopoverContent width='fixed'>
110+
<p className='body-2 text-base'>
111+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
112+
quos.
113+
</p>
114+
</PopoverContent>
115+
</Popover>
116+
</div>
117+
),
118+
};
119+
120+
export const PositionShowcase: Story = {
121+
render: () => {
122+
const side = ['top', 'bottom', 'left', 'right'] as const;
123+
const align = ['end', 'center', 'start'] as const;
124+
125+
return (
126+
<div className='flex flex-col items-center gap-16'>
127+
{side.map((side) => (
128+
<div key={side} className='flex items-center gap-16'>
129+
{align.map((align) => (
130+
<Popover key={side} overlay={false}>
131+
<PopoverTrigger>
132+
<Button appearance='gray'>{`${side}-${align}`}</Button>
133+
</PopoverTrigger>
134+
<PopoverContent side={side} align={align}>
135+
<DefaultContent />
136+
</PopoverContent>
137+
</Popover>
138+
))}
139+
</div>
140+
))}
141+
</div>
142+
);
143+
},
144+
};
145+
146+
export const WithOverlay: Story = {
147+
render: () => (
148+
<div className='flex items-center gap-16'>
149+
<Popover overlay>
150+
<PopoverTrigger>
151+
<Button appearance='gray'>With Overlay (default)</Button>
152+
</PopoverTrigger>
153+
<PopoverContent width='fixed'>
154+
<DefaultContent />
155+
</PopoverContent>
156+
</Popover>
157+
158+
<Popover overlay={false}>
159+
<PopoverTrigger>
160+
<Button appearance='gray'>Without Overlay</Button>
161+
</PopoverTrigger>
162+
<PopoverContent width='fixed'>
163+
<DefaultContent />
164+
</PopoverContent>
165+
</Popover>
166+
</div>
167+
),
168+
};
169+
170+
export const Controlled: Story = {
171+
render: () => {
172+
const [open, setOpen] = React.useState(false);
173+
174+
return (
175+
<div className='flex items-center gap-16'>
176+
<Popover
177+
open={open}
178+
onOpenChange={(isOpen: boolean) => setOpen(isOpen)}
179+
>
180+
<PopoverTrigger>
181+
<Button appearance='gray'>{open ? 'Close' : 'Open'} Popover</Button>
182+
</PopoverTrigger>
183+
<PopoverContent>
184+
<div className='flex flex-col gap-16'>
185+
<DefaultContent />
186+
<Button
187+
size='sm'
188+
appearance='gray'
189+
onClick={() => setOpen(false)}
190+
>
191+
Close
192+
</Button>
193+
</div>
194+
</PopoverContent>
195+
</Popover>
196+
<span className='body-2 text-muted'>
197+
State: {open ? 'Open' : 'Closed'}
198+
</span>
199+
</div>
200+
);
201+
},
202+
};
203+
204+
export const WithRenderProp: Story = {
205+
render: () => (
206+
<Popover>
207+
<PopoverTrigger
208+
render={<Button appearance='gray'>Custom Trigger</Button>}
209+
/>
210+
<PopoverContent>
211+
<div className='flex flex-col gap-8'>
212+
<p className='heading-4-semi-bold text-base'>Render Prop</p>
213+
<p className='body-2 text-muted'>
214+
The trigger uses the render prop to compose with a Button component.
215+
</p>
216+
</div>
217+
</PopoverContent>
218+
</Popover>
219+
),
220+
};
221+
222+
export const DetachedTrigger: Story = {
223+
render: () => {
224+
const handle = createPopoverHandle<{ label: string }>();
225+
226+
return (
227+
<div className='flex items-center gap-16'>
228+
<PopoverTrigger handle={handle} payload={{ label: 'Button A' }}>
229+
<Button appearance='gray'>Trigger A</Button>
230+
</PopoverTrigger>
231+
232+
<PopoverTrigger handle={handle} payload={{ label: 'Button B' }}>
233+
<Button appearance='gray'>Trigger B</Button>
234+
</PopoverTrigger>
235+
236+
<Popover handle={handle}>
237+
{({ payload }: { payload: { label: string } | undefined }) => (
238+
<PopoverContent>
239+
<div className='flex flex-col gap-8'>
240+
<p className='heading-4-semi-bold text-base'>
241+
Detached Trigger
242+
</p>
243+
<p className='body-2 text-muted'>
244+
Opened by: {payload?.label ?? 'unknown'}
245+
</p>
246+
</div>
247+
</PopoverContent>
248+
)}
249+
</Popover>
250+
</div>
251+
);
252+
},
253+
};

0 commit comments

Comments
 (0)