@arbor-education/design-system.components 0.4.0 → 0.4.2
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 +12 -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/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/formField/inputs/selectDropdown/SelectDropdown.d.ts +2 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +2 -2
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts +12 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts.map +1 -1
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js +13 -0
- package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js.map +1 -1
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/Table.js +0 -2
- package/dist/components/table/Table.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +2 -10
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/Table.test.js +2 -40
- package/dist/components/table/Table.test.js.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js +22 -12
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js.map +1 -1
- 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 +401 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -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/dropdown/Dropdown.tsx +5 -1
- package/src/components/dropdown/dropdown.scss +4 -1
- package/src/components/dropdown/items/DropdownGroup.tsx +11 -0
- package/src/components/dropdown/items/DropdownSeparator.tsx +9 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.stories.tsx +15 -0
- package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +5 -1
- package/src/components/table/Table.stories.tsx +2 -10
- package/src/components/table/Table.test.tsx +2 -49
- package/src/components/table/Table.tsx +0 -2
- package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +34 -19
- package/src/components/table/table.scss +4 -2
- 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/index.scss +2 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +1 -1
- package/vite-env.d.ts +31 -0
- package/dist/components/table/cellRenderers/SelectDropdownCellEditor.d.ts +0 -8
- package/dist/components/table/cellRenderers/SelectDropdownCellEditor.d.ts.map +0 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellEditor.js +0 -19
- package/dist/components/table/cellRenderers/SelectDropdownCellEditor.js.map +0 -1
- package/src/components/table/cellRenderers/SelectDropdownCellEditor.tsx +0 -43
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { UserDropdown } from './UserDropdown';
|
|
3
|
+
import { ArborLogo, GovhubLogo, KeyLogo, RobinLogo, SampeopleLogo, TimetablerLogo } from './assets/logos';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof UserDropdown> = {
|
|
6
|
+
title: 'Components/UserDropdown',
|
|
7
|
+
component: UserDropdown,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof UserDropdown>;
|
|
12
|
+
|
|
13
|
+
// Mock avatar image (placeholder)
|
|
14
|
+
const avatarSrc = 'data:image/svg+xml,%3Csvg width="32" height="32" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="16" cy="16" r="16" fill="%233cad51"/%3E%3Ctext x="16" y="21" font-family="Arial" font-size="14" font-weight="bold" fill="white" text-anchor="middle"%3ECM%3C/text%3E%3C/svg%3E';
|
|
15
|
+
|
|
16
|
+
// Import real logos from assets
|
|
17
|
+
const logoArborWorkflows = ArborLogo;
|
|
18
|
+
const logoGovernorHub = GovhubLogo;
|
|
19
|
+
const logoTheKey = KeyLogo;
|
|
20
|
+
const logoRobin = RobinLogo;
|
|
21
|
+
const logoSamPeople = SampeopleLogo;
|
|
22
|
+
const logoTimetabler = TimetablerLogo;
|
|
23
|
+
|
|
24
|
+
const AppLogo = ({ src, alt }: { src: string; alt: string }) => (
|
|
25
|
+
<img src={src} alt={alt} width={24} height={24} style={{ objectFit: 'contain' }} />
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Mock company logo SVG
|
|
29
|
+
const mockLogoSrc = 'data:image/svg+xml,%3Csvg width="50" height="30" xmlns="http://www.w3.org/2000/svg"%3E%3Crect width="50" height="30" rx="4" fill="%233cad51"/%3E%3Ctext x="25" y="20" font-family="Arial" font-size="14" font-weight="bold" fill="white" text-anchor="middle"%3ELogo%3C/text%3E%3C/svg%3E';
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
user: {
|
|
34
|
+
name: 'Christine Montgomery-Smith',
|
|
35
|
+
subtitle: 'Business Manager',
|
|
36
|
+
avatarSrc,
|
|
37
|
+
avatarAlt: 'Christine Montgomery-Smith',
|
|
38
|
+
},
|
|
39
|
+
logoSrc: mockLogoSrc,
|
|
40
|
+
logoAlt: 'Arbor',
|
|
41
|
+
sections: [
|
|
42
|
+
{
|
|
43
|
+
label: 'My Apps',
|
|
44
|
+
apps: [
|
|
45
|
+
{
|
|
46
|
+
logo: <AppLogo src={logoArborWorkflows} alt="Arbor Workflows" />,
|
|
47
|
+
name: 'Arbor Workflows',
|
|
48
|
+
onClick: () => { console.log('Open Arbor Workflows'); },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
logo: <AppLogo src={logoGovernorHub} alt="GovernorHub" />,
|
|
52
|
+
name: 'GovernorHub',
|
|
53
|
+
onClick: () => { console.log('Open GovernorHub'); },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
logo: <AppLogo src={logoTheKey} alt="The Key" />,
|
|
57
|
+
name: 'The Key',
|
|
58
|
+
onClick: () => { console.log('Open The Key'); },
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
discoverSection: {
|
|
64
|
+
label: 'Discover the Arbor suite',
|
|
65
|
+
defaultOpen: true,
|
|
66
|
+
apps: [
|
|
67
|
+
{
|
|
68
|
+
logo: <AppLogo src={logoArborWorkflows} alt="Arbor Finance" />,
|
|
69
|
+
name: 'Arbor Finance',
|
|
70
|
+
description: 'Cloud-based accounting',
|
|
71
|
+
onClick: () => { console.log('Open Arbor Finance'); },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
logo: <AppLogo src={logoRobin} alt="Robin" />,
|
|
75
|
+
name: 'Robin',
|
|
76
|
+
description: 'AI website compliance checks',
|
|
77
|
+
onClick: () => { console.log('Open Robin'); },
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
logo: <AppLogo src={logoSamPeople} alt="SAMPeople" />,
|
|
81
|
+
name: 'SAMPeople',
|
|
82
|
+
description: 'HR and recruitment',
|
|
83
|
+
onClick: () => { console.log('Open SAMPeople'); },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
logo: <AppLogo src={logoTimetabler} alt="Timetabler" />,
|
|
87
|
+
name: 'Timetabler',
|
|
88
|
+
description: 'Schedule timetables and more',
|
|
89
|
+
onClick: () => { console.log('Open Timetabler'); },
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
userInfoAction: { type: 'link', onClick: () => { console.log('View profile'); } },
|
|
94
|
+
onSignOut: () => { console.log('Sign out'); },
|
|
95
|
+
signOutLabel: 'Sign out of Arbor',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const WithApps: Story = {
|
|
100
|
+
args: {
|
|
101
|
+
user: {
|
|
102
|
+
name: 'Christine Montgomery-Smith',
|
|
103
|
+
subtitle: 'Business Manager',
|
|
104
|
+
avatarInitials: 'CM',
|
|
105
|
+
},
|
|
106
|
+
sections: [
|
|
107
|
+
{
|
|
108
|
+
label: 'Quick Actions',
|
|
109
|
+
apps: [
|
|
110
|
+
{ name: 'Settings', onClick: () => { console.log('Settings'); } },
|
|
111
|
+
{ name: 'Help', onClick: () => { console.log('Help'); } },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
onSignOut: () => { console.log('Sign out'); },
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const Minimal: Story = {
|
|
120
|
+
args: {
|
|
121
|
+
user: {
|
|
122
|
+
name: 'Christine Montgomery',
|
|
123
|
+
avatarInitials: 'CM',
|
|
124
|
+
avatarAlt: 'Christine Montgomery',
|
|
125
|
+
},
|
|
126
|
+
onSignOut: () => { console.log('Sign out'); },
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const WithMenu: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
user: {
|
|
133
|
+
name: 'Christine Montgomery-Smith',
|
|
134
|
+
subtitle: 'Business Manager',
|
|
135
|
+
avatarSrc,
|
|
136
|
+
avatarAlt: 'Christine Montgomery-Smith',
|
|
137
|
+
},
|
|
138
|
+
logoSrc: mockLogoSrc,
|
|
139
|
+
logoAlt: 'Arbor',
|
|
140
|
+
sections: [
|
|
141
|
+
{
|
|
142
|
+
label: 'My Apps',
|
|
143
|
+
apps: [
|
|
144
|
+
{
|
|
145
|
+
logo: <AppLogo src={logoArborWorkflows} alt="Arbor Workflows" />,
|
|
146
|
+
name: 'Arbor Workflows',
|
|
147
|
+
onClick: () => { console.log('Open Arbor Workflows'); },
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
userInfoAction: {
|
|
153
|
+
type: 'menu',
|
|
154
|
+
items: [
|
|
155
|
+
{ label: 'Help centre', onClick: () => { console.log('Help centre'); } },
|
|
156
|
+
{ label: 'About', onClick: () => { console.log('About'); } },
|
|
157
|
+
{ label: 'Leave feedback', onClick: () => { console.log('Leave feedback'); } },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
onSignOut: () => { console.log('Sign out'); },
|
|
161
|
+
signOutLabel: 'Sign out of Arbor',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const DarkVariant: Story = {
|
|
166
|
+
args: {
|
|
167
|
+
user: {
|
|
168
|
+
name: 'Christine Montgomery-Smith',
|
|
169
|
+
subtitle: 'Business Manager',
|
|
170
|
+
avatarSrc,
|
|
171
|
+
avatarAlt: 'Christine Montgomery-Smith',
|
|
172
|
+
},
|
|
173
|
+
logoSrc: mockLogoSrc,
|
|
174
|
+
logoAlt: 'Arbor',
|
|
175
|
+
variant: 'dark',
|
|
176
|
+
sections: [
|
|
177
|
+
{
|
|
178
|
+
label: 'My Apps',
|
|
179
|
+
apps: [
|
|
180
|
+
{
|
|
181
|
+
logo: <AppLogo src={logoArborWorkflows} alt="Arbor Workflows" />,
|
|
182
|
+
name: 'Arbor Workflows',
|
|
183
|
+
onClick: () => { console.log('Open Arbor Workflows'); },
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
onSignOut: () => { console.log('Sign out'); },
|
|
189
|
+
signOutLabel: 'Sign out of Arbor',
|
|
190
|
+
},
|
|
191
|
+
parameters: {
|
|
192
|
+
backgrounds: {
|
|
193
|
+
default: 'dark',
|
|
194
|
+
values: [
|
|
195
|
+
{ name: 'dark', value: '#2f2f2f' },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const LightVariant: Story = {
|
|
202
|
+
args: {
|
|
203
|
+
user: {
|
|
204
|
+
name: 'Christine Montgomery-Smith',
|
|
205
|
+
subtitle: 'Business Manager',
|
|
206
|
+
avatarSrc,
|
|
207
|
+
avatarAlt: 'Christine Montgomery-Smith',
|
|
208
|
+
},
|
|
209
|
+
logoSrc: mockLogoSrc,
|
|
210
|
+
logoAlt: 'Arbor',
|
|
211
|
+
variant: 'light',
|
|
212
|
+
sections: [
|
|
213
|
+
{
|
|
214
|
+
label: 'My Apps',
|
|
215
|
+
apps: [
|
|
216
|
+
{
|
|
217
|
+
logo: <AppLogo src={logoArborWorkflows} alt="Arbor Workflows" />,
|
|
218
|
+
name: 'Arbor Workflows',
|
|
219
|
+
onClick: () => { console.log('Open Arbor Workflows'); },
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
onSignOut: () => { console.log('Sign out'); },
|
|
225
|
+
signOutLabel: 'Sign out of Arbor',
|
|
226
|
+
},
|
|
227
|
+
parameters: {
|
|
228
|
+
backgrounds: {
|
|
229
|
+
default: 'light',
|
|
230
|
+
values: [
|
|
231
|
+
{ name: 'light', value: '#ffffff' },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export default meta;
|
|
@@ -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
|