@arbor-education/design-system.components 0.3.6 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +9 -0
- package/dist/components/avatar/Avatar.d.ts +11 -0
- package/dist/components/avatar/Avatar.d.ts.map +1 -0
- package/dist/components/avatar/Avatar.js +17 -0
- package/dist/components/avatar/Avatar.js.map +1 -0
- package/dist/components/avatar/Avatar.stories.d.ts +14 -0
- package/dist/components/avatar/Avatar.stories.d.ts.map +1 -0
- package/dist/components/avatar/Avatar.stories.js +66 -0
- package/dist/components/avatar/Avatar.stories.js.map +1 -0
- package/dist/components/avatar/Avatar.test.d.ts +2 -0
- package/dist/components/avatar/Avatar.test.d.ts.map +1 -0
- package/dist/components/avatar/Avatar.test.js +51 -0
- package/dist/components/avatar/Avatar.test.js.map +1 -0
- package/dist/components/banner/Banner.d.ts +19 -0
- package/dist/components/banner/Banner.d.ts.map +1 -0
- package/dist/components/banner/Banner.js +33 -0
- package/dist/components/banner/Banner.js.map +1 -0
- package/dist/components/banner/Banner.stories.d.ts +72 -0
- package/dist/components/banner/Banner.stories.d.ts.map +1 -0
- package/dist/components/banner/Banner.stories.js +84 -0
- package/dist/components/banner/Banner.stories.js.map +1 -0
- package/dist/components/banner/Banner.test.d.ts +2 -0
- package/dist/components/banner/Banner.test.d.ts.map +1 -0
- package/dist/components/banner/Banner.test.js +72 -0
- package/dist/components/banner/Banner.test.js.map +1 -0
- package/dist/components/dropdown/Dropdown.d.ts +2 -0
- package/dist/components/dropdown/Dropdown.d.ts.map +1 -1
- package/dist/components/dropdown/Dropdown.js +5 -1
- package/dist/components/dropdown/Dropdown.js.map +1 -1
- package/dist/components/dropdown/items/DropdownGroup.d.ts +3 -0
- package/dist/components/dropdown/items/DropdownGroup.d.ts.map +1 -0
- package/dist/components/dropdown/items/DropdownGroup.js +8 -0
- package/dist/components/dropdown/items/DropdownGroup.js.map +1 -0
- package/dist/components/dropdown/items/DropdownSeparator.d.ts +3 -0
- package/dist/components/dropdown/items/DropdownSeparator.d.ts.map +1 -0
- package/dist/components/dropdown/items/DropdownSeparator.js +8 -0
- package/dist/components/dropdown/items/DropdownSeparator.js.map +1 -0
- package/dist/components/userDropdown/UserDropdown.d.ts +47 -0
- package/dist/components/userDropdown/UserDropdown.d.ts.map +1 -0
- package/dist/components/userDropdown/UserDropdown.js +13 -0
- package/dist/components/userDropdown/UserDropdown.js.map +1 -0
- package/dist/components/userDropdown/UserDropdown.stories.d.ts +12 -0
- package/dist/components/userDropdown/UserDropdown.stories.d.ts.map +1 -0
- package/dist/components/userDropdown/UserDropdown.stories.js +222 -0
- package/dist/components/userDropdown/UserDropdown.stories.js.map +1 -0
- package/dist/components/userDropdown/UserDropdown.test.d.ts +2 -0
- package/dist/components/userDropdown/UserDropdown.test.d.ts.map +1 -0
- package/dist/components/userDropdown/UserDropdown.test.js +197 -0
- package/dist/components/userDropdown/UserDropdown.test.js.map +1 -0
- package/dist/components/userDropdown/assets/arbor.png +0 -0
- package/dist/components/userDropdown/assets/govhub.png +0 -0
- package/dist/components/userDropdown/assets/key.png +0 -0
- package/dist/components/userDropdown/assets/logos.d.ts +7 -0
- package/dist/components/userDropdown/assets/logos.d.ts.map +1 -0
- package/dist/components/userDropdown/assets/logos.js +13 -0
- package/dist/components/userDropdown/assets/logos.js.map +1 -0
- package/dist/components/userDropdown/assets/robin.png +0 -0
- package/dist/components/userDropdown/assets/sampeople.png +0 -0
- package/dist/components/userDropdown/assets/timetabler.png +0 -0
- package/dist/components/userDropdown/internal/UserDropdownAppItem.d.ts +3 -0
- package/dist/components/userDropdown/internal/UserDropdownAppItem.d.ts.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownAppItem.js +9 -0
- package/dist/components/userDropdown/internal/UserDropdownAppItem.js.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.d.ts +9 -0
- package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.d.ts.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.js +11 -0
- package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.js.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownSignOut.d.ts +7 -0
- package/dist/components/userDropdown/internal/UserDropdownSignOut.d.ts.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownSignOut.js +9 -0
- package/dist/components/userDropdown/internal/UserDropdownSignOut.js.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownTrigger.d.ts +11 -0
- package/dist/components/userDropdown/internal/UserDropdownTrigger.d.ts.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownTrigger.js +10 -0
- package/dist/components/userDropdown/internal/UserDropdownTrigger.js.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownUserInfo.d.ts +8 -0
- package/dist/components/userDropdown/internal/UserDropdownUserInfo.d.ts.map +1 -0
- package/dist/components/userDropdown/internal/UserDropdownUserInfo.js +17 -0
- package/dist/components/userDropdown/internal/UserDropdownUserInfo.js.map +1 -0
- package/dist/index.css +463 -53
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/avatar/Avatar.stories.tsx +84 -0
- package/src/components/avatar/Avatar.test.tsx +60 -0
- package/src/components/avatar/Avatar.tsx +68 -0
- package/src/components/avatar/avatar.scss +71 -0
- package/src/components/banner/Banner.stories.tsx +96 -0
- package/src/components/banner/Banner.test.tsx +86 -0
- package/src/components/banner/Banner.tsx +81 -0
- package/src/components/banner/banner.scss +67 -0
- package/src/components/button/button.scss +0 -5
- package/src/components/card/card.scss +0 -3
- package/src/components/dropdown/Dropdown.tsx +5 -1
- package/src/components/dropdown/dropdown.scss +4 -4
- package/src/components/dropdown/items/DropdownGroup.tsx +11 -0
- package/src/components/dropdown/items/DropdownSeparator.tsx +9 -0
- package/src/components/formField/fieldset/fieldset.scss +0 -2
- package/src/components/formField/formField.scss +0 -2
- package/src/components/formField/inputs/checkbox/checkboxInput.scss +0 -2
- package/src/components/formField/inputs/input.scss +0 -3
- package/src/components/formField/inputs/radio/radioButtonInput.scss +0 -2
- package/src/components/formField/inputs/selectDropdown/selectDropdown.scss +0 -1
- package/src/components/formField/label/label.scss +0 -2
- package/src/components/modal/modal.scss +0 -3
- package/src/components/pill/pill.scss +0 -3
- package/src/components/searchBar/searchBar.scss +0 -3
- package/src/components/table/columnFilters/columnFilters.scss +0 -6
- package/src/components/table/pagination/pagination.scss +0 -4
- package/src/components/tabs/tabs.scss +0 -2
- package/src/components/tag/tag.scss +0 -3
- package/src/components/toast/toast.scss +0 -1
- package/src/components/tooltip/tooltip.scss +0 -3
- package/src/components/userDropdown/UserDropdown.stories.tsx +237 -0
- package/src/components/userDropdown/UserDropdown.test.tsx +349 -0
- package/src/components/userDropdown/UserDropdown.tsx +110 -0
- package/src/components/userDropdown/assets/arbor.png +0 -0
- package/src/components/userDropdown/assets/govhub.png +0 -0
- package/src/components/userDropdown/assets/key.png +0 -0
- package/src/components/userDropdown/assets/logos.ts +13 -0
- package/src/components/userDropdown/assets/robin.png +0 -0
- package/src/components/userDropdown/assets/sampeople.png +0 -0
- package/src/components/userDropdown/assets/timetabler.png +0 -0
- package/src/components/userDropdown/internal/UserDropdownAppItem.tsx +21 -0
- package/src/components/userDropdown/internal/UserDropdownCollapsibleSection.tsx +38 -0
- package/src/components/userDropdown/internal/UserDropdownSignOut.tsx +19 -0
- package/src/components/userDropdown/internal/UserDropdownTrigger.tsx +42 -0
- package/src/components/userDropdown/internal/UserDropdownUserInfo.tsx +60 -0
- package/src/components/userDropdown/userDropdown.scss +377 -0
- package/src/global.scss +9 -1
- package/src/index.scss +3 -0
- package/src/index.ts +5 -0
- package/tsconfig.json +1 -1
- package/vite-env.d.ts +31 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { expect, test, describe, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { UserDropdown } from './UserDropdown';
|
|
5
|
+
import '@testing-library/jest-dom/vitest';
|
|
6
|
+
|
|
7
|
+
describe('UserDropdown', () => {
|
|
8
|
+
const mockUser = {
|
|
9
|
+
name: 'John Doe',
|
|
10
|
+
subtitle: 'Software Engineer',
|
|
11
|
+
avatarInitials: 'JD',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const mockOnSignOut = vi.fn();
|
|
15
|
+
|
|
16
|
+
const clickTrigger = async (user: ReturnType<typeof userEvent.setup>) => {
|
|
17
|
+
const trigger = screen.getByRole('button', { expanded: false });
|
|
18
|
+
await user.click(trigger);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
test('renders with minimal props', async () => {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
|
|
24
|
+
render(
|
|
25
|
+
<UserDropdown
|
|
26
|
+
user={mockUser}
|
|
27
|
+
onSignOut={mockOnSignOut}
|
|
28
|
+
/>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
await clickTrigger(user);
|
|
32
|
+
|
|
33
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Software Engineer')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('Sign out')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('renders user info with avatar image', async () => {
|
|
39
|
+
const user = userEvent.setup();
|
|
40
|
+
|
|
41
|
+
render(
|
|
42
|
+
<UserDropdown
|
|
43
|
+
user={{
|
|
44
|
+
...mockUser,
|
|
45
|
+
avatarSrc: 'https://example.com/avatar.jpg',
|
|
46
|
+
avatarAlt: 'Test User',
|
|
47
|
+
}}
|
|
48
|
+
onSignOut={mockOnSignOut}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await clickTrigger(user);
|
|
53
|
+
|
|
54
|
+
const images = screen.getAllByRole('img', { hidden: true });
|
|
55
|
+
const avatar = images.find(img => img.getAttribute('src') === 'https://example.com/avatar.jpg');
|
|
56
|
+
expect(avatar).toBeDefined();
|
|
57
|
+
expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.jpg');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('renders with logo', () => {
|
|
61
|
+
render(
|
|
62
|
+
<UserDropdown
|
|
63
|
+
user={mockUser}
|
|
64
|
+
logoSrc="https://example.com/logo.png"
|
|
65
|
+
logoAlt="Company Logo"
|
|
66
|
+
onSignOut={mockOnSignOut}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const logo = screen.getByAltText('Company Logo');
|
|
71
|
+
expect(logo).toBeInTheDocument();
|
|
72
|
+
expect(logo).toHaveAttribute('src', 'https://example.com/logo.png');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('renders without role', async () => {
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
|
|
78
|
+
render(
|
|
79
|
+
<UserDropdown
|
|
80
|
+
user={{ name: 'John Doe', avatarInitials: 'JD' }}
|
|
81
|
+
onSignOut={mockOnSignOut}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await clickTrigger(user);
|
|
86
|
+
|
|
87
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
88
|
+
expect(screen.queryByText('Software Engineer')).not.toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('renders app sections', async () => {
|
|
92
|
+
const user = userEvent.setup();
|
|
93
|
+
|
|
94
|
+
render(
|
|
95
|
+
<UserDropdown
|
|
96
|
+
user={mockUser}
|
|
97
|
+
sections={[
|
|
98
|
+
{
|
|
99
|
+
label: 'My Apps',
|
|
100
|
+
apps: [
|
|
101
|
+
{ name: 'App 1', onClick: vi.fn() },
|
|
102
|
+
{ name: 'App 2', onClick: vi.fn() },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
]}
|
|
106
|
+
onSignOut={mockOnSignOut}
|
|
107
|
+
/>,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await clickTrigger(user);
|
|
111
|
+
|
|
112
|
+
expect(screen.getByText('My Apps')).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('App 1')).toBeInTheDocument();
|
|
114
|
+
expect(screen.getByText('App 2')).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('renders app item with description', async () => {
|
|
118
|
+
const user = userEvent.setup();
|
|
119
|
+
|
|
120
|
+
render(
|
|
121
|
+
<UserDropdown
|
|
122
|
+
user={mockUser}
|
|
123
|
+
sections={[
|
|
124
|
+
{
|
|
125
|
+
apps: [
|
|
126
|
+
{ name: 'My App', description: 'A great app', onClick: vi.fn() },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
]}
|
|
130
|
+
onSignOut={mockOnSignOut}
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
await clickTrigger(user);
|
|
135
|
+
|
|
136
|
+
expect(screen.getByText('My App')).toBeInTheDocument();
|
|
137
|
+
expect(screen.getByText('A great app')).toBeInTheDocument();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('renders app item with logo', async () => {
|
|
141
|
+
const user = userEvent.setup();
|
|
142
|
+
|
|
143
|
+
render(
|
|
144
|
+
<UserDropdown
|
|
145
|
+
user={mockUser}
|
|
146
|
+
sections={[
|
|
147
|
+
{
|
|
148
|
+
apps: [
|
|
149
|
+
{
|
|
150
|
+
name: 'My App',
|
|
151
|
+
logo: <img src="logo.png" alt="Logo" />,
|
|
152
|
+
onClick: vi.fn(),
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
]}
|
|
157
|
+
onSignOut={mockOnSignOut}
|
|
158
|
+
/>,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
await clickTrigger(user);
|
|
162
|
+
|
|
163
|
+
expect(screen.getByAltText('Logo')).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('renders discover section collapsed by default', async () => {
|
|
167
|
+
const user = userEvent.setup();
|
|
168
|
+
|
|
169
|
+
render(
|
|
170
|
+
<UserDropdown
|
|
171
|
+
user={mockUser}
|
|
172
|
+
discoverSection={{
|
|
173
|
+
label: 'Discover',
|
|
174
|
+
apps: [
|
|
175
|
+
{ name: 'Hidden App', onClick: vi.fn() },
|
|
176
|
+
],
|
|
177
|
+
}}
|
|
178
|
+
onSignOut={mockOnSignOut}
|
|
179
|
+
/>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
await clickTrigger(user);
|
|
183
|
+
|
|
184
|
+
expect(screen.getByText('Discover')).toBeInTheDocument();
|
|
185
|
+
expect(screen.queryByText('Hidden App')).not.toBeInTheDocument();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('renders discover section open when defaultOpen is true', async () => {
|
|
189
|
+
const user = userEvent.setup();
|
|
190
|
+
|
|
191
|
+
render(
|
|
192
|
+
<UserDropdown
|
|
193
|
+
user={mockUser}
|
|
194
|
+
discoverSection={{
|
|
195
|
+
label: 'Discover',
|
|
196
|
+
defaultOpen: true,
|
|
197
|
+
apps: [
|
|
198
|
+
{ name: 'Visible App', onClick: vi.fn() },
|
|
199
|
+
],
|
|
200
|
+
}}
|
|
201
|
+
onSignOut={mockOnSignOut}
|
|
202
|
+
/>,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
await clickTrigger(user);
|
|
206
|
+
|
|
207
|
+
expect(screen.getByText('Discover')).toBeInTheDocument();
|
|
208
|
+
expect(screen.getByText('Visible App')).toBeInTheDocument();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('toggles discover section when clicked', async () => {
|
|
212
|
+
const user = userEvent.setup();
|
|
213
|
+
|
|
214
|
+
render(
|
|
215
|
+
<UserDropdown
|
|
216
|
+
user={mockUser}
|
|
217
|
+
discoverSection={{
|
|
218
|
+
label: 'Discover',
|
|
219
|
+
apps: [
|
|
220
|
+
{ name: 'Toggle App', onClick: vi.fn() },
|
|
221
|
+
],
|
|
222
|
+
}}
|
|
223
|
+
onSignOut={mockOnSignOut}
|
|
224
|
+
/>,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
await clickTrigger(user);
|
|
228
|
+
|
|
229
|
+
expect(screen.queryByText('Toggle App')).not.toBeInTheDocument();
|
|
230
|
+
|
|
231
|
+
await user.click(screen.getByText('Discover'));
|
|
232
|
+
expect(screen.getByText('Toggle App')).toBeInTheDocument();
|
|
233
|
+
|
|
234
|
+
await user.click(screen.getByText('Discover'));
|
|
235
|
+
expect(screen.queryByText('Toggle App')).not.toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('calls userInfoAction onClick when user info is clicked with link action', async () => {
|
|
239
|
+
const user = userEvent.setup();
|
|
240
|
+
const handleClick = vi.fn();
|
|
241
|
+
|
|
242
|
+
render(
|
|
243
|
+
<UserDropdown
|
|
244
|
+
user={mockUser}
|
|
245
|
+
userInfoAction={{ type: 'link', onClick: handleClick }}
|
|
246
|
+
onSignOut={mockOnSignOut}
|
|
247
|
+
/>,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
await clickTrigger(user);
|
|
251
|
+
|
|
252
|
+
await user.click(screen.getByText('John Doe'));
|
|
253
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('calls app onClick when app item is clicked', async () => {
|
|
257
|
+
const user = userEvent.setup();
|
|
258
|
+
const handleClick = vi.fn();
|
|
259
|
+
|
|
260
|
+
render(
|
|
261
|
+
<UserDropdown
|
|
262
|
+
user={mockUser}
|
|
263
|
+
sections={[
|
|
264
|
+
{
|
|
265
|
+
apps: [
|
|
266
|
+
{ name: 'My App', onClick: handleClick },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
]}
|
|
270
|
+
onSignOut={mockOnSignOut}
|
|
271
|
+
/>,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
await clickTrigger(user);
|
|
275
|
+
|
|
276
|
+
await user.click(screen.getByText('My App'));
|
|
277
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('calls onSignOut when sign out is clicked', async () => {
|
|
281
|
+
const user = userEvent.setup();
|
|
282
|
+
const handleSignOut = vi.fn();
|
|
283
|
+
|
|
284
|
+
render(
|
|
285
|
+
<UserDropdown
|
|
286
|
+
user={mockUser}
|
|
287
|
+
onSignOut={handleSignOut}
|
|
288
|
+
/>,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
await clickTrigger(user);
|
|
292
|
+
|
|
293
|
+
await user.click(screen.getByText('Sign out'));
|
|
294
|
+
expect(handleSignOut).toHaveBeenCalledTimes(1);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('renders custom sign out label', async () => {
|
|
298
|
+
const user = userEvent.setup();
|
|
299
|
+
|
|
300
|
+
render(
|
|
301
|
+
<UserDropdown
|
|
302
|
+
user={mockUser}
|
|
303
|
+
onSignOut={mockOnSignOut}
|
|
304
|
+
signOutLabel="Log out"
|
|
305
|
+
/>,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
await clickTrigger(user);
|
|
309
|
+
|
|
310
|
+
expect(screen.getByText('Log out')).toBeInTheDocument();
|
|
311
|
+
expect(screen.queryByText('Sign out')).not.toBeInTheDocument();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('renders complete dropdown structure', async () => {
|
|
315
|
+
const user = userEvent.setup();
|
|
316
|
+
|
|
317
|
+
render(
|
|
318
|
+
<UserDropdown
|
|
319
|
+
user={mockUser}
|
|
320
|
+
logoSrc="https://example.com/logo.png"
|
|
321
|
+
sections={[
|
|
322
|
+
{
|
|
323
|
+
label: 'Apps',
|
|
324
|
+
apps: [
|
|
325
|
+
{ name: 'App 1', onClick: vi.fn() },
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
]}
|
|
329
|
+
discoverSection={{
|
|
330
|
+
label: 'Discover',
|
|
331
|
+
apps: [
|
|
332
|
+
{ name: 'App 2', onClick: vi.fn() },
|
|
333
|
+
],
|
|
334
|
+
}}
|
|
335
|
+
userInfoAction={{ type: 'link', onClick: vi.fn() }}
|
|
336
|
+
onSignOut={mockOnSignOut}
|
|
337
|
+
signOutLabel="Sign out of Arbor"
|
|
338
|
+
/>,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
await clickTrigger(user);
|
|
342
|
+
|
|
343
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
344
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
345
|
+
expect(screen.getByText('Apps')).toBeInTheDocument();
|
|
346
|
+
expect(screen.getByText('App 1')).toBeInTheDocument();
|
|
347
|
+
expect(screen.getByText('Sign out of Arbor')).toBeInTheDocument();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Dropdown } from 'Components/dropdown/Dropdown';
|
|
3
|
+
import { UserDropdownTrigger } from './internal/UserDropdownTrigger';
|
|
4
|
+
import { UserDropdownUserInfo } from './internal/UserDropdownUserInfo';
|
|
5
|
+
import { UserDropdownAppItem } from './internal/UserDropdownAppItem';
|
|
6
|
+
import { UserDropdownCollapsibleSection } from './internal/UserDropdownCollapsibleSection';
|
|
7
|
+
import { UserDropdownSignOut } from './internal/UserDropdownSignOut';
|
|
8
|
+
|
|
9
|
+
export type UserDropdownUser = {
|
|
10
|
+
name: string;
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
avatarSrc?: string;
|
|
13
|
+
avatarAlt?: string;
|
|
14
|
+
avatarInitials?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type UserDropdownApp = {
|
|
18
|
+
logo?: React.ReactNode;
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
onClick?: () => void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type UserDropdownSection = {
|
|
25
|
+
label?: string;
|
|
26
|
+
apps: UserDropdownApp[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type UserDropdownUserInfoAction
|
|
30
|
+
= | { type: 'link'; onClick: () => void }
|
|
31
|
+
| { type: 'menu'; items: Array<{ label: string; onClick: () => void }> };
|
|
32
|
+
|
|
33
|
+
export type UserDropdownProps = {
|
|
34
|
+
user: UserDropdownUser;
|
|
35
|
+
logoSrc?: string;
|
|
36
|
+
logoAlt?: string;
|
|
37
|
+
sections?: UserDropdownSection[];
|
|
38
|
+
discoverSection?: {
|
|
39
|
+
label: string;
|
|
40
|
+
apps: UserDropdownApp[];
|
|
41
|
+
defaultOpen?: boolean;
|
|
42
|
+
};
|
|
43
|
+
userInfoAction?: UserDropdownUserInfoAction;
|
|
44
|
+
onSignOut: () => void;
|
|
45
|
+
signOutLabel?: string;
|
|
46
|
+
open?: boolean;
|
|
47
|
+
onOpenChange?: (open: boolean) => void;
|
|
48
|
+
variant?: 'dark' | 'light';
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const UserDropdown = (props: UserDropdownProps) => {
|
|
52
|
+
const {
|
|
53
|
+
user,
|
|
54
|
+
logoSrc,
|
|
55
|
+
logoAlt,
|
|
56
|
+
sections = [],
|
|
57
|
+
discoverSection,
|
|
58
|
+
userInfoAction,
|
|
59
|
+
onSignOut,
|
|
60
|
+
signOutLabel,
|
|
61
|
+
open,
|
|
62
|
+
onOpenChange,
|
|
63
|
+
variant = 'dark',
|
|
64
|
+
} = props;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Dropdown open={open} onOpenChange={onOpenChange}>
|
|
68
|
+
<UserDropdownTrigger
|
|
69
|
+
avatarSrc={user.avatarSrc}
|
|
70
|
+
avatarAlt={user.avatarAlt}
|
|
71
|
+
avatarInitials={user.avatarInitials}
|
|
72
|
+
logoSrc={logoSrc}
|
|
73
|
+
logoAlt={logoAlt}
|
|
74
|
+
variant={variant}
|
|
75
|
+
/>
|
|
76
|
+
|
|
77
|
+
<Dropdown.Content className="ds-user-dropdown__content" contentProps={{ sideOffset: 12 }}>
|
|
78
|
+
<UserDropdownUserInfo user={user} action={userInfoAction} />
|
|
79
|
+
|
|
80
|
+
{sections.length > 0 && (
|
|
81
|
+
<>
|
|
82
|
+
<Dropdown.Separator className="ds-user-dropdown__divider" />
|
|
83
|
+
{sections.map((section, index) => (
|
|
84
|
+
<React.Fragment key={index}>
|
|
85
|
+
{section.label && <div className="ds-user-dropdown__section-label">{section.label}</div>}
|
|
86
|
+
<Dropdown.Group className="ds-user-dropdown__section">
|
|
87
|
+
{section.apps.map((app, appIndex) => (
|
|
88
|
+
<UserDropdownAppItem key={appIndex} {...app} />
|
|
89
|
+
))}
|
|
90
|
+
</Dropdown.Group>
|
|
91
|
+
</React.Fragment>
|
|
92
|
+
))}
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{discoverSection && (
|
|
97
|
+
<UserDropdownCollapsibleSection
|
|
98
|
+
label={discoverSection.label}
|
|
99
|
+
apps={discoverSection.apps}
|
|
100
|
+
defaultOpen={discoverSection.defaultOpen}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<Dropdown.Separator className="ds-user-dropdown__divider" />
|
|
105
|
+
|
|
106
|
+
<UserDropdownSignOut onClick={onSignOut} label={signOutLabel} />
|
|
107
|
+
</Dropdown.Content>
|
|
108
|
+
</Dropdown>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import arborPng from './arbor.png';
|
|
2
|
+
import govhubPng from './govhub.png';
|
|
3
|
+
import keyPng from './key.png';
|
|
4
|
+
import robinPng from './robin.png';
|
|
5
|
+
import sampeoplePng from './sampeople.png';
|
|
6
|
+
import timetablerPng from './timetabler.png';
|
|
7
|
+
|
|
8
|
+
export const ArborLogo = arborPng;
|
|
9
|
+
export const GovhubLogo = govhubPng;
|
|
10
|
+
export const KeyLogo = keyPng;
|
|
11
|
+
export const RobinLogo = robinPng;
|
|
12
|
+
export const SampeopleLogo = sampeoplePng;
|
|
13
|
+
export const TimetablerLogo = timetablerPng;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Dropdown } from 'Components/dropdown/Dropdown';
|
|
3
|
+
import { Icon } from 'Components/icon/Icon';
|
|
4
|
+
import type { UserDropdownApp } from '../UserDropdown';
|
|
5
|
+
|
|
6
|
+
export const UserDropdownAppItem = (props: UserDropdownApp) => {
|
|
7
|
+
const { logo, name, description, onClick } = props;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Dropdown.Item className="ds-user-dropdown__app-item" onClick={onClick}>
|
|
11
|
+
{logo && <div className="ds-user-dropdown__app-item-logo">{logo}</div>}
|
|
12
|
+
<div className="ds-user-dropdown__app-item-text">
|
|
13
|
+
<div className="ds-user-dropdown__app-item-name">{name}</div>
|
|
14
|
+
{description && <div className="ds-user-dropdown__app-item-description">{description}</div>}
|
|
15
|
+
</div>
|
|
16
|
+
<div className="ds-user-dropdown__app-item-arrow" aria-hidden="true">
|
|
17
|
+
<Icon name="arrow-up-right" size={16} />
|
|
18
|
+
</div>
|
|
19
|
+
</Dropdown.Item>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Icon } from 'Components/icon/Icon';
|
|
3
|
+
import { Dropdown } from 'Components/dropdown/Dropdown';
|
|
4
|
+
import type { UserDropdownApp } from '../UserDropdown';
|
|
5
|
+
import { UserDropdownAppItem } from './UserDropdownAppItem';
|
|
6
|
+
|
|
7
|
+
type UserDropdownCollapsibleSectionProps = {
|
|
8
|
+
label: string;
|
|
9
|
+
apps: UserDropdownApp[];
|
|
10
|
+
defaultOpen?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const UserDropdownCollapsibleSection = (props: UserDropdownCollapsibleSectionProps) => {
|
|
14
|
+
const { label, apps, defaultOpen = false } = props;
|
|
15
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="ds-user-dropdown__collapsible-section">
|
|
19
|
+
<div
|
|
20
|
+
className="ds-user-dropdown__collapsible-section-header"
|
|
21
|
+
onClick={() => { setIsOpen(!isOpen); }}
|
|
22
|
+
role="button"
|
|
23
|
+
tabIndex={0}
|
|
24
|
+
aria-expanded={isOpen}
|
|
25
|
+
>
|
|
26
|
+
<span className="ds-user-dropdown__collapsible-section-label">{label}</span>
|
|
27
|
+
<Icon name={isOpen ? 'chevron-up' : 'chevron-down'} size={16} className="ds-user-dropdown__collapsible-section-icon" />
|
|
28
|
+
</div>
|
|
29
|
+
{isOpen && (
|
|
30
|
+
<Dropdown.Group className="ds-user-dropdown__collapsible-section-content">
|
|
31
|
+
{apps.map((app, index) => (
|
|
32
|
+
<UserDropdownAppItem key={index} {...app} />
|
|
33
|
+
))}
|
|
34
|
+
</Dropdown.Group>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Icon } from 'Components/icon/Icon';
|
|
3
|
+
import { Dropdown } from 'Components/dropdown/Dropdown';
|
|
4
|
+
|
|
5
|
+
type UserDropdownSignOutProps = {
|
|
6
|
+
onClick: () => void;
|
|
7
|
+
label?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const UserDropdownSignOut = (props: UserDropdownSignOutProps) => {
|
|
11
|
+
const { onClick, label = 'Sign out' } = props;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Dropdown.Item className="ds-user-dropdown__sign-out" onClick={onClick}>
|
|
15
|
+
<Icon name="log-out" size={24} />
|
|
16
|
+
<span className="ds-user-dropdown__sign-out-label">{label}</span>
|
|
17
|
+
</Dropdown.Item>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Dropdown } from 'Components/dropdown/Dropdown';
|
|
3
|
+
import { Avatar } from 'Components/avatar/Avatar';
|
|
4
|
+
import { Button } from 'Components/button/Button';
|
|
5
|
+
|
|
6
|
+
type UserDropdownTriggerProps = {
|
|
7
|
+
avatarSrc?: string;
|
|
8
|
+
avatarAlt?: string;
|
|
9
|
+
avatarInitials?: string;
|
|
10
|
+
logoSrc?: string;
|
|
11
|
+
logoAlt?: string;
|
|
12
|
+
variant?: 'dark' | 'light';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const UserDropdownTrigger = (props: UserDropdownTriggerProps) => {
|
|
16
|
+
const { avatarSrc, avatarAlt, avatarInitials, logoSrc, logoAlt = 'Logo', variant = 'dark' } = props;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={`ds-user-dropdown__trigger-container ds-user-dropdown__trigger-container--${variant}${
|
|
21
|
+
logoSrc ? ' ds-user-dropdown__trigger-container--with-logo' : ''
|
|
22
|
+
}`}
|
|
23
|
+
>
|
|
24
|
+
<Dropdown.Trigger>
|
|
25
|
+
<Button
|
|
26
|
+
variant="secondary"
|
|
27
|
+
borderless={true}
|
|
28
|
+
iconRightName="chevron-down"
|
|
29
|
+
className="ds-user-dropdown__trigger"
|
|
30
|
+
>
|
|
31
|
+
<Avatar size="medium" src={avatarSrc} alt={avatarAlt} initials={avatarInitials} />
|
|
32
|
+
</Button>
|
|
33
|
+
</Dropdown.Trigger>
|
|
34
|
+
{logoSrc && (
|
|
35
|
+
<>
|
|
36
|
+
<div className="ds-user-dropdown__trigger-divider" />
|
|
37
|
+
<img src={logoSrc} alt={logoAlt} className="ds-user-dropdown__trigger-logo" />
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { DropdownMenu } from 'radix-ui';
|
|
3
|
+
import { Avatar } from 'Components/avatar/Avatar';
|
|
4
|
+
import { Icon } from 'Components/icon/Icon';
|
|
5
|
+
import { Dropdown } from 'Components/dropdown/Dropdown';
|
|
6
|
+
import { PopupParentContext } from 'Utils/PopupParentContext';
|
|
7
|
+
import type { UserDropdownUser, UserDropdownUserInfoAction } from '../UserDropdown';
|
|
8
|
+
|
|
9
|
+
type UserDropdownUserInfoProps = {
|
|
10
|
+
user: UserDropdownUser;
|
|
11
|
+
action?: UserDropdownUserInfoAction;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const UserDropdownUserInfo = (props: UserDropdownUserInfoProps) => {
|
|
15
|
+
const { user, action } = props;
|
|
16
|
+
const popupParentRef = useContext(PopupParentContext);
|
|
17
|
+
|
|
18
|
+
const content = (
|
|
19
|
+
<>
|
|
20
|
+
<Avatar size="medium" src={user.avatarSrc} alt={user.avatarAlt} initials={user.avatarInitials} />
|
|
21
|
+
<div className="ds-user-dropdown__user-info-text">
|
|
22
|
+
<div className="ds-user-dropdown__user-info-name">{user.name}</div>
|
|
23
|
+
{user.subtitle && <div className="ds-user-dropdown__user-info-role">{user.subtitle}</div>}
|
|
24
|
+
</div>
|
|
25
|
+
{action && <Icon name="chevron-right" size={16} className="ds-user-dropdown__user-info-chevron" />}
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (action?.type === 'menu') {
|
|
30
|
+
return (
|
|
31
|
+
<DropdownMenu.Sub>
|
|
32
|
+
<DropdownMenu.SubTrigger className="ds-user-dropdown__user-info">
|
|
33
|
+
{content}
|
|
34
|
+
</DropdownMenu.SubTrigger>
|
|
35
|
+
<DropdownMenu.Portal container={popupParentRef.current}>
|
|
36
|
+
<DropdownMenu.SubContent className="ds-user-dropdown__sub-content">
|
|
37
|
+
{action.items.map((item, index) => (
|
|
38
|
+
<DropdownMenu.Item
|
|
39
|
+
key={index}
|
|
40
|
+
className="ds-user-dropdown__sub-item"
|
|
41
|
+
onSelect={item.onClick}
|
|
42
|
+
>
|
|
43
|
+
{item.label}
|
|
44
|
+
</DropdownMenu.Item>
|
|
45
|
+
))}
|
|
46
|
+
</DropdownMenu.SubContent>
|
|
47
|
+
</DropdownMenu.Portal>
|
|
48
|
+
</DropdownMenu.Sub>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Dropdown.Item
|
|
54
|
+
className="ds-user-dropdown__user-info"
|
|
55
|
+
onClick={action?.type === 'link' ? action.onClick : undefined}
|
|
56
|
+
>
|
|
57
|
+
{content}
|
|
58
|
+
</Dropdown.Item>
|
|
59
|
+
);
|
|
60
|
+
};
|