@agregio-solutions/design-system 1.90.1 → 1.92.0

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 (68) hide show
  1. package/dist/design-system.cjs +9 -5
  2. package/dist/design-system.js +14 -6
  3. package/dist/packages/components/Accordion/doc.md +342 -0
  4. package/dist/packages/components/Badge/doc.md +192 -0
  5. package/dist/packages/components/Breadcrumbs/doc.md +332 -0
  6. package/dist/packages/components/Button/doc.md +425 -0
  7. package/dist/packages/components/Calendar/doc.md +465 -0
  8. package/dist/packages/components/ChartLegend/doc.md +151 -0
  9. package/dist/packages/components/ChartTooltip/doc.md +124 -0
  10. package/dist/packages/components/Checkbox/doc.md +329 -0
  11. package/dist/packages/components/CheckboxGroup/doc.md +242 -0
  12. package/dist/packages/components/Chip/doc.md +99 -0
  13. package/dist/packages/components/Combobox/Combobox.d.ts +8 -0
  14. package/dist/packages/components/Combobox/doc.md +680 -0
  15. package/dist/packages/components/DataTable/doc.md +1124 -0
  16. package/dist/packages/components/DatePicker/doc.md +579 -0
  17. package/dist/packages/components/DateRangePicker/doc.md +638 -0
  18. package/dist/packages/components/Drawer/doc.md +338 -0
  19. package/dist/packages/components/Dropdown/Dropdown.d.ts +4 -0
  20. package/dist/packages/components/Dropdown/doc.md +205 -0
  21. package/dist/packages/components/EmptyState/doc.md +101 -0
  22. package/dist/packages/components/FileUpload/doc.md +449 -0
  23. package/dist/packages/components/Filter/doc.md +196 -0
  24. package/dist/packages/components/Header/doc.md +373 -0
  25. package/dist/packages/components/I18nProvider/doc.md +187 -0
  26. package/dist/packages/components/Icon/doc.md +63 -0
  27. package/dist/packages/components/Label/doc.md +60 -0
  28. package/dist/packages/components/LinearProgressBar/doc.md +148 -0
  29. package/dist/packages/components/Link/doc.md +206 -0
  30. package/dist/packages/components/List/doc.md +481 -0
  31. package/dist/packages/components/Loader/doc.md +53 -0
  32. package/dist/packages/components/Menu/Menu.d.ts +5 -1
  33. package/dist/packages/components/Menu/doc.md +231 -0
  34. package/dist/packages/components/Message/doc.md +166 -0
  35. package/dist/packages/components/Modal/doc.md +289 -0
  36. package/dist/packages/components/Navigation/doc.md +992 -0
  37. package/dist/packages/components/NavigationItem/doc.md +167 -0
  38. package/dist/packages/components/NotificationCard/doc.md +206 -0
  39. package/dist/packages/components/Notifications/doc.md +240 -0
  40. package/dist/packages/components/NumberField/doc.md +582 -0
  41. package/dist/packages/components/PageLayout/doc.md +651 -0
  42. package/dist/packages/components/Pagination/doc.md +227 -0
  43. package/dist/packages/components/Popover/doc.md +245 -0
  44. package/dist/packages/components/Radio/doc.md +370 -0
  45. package/dist/packages/components/RouterProvider/doc.md +64 -0
  46. package/dist/packages/components/SearchBar/doc.md +504 -0
  47. package/dist/packages/components/SegmentedControl/doc.md +398 -0
  48. package/dist/packages/components/Select/Select.d.ts +4 -0
  49. package/dist/packages/components/Select/doc.md +1133 -0
  50. package/dist/packages/components/Skeleton/doc.md +129 -0
  51. package/dist/packages/components/Slider/doc.md +362 -0
  52. package/dist/packages/components/Stepper/doc.md +104 -0
  53. package/dist/packages/components/Switch/doc.md +296 -0
  54. package/dist/packages/components/Tabs/doc.md +295 -0
  55. package/dist/packages/components/Tag/doc.md +81 -0
  56. package/dist/packages/components/TextInput/doc.md +490 -0
  57. package/dist/packages/components/TimeField/doc.md +353 -0
  58. package/dist/packages/components/Timeline/doc.md +1046 -0
  59. package/dist/packages/components/Toaster/doc.md +263 -0
  60. package/dist/packages/components/ToggleButton/doc.md +108 -0
  61. package/dist/packages/components/ToggleButtonGroup/doc.md +307 -0
  62. package/dist/packages/components/Tooltip/doc.md +206 -0
  63. package/dist/packages/components/YearMonthPicker/YearMonthPicker.d.ts +8 -0
  64. package/dist/packages/components/YearMonthPicker/doc.md +638 -0
  65. package/dist/public_docs/components.md +68 -0
  66. package/dist/public_docs/index.md +30 -0
  67. package/dist/public_docs/tokens.md +121 -0
  68. package/package.json +3 -2
