@yuno-payments/dashboard-design-system 0.0.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/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/README.md +69 -0
- package/components.json +21 -0
- package/eslint.config.js +26 -0
- package/index.html +13 -0
- package/package.json +57 -0
- package/public/vite.svg +1 -0
- package/src/App.css +42 -0
- package/src/App.tsx +11 -0
- package/src/assets/react.svg +1 -0
- package/src/components/atoms/button/button.stories.tsx +222 -0
- package/src/components/atoms/button/button.test.tsx +78 -0
- package/src/components/atoms/button/index.tsx +80 -0
- package/src/components/atoms/checkbox/checkbox.stories.tsx +314 -0
- package/src/components/atoms/checkbox/checkbox.test.tsx +278 -0
- package/src/components/atoms/checkbox/index.tsx +103 -0
- package/src/components/atoms/chip/chip.stories.tsx +317 -0
- package/src/components/atoms/chip/chip.test.tsx +300 -0
- package/src/components/atoms/chip/index.tsx +114 -0
- package/src/components/atoms/input/index.tsx +27 -0
- package/src/components/atoms/link/index.tsx +79 -0
- package/src/components/atoms/link/link.stories.tsx +159 -0
- package/src/components/atoms/link/link.test.tsx +176 -0
- package/src/components/atoms/radiobutton/index.tsx +103 -0
- package/src/components/atoms/radiobutton/radiobutton.stories.tsx +314 -0
- package/src/components/atoms/radiobutton/radiobutton.test.tsx +245 -0
- package/src/components/atoms/tag/index.tsx +196 -0
- package/src/components/atoms/tag/tag.stories.tsx +281 -0
- package/src/components/atoms/tag/tag.test.tsx +282 -0
- package/src/components/atoms/typography/index.tsx +62 -0
- package/src/components/atoms/typography/typography.stories.tsx +214 -0
- package/src/components/atoms/typography/typography.test.tsx +187 -0
- package/src/components/index.tsx +17 -0
- package/src/components/molecules/announcement/announcement.stories.tsx +277 -0
- package/src/components/molecules/announcement/announcement.test.tsx +354 -0
- package/src/components/molecules/announcement/index.tsx +200 -0
- package/src/components/molecules/notification-alert/index.tsx +293 -0
- package/src/components/molecules/notification-alert/notification-alert.stories.tsx +418 -0
- package/src/components/molecules/notification-alert/notification-alert.test.tsx +454 -0
- package/src/components/molecules/popover/index.tsx +175 -0
- package/src/components/molecules/popover/popover.stories.tsx +241 -0
- package/src/components/molecules/popover/popover.test.tsx +191 -0
- package/src/components/molecules/textfield/index.tsx +154 -0
- package/src/components/molecules/textfield/textfield.stories.tsx +168 -0
- package/src/components/molecules/textfield/textfield.test.tsx +157 -0
- package/src/components/molecules/tooltip/index.tsx +263 -0
- package/src/components/molecules/tooltip/tooltip.stories.tsx +363 -0
- package/src/components/molecules/tooltip/tooltip.test.tsx +468 -0
- package/src/components/organisms/dialog/dialog.stories.tsx +522 -0
- package/src/components/organisms/dialog/dialog.test.tsx +525 -0
- package/src/components/organisms/dialog/index.tsx +233 -0
- package/src/components/organisms/dropdown/dropdown.stories.tsx +529 -0
- package/src/components/organisms/dropdown/dropdown.test.tsx +390 -0
- package/src/components/organisms/dropdown/index.tsx +624 -0
- package/src/index.css +184 -0
- package/src/lib/color-utils.ts +94 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/stories/Colors.stories.tsx +107 -0
- package/src/stories/Shadows.stories.tsx +110 -0
- package/src/stories/Spacing.stories.tsx +121 -0
- package/src/stories/Typography.stories.tsx +197 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +43 -0
- package/vitest.config.ts +15 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
import '@testing-library/jest-dom'
|
|
4
|
+
import { Dropdown } from './index'
|
|
5
|
+
|
|
6
|
+
const mockFn = () => vi.fn()
|
|
7
|
+
|
|
8
|
+
const mockInitialData = [
|
|
9
|
+
{
|
|
10
|
+
header: 'Main Category',
|
|
11
|
+
type: 'checkbox' as const,
|
|
12
|
+
options: [
|
|
13
|
+
{ label: 'Option 1', value: 'option1' },
|
|
14
|
+
{ label: 'Option 2', value: 'option2' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
header: 'Secondary Category',
|
|
19
|
+
options: [
|
|
20
|
+
{ label: 'Option A', value: 'optionA' },
|
|
21
|
+
{ label: 'Option B', value: 'optionB' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const mockRadioButtonData = [
|
|
27
|
+
{
|
|
28
|
+
header: 'Radio Options',
|
|
29
|
+
type: 'radiobutton' as const,
|
|
30
|
+
options: [
|
|
31
|
+
{ label: 'Juan', value: 'Juan' },
|
|
32
|
+
{ label: 'Rodrigo', value: 'Rodrigo' },
|
|
33
|
+
{ label: 'Federico', value: 'Federico', disabled: true },
|
|
34
|
+
{ label: 'Nahuel', value: 'Nahuel' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const mockNestedData = [
|
|
40
|
+
{
|
|
41
|
+
header: 'Nested Options',
|
|
42
|
+
options: [
|
|
43
|
+
{
|
|
44
|
+
label: 'Cuentas test',
|
|
45
|
+
value: 'cuentas-test',
|
|
46
|
+
options: [
|
|
47
|
+
{ label: 'Juan', value: 'Juan' },
|
|
48
|
+
{ label: 'Rodrigo', value: 'Rodrigo' },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: 'Cuentas Yuno',
|
|
53
|
+
value: 'cuentas-yuno',
|
|
54
|
+
options: [
|
|
55
|
+
{ label: 'Omelia', value: 'Omelia' },
|
|
56
|
+
{ label: 'Federico', value: 'Federico', disabled: true },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
const mockTwoLevelsData = [
|
|
64
|
+
{
|
|
65
|
+
header: 'Two Level Options',
|
|
66
|
+
options: [
|
|
67
|
+
{
|
|
68
|
+
label: 'Cuentas test',
|
|
69
|
+
value: 'cuentas-test',
|
|
70
|
+
options: [
|
|
71
|
+
{
|
|
72
|
+
label: 'Cuentas Yuno',
|
|
73
|
+
value: 'cuentas-yuno',
|
|
74
|
+
options: [
|
|
75
|
+
{ label: 'Omelia', value: 'Omelia' },
|
|
76
|
+
{ label: 'Federico', value: 'Federico' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: 'Cuentas Avianca',
|
|
81
|
+
value: 'cuentas-avianca',
|
|
82
|
+
options: [
|
|
83
|
+
{ label: 'Tom', value: 'Tom' },
|
|
84
|
+
{ label: 'Bruno', value: 'Bruno', disabled: true },
|
|
85
|
+
{ label: 'Zac', value: 'Zac' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
const setup = (props = {}) => {
|
|
95
|
+
const defaultProps = {
|
|
96
|
+
onChangeSelected: mockFn(),
|
|
97
|
+
initialData: mockInitialData,
|
|
98
|
+
placeholder: 'Select an option',
|
|
99
|
+
onlyOneValue: true,
|
|
100
|
+
disabled: false,
|
|
101
|
+
showSearchBar: false,
|
|
102
|
+
initialDataSelected: [],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return render(<Dropdown {...defaultProps} {...props} />)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe('Dropdown', () => {
|
|
109
|
+
it('renders with placeholder', () => {
|
|
110
|
+
setup({})
|
|
111
|
+
expect(screen.getByText('Select an option')).toBeInTheDocument()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('opens dropdown on click', async () => {
|
|
115
|
+
setup({})
|
|
116
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
117
|
+
fireEvent.click(dropdown)
|
|
118
|
+
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(screen.getByTestId('dropdown-list-popper')).toBeInTheDocument()
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('selects an option in single-select mode', async () => {
|
|
125
|
+
const onChangeSelected = mockFn()
|
|
126
|
+
setup({ onChangeSelected })
|
|
127
|
+
|
|
128
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
129
|
+
fireEvent.click(dropdown)
|
|
130
|
+
|
|
131
|
+
await waitFor(() => {
|
|
132
|
+
expect(screen.getByTestId('dropdown-list-popper')).toBeInTheDocument()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const option = screen.getByText('Option 1')
|
|
136
|
+
fireEvent.click(option)
|
|
137
|
+
|
|
138
|
+
expect(onChangeSelected).toHaveBeenCalledWith(['option1'])
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('selects multiple options in multi-select mode', async () => {
|
|
142
|
+
const onChangeSelected = mockFn()
|
|
143
|
+
setup({ onChangeSelected, onlyOneValue: false })
|
|
144
|
+
|
|
145
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
146
|
+
fireEvent.click(dropdown)
|
|
147
|
+
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(screen.getByTestId('dropdown-list-popper')).toBeInTheDocument()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const option1 = screen.getByText('Option 1')
|
|
153
|
+
const option2 = screen.getByText('Option 2')
|
|
154
|
+
|
|
155
|
+
fireEvent.click(option1)
|
|
156
|
+
fireEvent.click(option2)
|
|
157
|
+
|
|
158
|
+
expect(onChangeSelected).toHaveBeenCalledWith(['option1'])
|
|
159
|
+
expect(onChangeSelected).toHaveBeenCalledWith(['option1', 'option2'])
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('disables dropdown when disabled prop is true', () => {
|
|
163
|
+
setup({ disabled: true })
|
|
164
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
165
|
+
expect(dropdown).toHaveAttribute('aria-disabled', 'true')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('shows search bar when showSearchBar is true', async () => {
|
|
169
|
+
setup({ showSearchBar: true })
|
|
170
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
171
|
+
fireEvent.click(dropdown)
|
|
172
|
+
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument()
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('filters options when searching', async () => {
|
|
179
|
+
setup({ showSearchBar: true })
|
|
180
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
181
|
+
fireEvent.click(dropdown)
|
|
182
|
+
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const searchInput = screen.getByPlaceholderText('Search...')
|
|
188
|
+
fireEvent.change(searchInput, { target: { value: 'Option 1' } })
|
|
189
|
+
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument()
|
|
192
|
+
expect(screen.queryByText('Option 2')).not.toBeInTheDocument()
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('handles radiobutton type options', async () => {
|
|
197
|
+
const onChangeSelected = mockFn()
|
|
198
|
+
setup({
|
|
199
|
+
onChangeSelected,
|
|
200
|
+
initialData: mockRadioButtonData,
|
|
201
|
+
onlyOneValue: true
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
205
|
+
fireEvent.click(dropdown)
|
|
206
|
+
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(screen.getByText('Juan')).toBeInTheDocument()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const option = screen.getByText('Juan')
|
|
212
|
+
fireEvent.click(option)
|
|
213
|
+
|
|
214
|
+
expect(onChangeSelected).toHaveBeenCalledWith(['Juan'])
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('handles nested options', async () => {
|
|
218
|
+
const onChangeSelected = mockFn()
|
|
219
|
+
setup({
|
|
220
|
+
onChangeSelected,
|
|
221
|
+
initialData: mockNestedData,
|
|
222
|
+
onlyOneValue: false
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
226
|
+
fireEvent.click(dropdown)
|
|
227
|
+
|
|
228
|
+
await waitFor(() => {
|
|
229
|
+
expect(screen.getByText('Cuentas test')).toBeInTheDocument()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const parentOption = screen.getByText('Cuentas test')
|
|
233
|
+
fireEvent.click(parentOption)
|
|
234
|
+
|
|
235
|
+
expect(onChangeSelected).toHaveBeenCalledWith(['Juan', 'Rodrigo'])
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('handles two-level nested options', async () => {
|
|
239
|
+
const onChangeSelected = mockFn()
|
|
240
|
+
setup({
|
|
241
|
+
onChangeSelected,
|
|
242
|
+
initialData: mockTwoLevelsData,
|
|
243
|
+
onlyOneValue: false
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
247
|
+
fireEvent.click(dropdown)
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(screen.getByText('Cuentas test')).toBeInTheDocument()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const parentOption = screen.getByText('Cuentas test')
|
|
254
|
+
fireEvent.click(parentOption)
|
|
255
|
+
|
|
256
|
+
expect(onChangeSelected).toHaveBeenCalledWith(['Omelia', 'Federico', 'Tom', 'Zac'])
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('shows tags for selected items in multi-select mode', async () => {
|
|
260
|
+
setup({
|
|
261
|
+
onlyOneValue: false,
|
|
262
|
+
initialDataSelected: ['option1', 'option2']
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument()
|
|
267
|
+
expect(screen.getByText('Option 2')).toBeInTheDocument()
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('removes tag when clicking remove button', async () => {
|
|
272
|
+
const onChangeSelected = mockFn()
|
|
273
|
+
setup({
|
|
274
|
+
onChangeSelected,
|
|
275
|
+
onlyOneValue: false,
|
|
276
|
+
initialDataSelected: ['option1', 'option2']
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
await waitFor(() => {
|
|
280
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const removeButtons = screen.getAllByRole('button', { name: /remove/i })
|
|
284
|
+
fireEvent.click(removeButtons[0])
|
|
285
|
+
|
|
286
|
+
expect(onChangeSelected).toHaveBeenCalledWith(['option2'])
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('closes dropdown when clicking outside', async () => {
|
|
290
|
+
setup({})
|
|
291
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
292
|
+
fireEvent.click(dropdown)
|
|
293
|
+
|
|
294
|
+
await waitFor(() => {
|
|
295
|
+
expect(screen.getByTestId('dropdown-list-popper')).toBeInTheDocument()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
fireEvent.mouseDown(document.body)
|
|
299
|
+
|
|
300
|
+
await waitFor(() => {
|
|
301
|
+
expect(screen.queryByTestId('dropdown-list-popper')).not.toBeInTheDocument()
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('closes dropdown when pressing Escape key', async () => {
|
|
306
|
+
setup({})
|
|
307
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
308
|
+
fireEvent.click(dropdown)
|
|
309
|
+
|
|
310
|
+
await waitFor(() => {
|
|
311
|
+
expect(screen.getByTestId('dropdown-list-popper')).toBeInTheDocument()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
fireEvent.keyDown(document, { key: 'Escape' })
|
|
315
|
+
|
|
316
|
+
await waitFor(() => {
|
|
317
|
+
expect(screen.queryByTestId('dropdown-list-popper')).not.toBeInTheDocument()
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('shows "No results found" when search yields no results', async () => {
|
|
322
|
+
setup({ showSearchBar: true })
|
|
323
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
324
|
+
fireEvent.click(dropdown)
|
|
325
|
+
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const searchInput = screen.getByPlaceholderText('Search...')
|
|
331
|
+
fireEvent.change(searchInput, { target: { value: 'nonexistent' } })
|
|
332
|
+
|
|
333
|
+
await waitFor(() => {
|
|
334
|
+
expect(screen.getByText('No results found')).toBeInTheDocument()
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('displays section headers when provided', async () => {
|
|
339
|
+
setup({})
|
|
340
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
341
|
+
fireEvent.click(dropdown)
|
|
342
|
+
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
expect(screen.getByText('Main Category')).toBeInTheDocument()
|
|
345
|
+
expect(screen.getByText('Secondary Category')).toBeInTheDocument()
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('handles disabled options correctly', async () => {
|
|
350
|
+
setup({ initialData: mockRadioButtonData })
|
|
351
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
352
|
+
fireEvent.click(dropdown)
|
|
353
|
+
|
|
354
|
+
await waitFor(() => {
|
|
355
|
+
expect(screen.getByText('Federico')).toBeInTheDocument()
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
const disabledOption = screen.getByText('Federico').closest('[role="option"]')
|
|
359
|
+
expect(disabledOption).toHaveAttribute('aria-disabled', 'true')
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('shows correct display value for single selection', async () => {
|
|
363
|
+
const onChangeSelected = mockFn()
|
|
364
|
+
setup({ onChangeSelected, showLabelOrValue: 'label' })
|
|
365
|
+
|
|
366
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
367
|
+
fireEvent.click(dropdown)
|
|
368
|
+
|
|
369
|
+
await waitFor(() => {
|
|
370
|
+
expect(screen.getByTestId('dropdown-list-popper')).toBeInTheDocument()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
const option = screen.getByText('Option 1')
|
|
374
|
+
fireEvent.click(option)
|
|
375
|
+
|
|
376
|
+
await waitFor(() => {
|
|
377
|
+
expect(screen.getByText('Option 1')).toBeInTheDocument()
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('shows count for multiple selections', async () => {
|
|
382
|
+
setup({
|
|
383
|
+
onlyOneValue: false,
|
|
384
|
+
initialDataSelected: ['option1', 'option2']
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const dropdown = screen.getByTestId('container-textfield')
|
|
388
|
+
expect(dropdown.textContent).toContain('2 selected')
|
|
389
|
+
})
|
|
390
|
+
})
|