@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CLAUDE.md +9 -0
  3. package/dist/components/avatar/Avatar.d.ts +11 -0
  4. package/dist/components/avatar/Avatar.d.ts.map +1 -0
  5. package/dist/components/avatar/Avatar.js +17 -0
  6. package/dist/components/avatar/Avatar.js.map +1 -0
  7. package/dist/components/avatar/Avatar.stories.d.ts +14 -0
  8. package/dist/components/avatar/Avatar.stories.d.ts.map +1 -0
  9. package/dist/components/avatar/Avatar.stories.js +66 -0
  10. package/dist/components/avatar/Avatar.stories.js.map +1 -0
  11. package/dist/components/avatar/Avatar.test.d.ts +2 -0
  12. package/dist/components/avatar/Avatar.test.d.ts.map +1 -0
  13. package/dist/components/avatar/Avatar.test.js +51 -0
  14. package/dist/components/avatar/Avatar.test.js.map +1 -0
  15. package/dist/components/banner/Banner.d.ts +19 -0
  16. package/dist/components/banner/Banner.d.ts.map +1 -0
  17. package/dist/components/banner/Banner.js +33 -0
  18. package/dist/components/banner/Banner.js.map +1 -0
  19. package/dist/components/banner/Banner.stories.d.ts +72 -0
  20. package/dist/components/banner/Banner.stories.d.ts.map +1 -0
  21. package/dist/components/banner/Banner.stories.js +84 -0
  22. package/dist/components/banner/Banner.stories.js.map +1 -0
  23. package/dist/components/banner/Banner.test.d.ts +2 -0
  24. package/dist/components/banner/Banner.test.d.ts.map +1 -0
  25. package/dist/components/banner/Banner.test.js +72 -0
  26. package/dist/components/banner/Banner.test.js.map +1 -0
  27. package/dist/components/dropdown/Dropdown.d.ts +2 -0
  28. package/dist/components/dropdown/Dropdown.d.ts.map +1 -1
  29. package/dist/components/dropdown/Dropdown.js +5 -1
  30. package/dist/components/dropdown/Dropdown.js.map +1 -1
  31. package/dist/components/dropdown/items/DropdownGroup.d.ts +3 -0
  32. package/dist/components/dropdown/items/DropdownGroup.d.ts.map +1 -0
  33. package/dist/components/dropdown/items/DropdownGroup.js +8 -0
  34. package/dist/components/dropdown/items/DropdownGroup.js.map +1 -0
  35. package/dist/components/dropdown/items/DropdownSeparator.d.ts +3 -0
  36. package/dist/components/dropdown/items/DropdownSeparator.d.ts.map +1 -0
  37. package/dist/components/dropdown/items/DropdownSeparator.js +8 -0
  38. package/dist/components/dropdown/items/DropdownSeparator.js.map +1 -0
  39. package/dist/components/userDropdown/UserDropdown.d.ts +47 -0
  40. package/dist/components/userDropdown/UserDropdown.d.ts.map +1 -0
  41. package/dist/components/userDropdown/UserDropdown.js +13 -0
  42. package/dist/components/userDropdown/UserDropdown.js.map +1 -0
  43. package/dist/components/userDropdown/UserDropdown.stories.d.ts +12 -0
  44. package/dist/components/userDropdown/UserDropdown.stories.d.ts.map +1 -0
  45. package/dist/components/userDropdown/UserDropdown.stories.js +222 -0
  46. package/dist/components/userDropdown/UserDropdown.stories.js.map +1 -0
  47. package/dist/components/userDropdown/UserDropdown.test.d.ts +2 -0
  48. package/dist/components/userDropdown/UserDropdown.test.d.ts.map +1 -0
  49. package/dist/components/userDropdown/UserDropdown.test.js +197 -0
  50. package/dist/components/userDropdown/UserDropdown.test.js.map +1 -0
  51. package/dist/components/userDropdown/assets/arbor.png +0 -0
  52. package/dist/components/userDropdown/assets/govhub.png +0 -0
  53. package/dist/components/userDropdown/assets/key.png +0 -0
  54. package/dist/components/userDropdown/assets/logos.d.ts +7 -0
  55. package/dist/components/userDropdown/assets/logos.d.ts.map +1 -0
  56. package/dist/components/userDropdown/assets/logos.js +13 -0
  57. package/dist/components/userDropdown/assets/logos.js.map +1 -0
  58. package/dist/components/userDropdown/assets/robin.png +0 -0
  59. package/dist/components/userDropdown/assets/sampeople.png +0 -0
  60. package/dist/components/userDropdown/assets/timetabler.png +0 -0
  61. package/dist/components/userDropdown/internal/UserDropdownAppItem.d.ts +3 -0
  62. package/dist/components/userDropdown/internal/UserDropdownAppItem.d.ts.map +1 -0
  63. package/dist/components/userDropdown/internal/UserDropdownAppItem.js +9 -0
  64. package/dist/components/userDropdown/internal/UserDropdownAppItem.js.map +1 -0
  65. package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.d.ts +9 -0
  66. package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.d.ts.map +1 -0
  67. package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.js +11 -0
  68. package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.js.map +1 -0
  69. package/dist/components/userDropdown/internal/UserDropdownSignOut.d.ts +7 -0
  70. package/dist/components/userDropdown/internal/UserDropdownSignOut.d.ts.map +1 -0
  71. package/dist/components/userDropdown/internal/UserDropdownSignOut.js +9 -0
  72. package/dist/components/userDropdown/internal/UserDropdownSignOut.js.map +1 -0
  73. package/dist/components/userDropdown/internal/UserDropdownTrigger.d.ts +11 -0
  74. package/dist/components/userDropdown/internal/UserDropdownTrigger.d.ts.map +1 -0
  75. package/dist/components/userDropdown/internal/UserDropdownTrigger.js +10 -0
  76. package/dist/components/userDropdown/internal/UserDropdownTrigger.js.map +1 -0
  77. package/dist/components/userDropdown/internal/UserDropdownUserInfo.d.ts +8 -0
  78. package/dist/components/userDropdown/internal/UserDropdownUserInfo.d.ts.map +1 -0
  79. package/dist/components/userDropdown/internal/UserDropdownUserInfo.js +17 -0
  80. package/dist/components/userDropdown/internal/UserDropdownUserInfo.js.map +1 -0
  81. package/dist/index.css +463 -53
  82. package/dist/index.css.map +1 -1
  83. package/dist/index.d.ts +5 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +4 -0
  86. package/dist/index.js.map +1 -1
  87. package/package.json +3 -2
  88. package/src/components/avatar/Avatar.stories.tsx +84 -0
  89. package/src/components/avatar/Avatar.test.tsx +60 -0
  90. package/src/components/avatar/Avatar.tsx +68 -0
  91. package/src/components/avatar/avatar.scss +71 -0
  92. package/src/components/banner/Banner.stories.tsx +96 -0
  93. package/src/components/banner/Banner.test.tsx +86 -0
  94. package/src/components/banner/Banner.tsx +81 -0
  95. package/src/components/banner/banner.scss +67 -0
  96. package/src/components/button/button.scss +0 -5
  97. package/src/components/card/card.scss +0 -3
  98. package/src/components/dropdown/Dropdown.tsx +5 -1
  99. package/src/components/dropdown/dropdown.scss +4 -4
  100. package/src/components/dropdown/items/DropdownGroup.tsx +11 -0
  101. package/src/components/dropdown/items/DropdownSeparator.tsx +9 -0
  102. package/src/components/formField/fieldset/fieldset.scss +0 -2
  103. package/src/components/formField/formField.scss +0 -2
  104. package/src/components/formField/inputs/checkbox/checkboxInput.scss +0 -2
  105. package/src/components/formField/inputs/input.scss +0 -3
  106. package/src/components/formField/inputs/radio/radioButtonInput.scss +0 -2
  107. package/src/components/formField/inputs/selectDropdown/selectDropdown.scss +0 -1
  108. package/src/components/formField/label/label.scss +0 -2
  109. package/src/components/modal/modal.scss +0 -3
  110. package/src/components/pill/pill.scss +0 -3
  111. package/src/components/searchBar/searchBar.scss +0 -3
  112. package/src/components/table/columnFilters/columnFilters.scss +0 -6
  113. package/src/components/table/pagination/pagination.scss +0 -4
  114. package/src/components/tabs/tabs.scss +0 -2
  115. package/src/components/tag/tag.scss +0 -3
  116. package/src/components/toast/toast.scss +0 -1
  117. package/src/components/tooltip/tooltip.scss +0 -3
  118. package/src/components/userDropdown/UserDropdown.stories.tsx +237 -0
  119. package/src/components/userDropdown/UserDropdown.test.tsx +349 -0
  120. package/src/components/userDropdown/UserDropdown.tsx +110 -0
  121. package/src/components/userDropdown/assets/arbor.png +0 -0
  122. package/src/components/userDropdown/assets/govhub.png +0 -0
  123. package/src/components/userDropdown/assets/key.png +0 -0
  124. package/src/components/userDropdown/assets/logos.ts +13 -0
  125. package/src/components/userDropdown/assets/robin.png +0 -0
  126. package/src/components/userDropdown/assets/sampeople.png +0 -0
  127. package/src/components/userDropdown/assets/timetabler.png +0 -0
  128. package/src/components/userDropdown/internal/UserDropdownAppItem.tsx +21 -0
  129. package/src/components/userDropdown/internal/UserDropdownCollapsibleSection.tsx +38 -0
  130. package/src/components/userDropdown/internal/UserDropdownSignOut.tsx +19 -0
  131. package/src/components/userDropdown/internal/UserDropdownTrigger.tsx +42 -0
  132. package/src/components/userDropdown/internal/UserDropdownUserInfo.tsx +60 -0
  133. package/src/components/userDropdown/userDropdown.scss +377 -0
  134. package/src/global.scss +9 -1
  135. package/src/index.scss +3 -0
  136. package/src/index.ts +5 -0
  137. package/tsconfig.json +1 -1
  138. 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
+ };
@@ -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;
@@ -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
+ };