@@ -0,0 +1,992 @@
1
+ # Navigation
2
+
3
+ ## Props
4
+
5
+ The complete Props documentation with JS doc for this component is available at this path:
6
+
7
+ node_modules/@agregio-solutions/design-system/dist/packages/components/Navigation/Navigation.d.ts
8
+
9
+ ## Example usage
10
+
11
+ Here are the Storybook Stories.
12
+
13
+ Base stories:
14
+
15
+ ```tsx
16
+ import { Meta, StoryObj } from "@storybook/react-vite";
17
+ import Navigation from "./Navigation";
18
+ import NavigationItem from "../NavigationItem/NavigationItem";
19
+ import { I18nProvider } from "react-aria-components";
20
+ import { fn } from "storybook/test";
21
+
22
+ const meta: Meta<typeof Navigation> = {
23
+ component: Navigation,
24
+ parameters: {
25
+ layout: "none",
26
+ },
27
+ argTypes: {
28
+ children: { control: false },
29
+ },
30
+ decorators: [
31
+ (Story) => (
32
+ <I18nProvider locale="EN-en">
33
+ <Story />
34
+ </I18nProvider>
35
+ ),
36
+ ],
37
+ };
38
+ export default meta;
39
+
40
+ type Story = StoryObj<typeof meta>;
41
+
42
+ export const Playground: Story = {
43
+ args: {
44
+ logo: "agregio",
45
+ onParametersClick: fn(),
46
+ onLogoutClick: fn(),
47
+ children: (
48
+ <>
49
+ <NavigationItem label="Item 1" href="/item-1" iconLeft="home" />
50
+
51
+ <NavigationItem
52
+ label="Item 2"
53
+ iconLeft="business"
54
+ subLinks={[
55
+ { href: "/item-2-1", label: "Item 2.1" },
56
+ { href: "/item-2-2", label: "Item 2.2" },
57
+ ]}
58
+ />
59
+
60
+ <NavigationItem
61
+ label="Item 3"
62
+ href="/item-3"
63
+ iconLeft="bolt"
64
+ isActive
65
+ />
66
+ </>
67
+ ),
68
+ },
69
+ };
70
+
71
+ export const NoSettings: Story = {
72
+ args: {
73
+ ...Playground.args,
74
+ onParametersClick: undefined,
75
+ },
76
+ };
77
+
78
+ export const NoLogout: Story = {
79
+ args: {
80
+ ...Playground.args,
81
+ onLogoutClick: undefined,
82
+ },
83
+ };
84
+
85
+ export const NoFooter: Story = {
86
+ args: {
87
+ ...Playground.args,
88
+ onParametersClick: undefined,
89
+ onLogoutClick: undefined,
90
+ },
91
+ };
92
+
93
+ export const InitialCollapsed: Story = {
94
+ args: {
95
+ ...Playground.args,
96
+ initialIsCollapsed: true,
97
+ },
98
+ };
99
+
100
+ export const WithSubItemActive: Story = {
101
+ args: {
102
+ ...Playground.args,
103
+ children: (
104
+ <>
105
+ <NavigationItem
106
+ label="Item 1"
107
+ iconLeft="business"
108
+ defaultOpen
109
+ subLinks={[
110
+ { href: "/item-2-1", label: "Item 1.1" },
111
+ { href: "/item-2-2", label: "Item 1.2", isActive: true },
112
+ ]}
113
+ />
114
+ </>
115
+ ),
116
+ },
117
+ };
118
+
119
+ export const LotsOfItems: Story = {
120
+ args: {
121
+ ...Playground.args,
122
+ children: (
123
+ <>
124
+ {Array.from({ length: 20 }).map((_, index) => (
125
+ <NavigationItem
126
+ key={index}
127
+ label={`Item ${index + 1}`}
128
+ iconLeft="business"
129
+ />
130
+ ))}
131
+ </>
132
+ ),
133
+ },
134
+ };
135
+
136
+ export const WithFooterElements: Story = {
137
+ args: {
138
+ ...Playground.args,
139
+ footerElements: (
140
+ <>
141
+ <NavigationItem label="FAQ" iconLeft="help_outline" />
142
+ </>
143
+ ),
144
+ },
145
+ };
146
+ ```
147
+
148
+ ## How to test this component
149
+
150
+ Here are some more advanced stories with more testing coverage and examples that you can read to understand how to test this component.
151
+
152
+ ```tsx
153
+ import { Meta, StoryObj } from "@storybook/react-vite";
154
+ import Navigation from "../Navigation";
155
+ import NavigationItem from "../../NavigationItem/NavigationItem";
156
+ import * as NavigationStories from "../Navigation.stories";
157
+ import { expect, userEvent, waitFor, within } from "storybook/test";
158
+ import {
159
+ expectNotPresent,
160
+ expectPresent,
161
+ } from "@internal/test-utils-storybook/test-utils-storybook";
162
+ import { Link, Outlet, Route, Routes, useLocation } from "react-router-dom";
163
+
164
+ const meta: Meta<typeof Navigation> = {
165
+ ...NavigationStories.default,
166
+ parameters: {
167
+ ...NavigationStories.default.parameters,
168
+ chromatic: { disableSnapshot: true },
169
+ },
170
+ component: Navigation,
171
+ };
172
+ export default meta;
173
+
174
+ type Story = StoryObj<typeof meta>;
175
+
176
+ export const BasicExampleUsage: Story = {
177
+ render: () => (
178
+ <Navigation logo="agregio">
179
+ <NavigationItem
180
+ label="Item 1"
181
+ iconLeft="business"
182
+ subLinks={[
183
+ { href: "/page-2/item-1", label: "Item 1.1" },
184
+ { href: "/page-2/item-2", label: "Item 1.2" },
185
+ ]}
186
+ />
187
+ </Navigation>
188
+ ),
189
+ };
190
+
191
+ export const ShouldHandleSubLinksWhenTheNavigationIsNotCollapsed: Story = {
192
+ render: BasicExampleUsage.render,
193
+ play: async ({ canvasElement }) => {
194
+ const canvas = within(canvasElement);
195
+ const user = userEvent.setup();
196
+ await canvas.findByText("Item 1");
197
+ await expectNotPresent(() => canvas.queryByText("Item 1.1"));
198
+ await expectNotPresent(() => canvas.queryByText("Item 1.2"));
199
+
200
+ await user.click(canvas.getByText("Item 1"));
201
+ await expectPresent(() => canvas.queryByText("Item 1.1"));
202
+ await expectPresent(() => canvas.queryByText("Item 1.2"));
203
+
204
+ await user.click(canvas.getByText("Item 1"));
205
+ await expectNotPresent(() => canvas.queryByText("Item 1.1"));
206
+ await expectNotPresent(() => canvas.queryByText("Item 1.2"));
207
+ },
208
+ };
209
+
210
+ export const ShouldHandleSubLinksWhenTheNavigationIsCollapsed: Story = {
211
+ render: BasicExampleUsage.render,
212
+ play: async ({ canvasElement }) => {
213
+ const canvas = within(canvasElement);
214
+ const user = userEvent.setup();
215
+
216
+ await user.click(canvas.getByTestId("navigation-mobile-collapsed-menu"));
217
+
218
+ await expect(canvas.getByText("Item 1")).not.toBeVisible();
219
+ await expectNotPresent(() => canvas.queryByText("Item 1.1"));
220
+ await expectNotPresent(() => canvas.queryByText("Item 1.2"));
221
+
222
+ await user.click(canvas.getByLabelText("Item 1"));
223
+
224
+ await canvas.findByText("Item 1");
225
+ await expectPresent(() => canvas.queryByText("Item 1.1"));
226
+ await expectPresent(() => canvas.queryByText("Item 1.2"));
227
+
228
+ await user.click(canvas.getByTestId("navigation-mobile-collapsed-menu"));
229
+ await expect(canvas.getByText("Item 1")).not.toBeVisible();
230
+ await expectNotPresent(() => canvas.queryByText("Item 1.1"));
231
+ await expectNotPresent(() => canvas.queryByText("Item 1.2"));
232
+ },
233
+ };
234
+
235
+ export const BaseStoryForTheOpenStateTests: Story = {
236
+ render: () => {
237
+ const AppExample = () => {
238
+ const location = useLocation();
239
+ const path = location.pathname;
240
+
241
+ return (
242
+ <div style={{ display: "flex", gap: "1rem" }}>
243
+ <Navigation logo="agregio" currentPath={path}>
244
+ <NavigationItem
245
+ label="Page 1"
246
+ iconLeft="business"
247
+ subLinks={[
248
+ { href: "/page-1/item-1", label: "Item 1.1" },
249
+ { href: "/page-1/item-2", label: "Item 1.2" },
250
+ ]}
251
+ />
252
+ <NavigationItem
253
+ label="Page 2"
254
+ iconLeft="access_time"
255
+ subLinks={[
256
+ { href: "/page-2/item-1", label: "Item 2.1" },
257
+ { href: "/page-2/item-2", label: "Item 2.2" },
258
+ ]}
259
+ />
260
+ </Navigation>
261
+
262
+ <main>
263
+ <ul>
264
+ <li>
265
+ <Link to="/">Link to: Home</Link>
266
+ </li>
267
+ <li>
268
+ <Link to="/page-1/item-1">Link to: Item 1.1</Link>
269
+ </li>
270
+ <li>
271
+ <Link to="/page-1/item-2">Link to: Item 1.2</Link>
272
+ </li>
273
+ <li>
274
+ <Link to="/page-2/item-1">Link to: Item 2.1</Link>
275
+ </li>
276
+ <li>
277
+ <Link to="/page-2/item-2">Link to: Item 2.2</Link>
278
+ </li>
279
+ </ul>
280
+
281
+ <Outlet />
282
+ </main>
283
+ </div>
284
+ );
285
+ };
286
+ return (
287
+ <Routes>
288
+ <Route element={<AppExample />}>
289
+ <Route path="/" element={<div>Page: /</div>} />
290
+ <Route
291
+ path="/page-1/item-1"
292
+ element={<div>Page: /page-1/item-1</div>}
293
+ />
294
+ <Route
295
+ path="/page-1/item-2"
296
+ element={<div>Page: /page-1/item-2</div>}
297
+ />
298
+ <Route
299
+ path="/page-2/item-1"
300
+ element={<div>Page: /page-2/item-1</div>}
301
+ />
302
+ <Route
303
+ path="/page-2/item-2"
304
+ element={<div>Page: /page-2/item-2</div>}
305
+ />
306
+ </Route>
307
+ </Routes>
308
+ );
309
+ },
310
+ };
311
+
312
+ // # Tests en commençant avec l'URL / et en cliquant en premier sur "Page 1"
313
+
314
+ // ## Test : L'utilisateur clique sur "Page 1"
315
+ // - le dropdown de "Page 1" s'ouvre
316
+
317
+ export const ShouldOpenTheDropDownMenu: Story = {
318
+ render: BaseStoryForTheOpenStateTests.render,
319
+ parameters: {
320
+ reactRouter: {
321
+ initialEntries: ["/"],
322
+ customRoutes: true,
323
+ },
324
+ },
325
+ play: async ({ canvasElement }) => {
326
+ const canvas = within(canvasElement);
327
+ const user = userEvent.setup();
328
+
329
+ await canvas.findByText("Page: /");
330
+ await canvas.findByText("Page 1");
331
+ await expect(canvas.queryByText("Item 1.1")).not.toBeInTheDocument();
332
+ await expect(canvas.queryByText("Item 1.2")).not.toBeInTheDocument();
333
+ await canvas.findByText("Page 2");
334
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
335
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
336
+
337
+ await user.click(canvas.getByText("Page 1"));
338
+
339
+ await canvas.findByText("Page 1");
340
+ await canvas.findByText("Item 1.1");
341
+ await canvas.findByText("Item 1.2");
342
+ await canvas.findByText("Page 2");
343
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
344
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
345
+ await canvas.findByText("Page: /");
346
+ },
347
+ };
348
+
349
+ // ## Test: L'utilisateur clique sur "Page 1" puis à nouveau sur "Page 1"
350
+ // - le dropdown de "Page 1" s'ouvre puis se ferme
351
+
352
+ export const ShouldToggleTheDropDownMenu: Story = {
353
+ render: BaseStoryForTheOpenStateTests.render,
354
+ parameters: {
355
+ reactRouter: {
356
+ initialEntries: ["/"],
357
+ customRoutes: true,
358
+ },
359
+ },
360
+ play: async ({ canvasElement }) => {
361
+ const canvas = within(canvasElement);
362
+ const user = userEvent.setup();
363
+
364
+ await canvas.findByText("Page: /");
365
+ await canvas.findByText("Page 1");
366
+ await expect(canvas.queryByText("Item 1.1")).not.toBeInTheDocument();
367
+ await expect(canvas.queryByText("Item 1.2")).not.toBeInTheDocument();
368
+
369
+ await user.click(canvas.getByText("Page 1"));
370
+ await canvas.findByText("Item 1.1");
371
+ await canvas.findByText("Item 1.2");
372
+
373
+ await user.click(canvas.getByText("Page 1"));
374
+ await expect(canvas.queryByText("Item 1.1")).not.toBeInTheDocument();
375
+ await expect(canvas.queryByText("Item 1.2")).not.toBeInTheDocument();
376
+ await canvas.findByText("Page: /");
377
+ },
378
+ };
379
+
380
+ // ## Test : L'utilisateur clique sur "Page 1" puis sur "Page 2"
381
+ // - le dropdown de "Page 1" s'ouvre
382
+ // - le dropdown de "Page 2" s'ouvre
383
+ // - les 2 dropdowns sont ouverts
384
+
385
+ export const ShouldOpenBothDropDownMenus: Story = {
386
+ render: BaseStoryForTheOpenStateTests.render,
387
+ parameters: {
388
+ reactRouter: {
389
+ initialEntries: ["/"],
390
+ customRoutes: true,
391
+ },
392
+ },
393
+ play: async ({ canvasElement }) => {
394
+ const canvas = within(canvasElement);
395
+ const user = userEvent.setup();
396
+
397
+ await canvas.findByText("Page: /");
398
+ await canvas.findByText("Page 1");
399
+ await expect(canvas.queryByText("Item 1.1")).not.toBeInTheDocument();
400
+ await expect(canvas.queryByText("Item 1.2")).not.toBeInTheDocument();
401
+ await canvas.findByText("Page 2");
402
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
403
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
404
+
405
+ await user.click(canvas.getByText("Page 1"));
406
+ await canvas.findByText("Page 1");
407
+ await canvas.findByText("Item 1.1");
408
+ await canvas.findByText("Item 1.2");
409
+ await canvas.findByText("Page 2");
410
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
411
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
412
+
413
+ await user.click(canvas.getByText("Page 2"));
414
+ await canvas.findByText("Page 1");
415
+ await canvas.findByText("Item 1.1");
416
+ await canvas.findByText("Item 1.2");
417
+ await canvas.findByText("Page 2");
418
+ await canvas.findByText("Item 2.1");
419
+ await canvas.findByText("Item 2.2");
420
+ await canvas.findByText("Page: /");
421
+ },
422
+ };
423
+
424
+ // ## Test : L'utilisateur clique sur "Page 1" puis sur "Sous menu 1.1"
425
+ // - le dropdown de "Page 1" s'ouvre
426
+ // - "Sous menu 1.1" devient "active"
427
+
428
+ export const ShouldClickOnPage1AndThenOnItem1: Story = {
429
+ render: BaseStoryForTheOpenStateTests.render,
430
+ parameters: {
431
+ reactRouter: {
432
+ initialEntries: ["/"],
433
+ customRoutes: true,
434
+ },
435
+ },
436
+ play: async ({ canvasElement }) => {
437
+ const canvas = within(canvasElement);
438
+ const user = userEvent.setup();
439
+
440
+ await canvas.findByText("Page: /");
441
+ await user.click(canvas.getByText("Page 1"));
442
+ await user.click(canvas.getByText("Item 1.1"));
443
+
444
+ await waitFor(async () => {
445
+ await canvas.findByText("Page: /page-1/item-1");
446
+ await canvas.findByText("Page 1");
447
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
448
+ "data-active",
449
+ "true",
450
+ );
451
+ await expect(canvas.getByText("Item 1.1").parentElement).toHaveAttribute(
452
+ "data-active",
453
+ "true",
454
+ );
455
+ await canvas.findByText("Item 1.2");
456
+ await canvas.findByText("Page 2");
457
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
458
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
459
+ });
460
+ },
461
+ };
462
+
463
+ // ## Test : L'utilisateur clique sur "Page 1" puis sur "Sous menu 1.1" puis à nouveau sur "Page 1"
464
+ // - le dropdown de "Page 1" s'ouvre
465
+ // - "Sous menu 1.1" devient "active" ainsi que "Page 1"
466
+ // - le dropdown de "Page 1" se ferme mais reste "active"
467
+
468
+ export const DropdownShouldStayActiveIfItHasASubLinkActiveButIsCosed: Story = {
469
+ render: BaseStoryForTheOpenStateTests.render,
470
+ parameters: {
471
+ reactRouter: {
472
+ initialEntries: ["/"],
473
+ customRoutes: true,
474
+ },
475
+ },
476
+ play: async ({ canvasElement }) => {
477
+ const canvas = within(canvasElement);
478
+ const user = userEvent.setup();
479
+
480
+ await canvas.findByText("Page: /");
481
+ await user.click(canvas.getByText("Page 1"));
482
+ await user.click(canvas.getByText("Item 1.1"));
483
+ await user.click(canvas.getByText("Page 1"));
484
+
485
+ await waitFor(async () => {
486
+ await canvas.findByText("Page: /page-1/item-1");
487
+ await canvas.findByText("Page 1");
488
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
489
+ "data-active",
490
+ "true",
491
+ );
492
+ await expect(canvas.queryByText("Item 1.1")).not.toBeInTheDocument();
493
+ await expect(canvas.queryByText("Item 1.2")).not.toBeInTheDocument();
494
+ await canvas.findByText("Page 2");
495
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
496
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
497
+ });
498
+ },
499
+ };
500
+
501
+ // # Tests en commençant avec l'URL / et en suivant un lien qui mène à "Sous menu 1.1"
502
+
503
+ // ## Test : L'utilisateur suit un lien qui mène à "Sous menu 1.1"
504
+ // - le dropdown de "Page 1" s'ouvre
505
+ // - "Sous menu 1.1" devient "active"
506
+
507
+ export const UserFollowsALinkThatLeadsToItem1: Story = {
508
+ render: BaseStoryForTheOpenStateTests.render,
509
+ parameters: {
510
+ reactRouter: {
511
+ initialEntries: ["/"],
512
+ customRoutes: true,
513
+ },
514
+ },
515
+ play: async ({ canvasElement }) => {
516
+ const canvas = within(canvasElement);
517
+ const user = userEvent.setup();
518
+
519
+ await canvas.findByText("Page: /");
520
+ await user.click(canvas.getByText("Link to: Item 1.1"));
521
+
522
+ await waitFor(async () => {
523
+ await canvas.findByText("Page 1");
524
+ await canvas.findByText("Item 1.1");
525
+ await canvas.findByText("Item 1.2");
526
+ await canvas.findByText("Page 2");
527
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
528
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
529
+ await canvas.findByText("Page: /page-1/item-1");
530
+ });
531
+ },
532
+ };
533
+
534
+ // ## Test : L'utilisateur suit un lien qui mène à "Sous menu 1.1" puis clique sur "Page 1"
535
+ // - le dropdown de "Page 1" s'ouvre
536
+ // - "Sous menu 1.1" devient "active"
537
+ // - puis le dropdown de "Page 1" se ferme
538
+
539
+ export const UserFollowsALinkThatLeadsToItem1AndThenClosesTheDropdown: Story = {
540
+ render: BaseStoryForTheOpenStateTests.render,
541
+ parameters: {
542
+ reactRouter: {
543
+ initialEntries: ["/"],
544
+ customRoutes: true,
545
+ },
546
+ },
547
+ play: async ({ canvasElement }) => {
548
+ const canvas = within(canvasElement);
549
+ const user = userEvent.setup();
550
+
551
+ await canvas.findByText("Page: /");
552
+ await user.click(canvas.getByText("Link to: Item 1.1"));
553
+ await user.click(canvas.getByText("Page 1"));
554
+
555
+ await waitFor(async () => {
556
+ await canvas.findByText("Page 1");
557
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
558
+ "data-active",
559
+ "true",
560
+ );
561
+ await expect(canvas.queryByText("Item 1.1")).not.toBeInTheDocument();
562
+ await expect(canvas.queryByText("Item 1.2")).not.toBeInTheDocument();
563
+ await canvas.findByText("Page 2");
564
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
565
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
566
+ await canvas.findByText("Page: /page-1/item-1");
567
+ });
568
+ },
569
+ };
570
+
571
+ // ## Test : L'utilisateur suit un lien qui mène à "Sous menu 1.1" puis clique sur "Page 2"
572
+ // - le dropdown de "Page 1" s'ouvre
573
+ // - "Sous menu 1.1" devient "active"
574
+ // - le dropdown de "Page 2" s'ouvre
575
+ // - les 2 dropdowns sont ouverts
576
+
577
+ export const UserFollowsALinkThatLeadsToItem1AndThenOpensTheDropdown2: Story = {
578
+ render: BaseStoryForTheOpenStateTests.render,
579
+ parameters: {
580
+ reactRouter: {
581
+ initialEntries: ["/"],
582
+ customRoutes: true,
583
+ },
584
+ },
585
+ play: async ({ canvasElement }) => {
586
+ const canvas = within(canvasElement);
587
+ const user = userEvent.setup();
588
+
589
+ await canvas.findByText("Page: /");
590
+ await user.click(canvas.getByText("Link to: Item 1.1"));
591
+ await user.click(canvas.getByText("Page 2"));
592
+
593
+ await waitFor(async () => {
594
+ await canvas.findByText("Page 1");
595
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
596
+ "data-active",
597
+ "true",
598
+ );
599
+ await expect(canvas.getByText("Item 1.1").parentElement).toHaveAttribute(
600
+ "data-active",
601
+ "true",
602
+ );
603
+ await canvas.findByText("Item 1.2");
604
+ await canvas.findByText("Page 2");
605
+ await canvas.findByText("Item 2.1");
606
+ await canvas.findByText("Item 2.2");
607
+ await canvas.findByText("Page: /page-1/item-1");
608
+ });
609
+ },
610
+ };
611
+
612
+ // # Tests en commençant avec l'URL de "Sous menu 1.1"
613
+
614
+ // ## Test : L'utilisateur ouvre une page qui mène à "Sous menu 1.1"
615
+
616
+ // - le dropdown de "Page 1" est déjà ouvert
617
+ // - "Sous menu 1.1" est déjà "active"
618
+
619
+ export const UserStartsFromItem1: Story = {
620
+ render: BaseStoryForTheOpenStateTests.render,
621
+ parameters: {
622
+ reactRouter: {
623
+ initialEntries: ["/page-1/item-1"],
624
+ customRoutes: true,
625
+ },
626
+ },
627
+ play: async ({ canvasElement }) => {
628
+ const canvas = within(canvasElement);
629
+
630
+ await canvas.findByText("Page: /page-1/item-1");
631
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
632
+ "data-active",
633
+ "true",
634
+ );
635
+ await canvas.findByText("Item 1.1");
636
+ await expect(canvas.getByText("Item 1.1").parentElement).toHaveAttribute(
637
+ "data-active",
638
+ "true",
639
+ );
640
+ await canvas.findByText("Item 1.2");
641
+ await canvas.findByText("Page 2");
642
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
643
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
644
+ await canvas.findByText("Page: /page-1/item-1");
645
+ },
646
+ };
647
+
648
+ // ## Test : L'utilisateur ouvre une page qui mène à "Sous menu 1.1" puis clique sur "Page 1"
649
+
650
+ // - le dropdown de "Page 1" est déjà ouvert
651
+ // - "Sous menu 1.1" est déjà "active"
652
+ // - puis le dropdown de "Page 1" se ferme
653
+ export const UserStartsFromItem1AndThenOpensTheDropdown1: Story = {
654
+ render: BaseStoryForTheOpenStateTests.render,
655
+ parameters: {
656
+ reactRouter: {
657
+ initialEntries: ["/page-1/item-1"],
658
+ customRoutes: true,
659
+ },
660
+ },
661
+ play: async ({ canvasElement }) => {
662
+ const canvas = within(canvasElement);
663
+ const user = userEvent.setup();
664
+
665
+ await canvas.findByText("Page: /page-1/item-1");
666
+ await user.click(canvas.getByText("Page 1"));
667
+
668
+ await waitFor(async () => {
669
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
670
+ "data-active",
671
+ "true",
672
+ );
673
+ await expect(canvas.queryByText("Item 1.1")).not.toBeInTheDocument();
674
+ await expect(canvas.queryByText("Item 1.2")).not.toBeInTheDocument();
675
+ await canvas.findByText("Page 2");
676
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
677
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
678
+ await canvas.findByText("Page: /page-1/item-1");
679
+ });
680
+ },
681
+ };
682
+
683
+ // ## Test : L'utilisateur ouvre une page qui mène à "Sous menu 1.1" puis clique sur "Page 2"
684
+
685
+ // - le dropdown de "Page 1" s'ouvre
686
+ // - le dropdown de "Page 2" s'ouvre
687
+ // - les 2 dropdowns sont ouverts
688
+ export const UserStartsFromItem1AndThenOpensTheDropdown2: Story = {
689
+ render: BaseStoryForTheOpenStateTests.render,
690
+ parameters: {
691
+ reactRouter: {
692
+ initialEntries: ["/page-1/item-1"],
693
+ customRoutes: true,
694
+ },
695
+ },
696
+ play: async ({ canvasElement }) => {
697
+ const canvas = within(canvasElement);
698
+ const user = userEvent.setup();
699
+
700
+ await canvas.findByText("Page: /page-1/item-1");
701
+ await user.click(canvas.getByText("Page 2"));
702
+
703
+ await waitFor(async () => {
704
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
705
+ "data-active",
706
+ "true",
707
+ );
708
+ await expect(canvas.getByText("Item 1.1").parentElement).toHaveAttribute(
709
+ "data-active",
710
+ "true",
711
+ );
712
+ await canvas.findByText("Item 1.2");
713
+ await canvas.findByText("Page 2");
714
+ await canvas.findByText("Item 2.1");
715
+ await canvas.findByText("Item 2.2");
716
+ await canvas.findByText("Page: /page-1/item-1");
717
+ });
718
+ },
719
+ };
720
+
721
+ // ## Test : L'utilisateur ouvre une page qui mène à "Sous menu 1.1" puis suit un lien vers "Sous menu 1.2"
722
+
723
+ // - le dropdown de "Page 1" est déjà ouvert
724
+ // - "Sous menu 1.1" est déjà "active"
725
+ // - "Sous menu 1.2" devient "active" à la place de "Sous menu 1.1"
726
+ export const UserStartsFromItem1AndThenFollowsALinkToItem12: Story = {
727
+ render: BaseStoryForTheOpenStateTests.render,
728
+ parameters: {
729
+ reactRouter: {
730
+ initialEntries: ["/page-1/item-1"],
731
+ customRoutes: true,
732
+ },
733
+ },
734
+ play: async ({ canvasElement }) => {
735
+ const canvas = within(canvasElement);
736
+ const user = userEvent.setup();
737
+
738
+ await canvas.findByText("Page: /page-1/item-1");
739
+ await user.click(canvas.getByText("Link to: Item 1.2"));
740
+
741
+ await waitFor(async () => {
742
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
743
+ "data-active",
744
+ "true",
745
+ );
746
+ await canvas.findByText("Item 1.1");
747
+ await expect(canvas.getByText("Item 1.2").parentElement).toHaveAttribute(
748
+ "data-active",
749
+ "true",
750
+ );
751
+ await canvas.findByText("Page 2");
752
+ await expect(canvas.queryByText("Item 2.1")).not.toBeInTheDocument();
753
+ await expect(canvas.queryByText("Item 2.2")).not.toBeInTheDocument();
754
+ await canvas.findByText("Page: /page-1/item-2");
755
+ });
756
+ },
757
+ };
758
+
759
+ // ## Test : L'utilisateur ouvre une page qui mène à "Sous menu 1.1" puis suit un lien vers "Sous menu 2.1"
760
+
761
+ // - le dropdown de "Page 1" est déjà ouvert
762
+ // - "Sous menu 1.1" est déjà "active"
763
+ // - le dropdown de "Page 2" s'ouvre
764
+ // - "Sous menu 2.1" devient "active" à la place de "Sous menu 1.1"
765
+ // - les 2 dropdowns sont ouverts
766
+ export const UserStartsFromItem1AndThenFollowsALinkToItem21: Story = {
767
+ render: BaseStoryForTheOpenStateTests.render,
768
+ parameters: {
769
+ reactRouter: {
770
+ initialEntries: ["/page-1/item-1"],
771
+ customRoutes: true,
772
+ },
773
+ },
774
+ play: async ({ canvasElement }) => {
775
+ const canvas = within(canvasElement);
776
+ const user = userEvent.setup();
777
+
778
+ await canvas.findByText("Page: /page-1/item-1");
779
+ await user.click(canvas.getByText("Link to: Item 2.1"));
780
+
781
+ await waitFor(async () => {
782
+ await canvas.findByText("Page 1");
783
+ await canvas.findByText("Item 1.1");
784
+ await canvas.findByText("Item 1.2");
785
+ await expect(canvas.getByText("Page 2").parentElement).toHaveAttribute(
786
+ "data-active",
787
+ "true",
788
+ );
789
+ await expect(canvas.getByText("Item 2.1").parentElement).toHaveAttribute(
790
+ "data-active",
791
+ "true",
792
+ );
793
+ await canvas.findByText("Item 2.2");
794
+ await canvas.findByText("Page: /page-2/item-1");
795
+ });
796
+ },
797
+ };
798
+
799
+ // # Tests pour le NavigationItem avec href="/"
800
+ export const BaseStoryForRootPathTests: Story = {
801
+ render: () => {
802
+ const AppExample = () => {
803
+ const location = useLocation();
804
+ const path = location.pathname;
805
+
806
+ return (
807
+ <div style={{ display: "flex", gap: "1rem" }}>
808
+ <Navigation logo="agregio" currentPath={path}>
809
+ <NavigationItem label="Home" iconLeft="home" href="/" />
810
+ <NavigationItem label="Page 1" iconLeft="business" href="/page-1" />
811
+ </Navigation>
812
+
813
+ <main>
814
+ <ul>
815
+ <li>
816
+ <Link to="/">Link to: Home</Link>
817
+ </li>
818
+ <li>
819
+ <Link to="/page-1">Link to: Page 1</Link>
820
+ </li>
821
+ </ul>
822
+ <Outlet />
823
+ </main>
824
+ </div>
825
+ );
826
+ };
827
+ return (
828
+ <Routes>
829
+ <Route element={<AppExample />}>
830
+ <Route path="/" element={<div>Page: /</div>} />
831
+ <Route path="/page-1" element={<div>Page: /page-1</div>} />
832
+ </Route>
833
+ </Routes>
834
+ );
835
+ },
836
+ };
837
+
838
+ // ## Test : L'item "Home" (href="/") est actif sur la route "/"
839
+ // - "Home" a data-active="true"
840
+ // - "Page 1" a data-active="false"
841
+ export const HomeItemShouldBeActiveOnRootPath: Story = {
842
+ render: BaseStoryForRootPathTests.render,
843
+ parameters: {
844
+ reactRouter: {
845
+ initialEntries: ["/"],
846
+ customRoutes: true,
847
+ },
848
+ },
849
+ play: async ({ canvasElement }) => {
850
+ const canvas = within(canvasElement);
851
+
852
+ await canvas.findByText("Page: /");
853
+ await expect(canvas.getByText("Home").parentElement).toHaveAttribute(
854
+ "data-active",
855
+ "true",
856
+ );
857
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
858
+ "data-active",
859
+ "false",
860
+ );
861
+ },
862
+ };
863
+
864
+ // ## Test : L'item "Home" (href="/") n'est PAS actif sur une autre route
865
+ // - "Home" a data-active="false"
866
+ // - "Page 1" a data-active="true"
867
+ export const HomeItemShouldNotBeActiveOnOtherPaths: Story = {
868
+ render: BaseStoryForRootPathTests.render,
869
+ parameters: {
870
+ reactRouter: {
871
+ initialEntries: ["/page-1"],
872
+ customRoutes: true,
873
+ },
874
+ },
875
+ play: async ({ canvasElement }) => {
876
+ const canvas = within(canvasElement);
877
+
878
+ await canvas.findByText("Page: /page-1");
879
+ await expect(canvas.getByText("Home").parentElement).toHaveAttribute(
880
+ "data-active",
881
+ "false",
882
+ );
883
+ await expect(canvas.getByText("Page 1").parentElement).toHaveAttribute(
884
+ "data-active",
885
+ "true",
886
+ );
887
+ },
888
+ };
889
+
890
+ export const WithPermissionsWrapperOnSubItem: Story = {
891
+ render: () => {
892
+ const PermissionsWrapper: React.FC<{ children: React.ReactNode }> = () => {
893
+ return null;
894
+ };
895
+
896
+ return (
897
+ <Navigation logo="agregio">
898
+ <NavigationItem
899
+ label="Item 1"
900
+ iconLeft="business"
901
+ subLinks={[
902
+ { href: "/page-2/item-1", label: "Item 1.1" },
903
+ {
904
+ href: "/page-2/item-2",
905
+ label: "Item 1.2",
906
+ wrapperComponent: (children) => (
907
+ <PermissionsWrapper>{children}</PermissionsWrapper>
908
+ ),
909
+ },
910
+ { href: "/page-2/item-3", label: "Item 1.3" },
911
+ ]}
912
+ />
913
+ </Navigation>
914
+ );
915
+ },
916
+ play: async ({ canvasElement }) => {
917
+ const canvas = within(canvasElement);
918
+ const user = userEvent.setup();
919
+
920
+ await user.click(canvas.getByText("Item 1"));
921
+ await canvas.findByText("Item 1.1");
922
+ await expectNotPresent(() => canvas.queryByText("Item 1.2"));
923
+ await canvas.findByText("Item 1.3");
924
+ },
925
+ };
926
+ ```
927
+
928
+ ## Developer notes
929
+
930
+ Here are the notes available for the developer on the built Storybook, you can read them to understand the component and how to use it.
931
+
932
+ ```mdx
933
+ import {
934
+ Canvas,
935
+ Meta,
936
+ Stories,
937
+ Controls,
938
+ Source,
939
+ } from "@storybook/addon-docs/blocks";
940
+
941
+ import * as Navigation from "./Navigation.stories";
942
+ import * as NavigationTests from "./tests/Navigation.stories";
943
+
944
+ <Meta of={Navigation} />
945
+
946
+ # Navigation
947
+
948
+ <Controls of={Navigation.Playground} />
949
+
950
+ ## Usage
951
+
952
+ Here is a basic example of how to use the `Navigation` component.
953
+
954
+ The example includes :
955
+
956
+ - a basic Routes definitions with react-router
957
+ - the `Navigation` component (using the `currentPath` prop to highlight the current active page)
958
+ - a `main` element that will display the page that is currently selected
959
+
960
+ Your application code may be different (depending on your router and your application structure), but the `Navigation` should work the same way.
961
+
962
+ <Source of={NavigationTests.BaseStoryForTheOpenStateTests} type="code" dark />
963
+
964
+ **Please note that this example is using the `currentPath` prop, which is currently experimental.** We are actually testing this feature and it is the way we want to handle the open and active states of the navigation items.
965
+
966
+ This prop enable a complete internal control of the open and active states of the navigation items.
967
+
968
+ For example, with this prop, the `Navigation` component will be able to highlight the active item based on the current path.
969
+ It will also allows the dropdown to open if the user follow a link that is one of its `subLinks`.
970
+
971
+ ## Manually control the open and active states of the navigation items
972
+
973
+ In case you need more control over the open and active states of the navigation items, you can use some some of the following props:
974
+
975
+ - `initialIsCollapsed` on the `Navigation` component
976
+ - `isActive` on the `NavigationItem` component
977
+ - `defaultOpen` on the `NavigationItem` component
978
+ - `onClick` on the `NavigationItem` component
979
+ - `isActive` on the `subLinks` of the `NavigationItem` component
980
+
981
+ **Please note that those props are actually considered to be replaced by the `currentPath` prop in the future.**
982
+
983
+ ## Regarding the logos
984
+
985
+ There is an exhaustive list of logos to display in the `Navigation` component.
986
+
987
+ This allows us to adjust its position and size to fit the Navigation component.
988
+
989
+ If you need a logo that is not in the list, feel free to ask us to add it!
990
+
991
+ <Stories />
992
+ ```