@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,1124 @@
1
+ # DataTable
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/DataTable/DataTable.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 Filter, { TypeTableColumnWithFilter } from "../Filter/Filter";
18
+ import arrayOfLength from "@internal/arrayOfLength/arrayOfLength";
19
+ import {
20
+ ColumnFiltersState,
21
+ ColumnSort,
22
+ SortingState,
23
+ createColumnHelper,
24
+ getCoreRowModel,
25
+ getExpandedRowModel,
26
+ getFilteredRowModel,
27
+ getPaginationRowModel,
28
+ getSortedRowModel,
29
+ useReactTable,
30
+ } from "@tanstack/react-table";
31
+ import DataTableHeader from "@packages/components/DataTable/DataTableHeader/DataTableHeader";
32
+ import { useMemo, useState } from "react";
33
+ import { useQuery, keepPreviousData } from "@tanstack/react-query";
34
+ import DataTableCell from "@packages/components/DataTable/DataTableCell/DataTableCell";
35
+ import DataTable from "@packages/components/DataTable/DataTable";
36
+ import CheckboxGroup from "../CheckboxGroup/CheckboxGroup";
37
+ import Checkbox from "../Checkbox/Checkbox";
38
+ import Radio from "../Radio/Radio";
39
+ import { I18nProvider } from "react-aria-components";
40
+ import Select from "../Select/Select";
41
+ import SelectItem from "../SelectItem/SelectItem";
42
+ import Button from "../Button/Button";
43
+ import Switch from "../Switch/Switch";
44
+ import { within } from "storybook/test";
45
+ import { someTime } from "@internal/test-utils-storybook/test-utils-storybook";
46
+ import DataTableRoot from "./DataTableRoot/DataTableRoot";
47
+ import DataTableRow from "./DataTableRow/DataTableRow";
48
+
49
+ const meta: Meta<typeof DataTable> = {
50
+ component: DataTable,
51
+ parameters: {
52
+ layout: "padded",
53
+ },
54
+ globals: {
55
+ backgrounds: { value: "light" },
56
+ },
57
+ decorators: [
58
+ (Story) => (
59
+ <I18nProvider locale={"en-EN"}>
60
+ <Story />
61
+ </I18nProvider>
62
+ ),
63
+ ],
64
+ };
65
+ export default meta;
66
+
67
+ export const WithReactTableFrontEndManaged: StoryObj<typeof DataTable> = {
68
+ render: () => {
69
+ const ParentComponent = () => {
70
+ // Below are just type definition of the data and its generation
71
+ // Typically you would get this from your API and will not have to create them here
72
+ // We let it so you can fully understand how the filter works based on the data provided
73
+
74
+ type User = {
75
+ firstname: string;
76
+ lastname: string;
77
+ email: string;
78
+ isValidated?: boolean;
79
+ day?: string;
80
+ foods?: Array<string>;
81
+ organization?: { id: string; name: string };
82
+ isActive?: boolean;
83
+ };
84
+
85
+ const ALL_FOODS = useMemo(
86
+ () => ["Pizza", "Hamburger", "Ramen", "Sushi", "Fries"],
87
+ [],
88
+ );
89
+
90
+ const FOODS = useMemo(
91
+ () => [
92
+ [ALL_FOODS[0], ALL_FOODS[1]],
93
+ [ALL_FOODS[1], ALL_FOODS[2]],
94
+ [ALL_FOODS[2], ALL_FOODS[3]],
95
+ [ALL_FOODS[3], ALL_FOODS[4]],
96
+ [ALL_FOODS[4], ALL_FOODS[0]],
97
+ ],
98
+ [ALL_FOODS],
99
+ );
100
+
101
+ const ORGANIZATIONS = useMemo(
102
+ () => [
103
+ { id: "1", name: "EDF Store and Forecast" },
104
+ { id: "2", name: "Agregio Solutions" },
105
+ { id: "3", name: "Dalkia" },
106
+ ],
107
+ [],
108
+ );
109
+
110
+ const DAYS = useMemo(
111
+ () => ["Monday", "Tuesday", "Wednesday", "Thursday"],
112
+ [],
113
+ );
114
+
115
+ const data: Array<User> = useMemo(
116
+ () =>
117
+ arrayOfLength(300).map((index) => ({
118
+ firstname: `Firstname ${index + 1}`,
119
+ lastname: `Lastname ${index + 1}`,
120
+ email: `john-doe${index + 1}@example.com`,
121
+ isValidated: index % 2 === 0,
122
+ foods: FOODS[index % FOODS.length],
123
+ day: DAYS[index % DAYS.length],
124
+ organization: ORGANIZATIONS[index % ORGANIZATIONS.length],
125
+ isActive: index % 2 === 0,
126
+ })),
127
+ [DAYS, FOODS, ORGANIZATIONS],
128
+ );
129
+
130
+ // Here starts the interesting stuff: columns definitions.
131
+ // All the features of the filters comes from those columns definitions
132
+ // Please read carefully each column, they all serve as a different example of how to use the filters
133
+ const columnHelper = useMemo(() => createColumnHelper<User>(), []);
134
+
135
+ const columns = useMemo(
136
+ () => [
137
+ columnHelper.accessor("firstname", {
138
+ // Here, because there is no "FilterInDrawer" component, there will be nothing in the filter drawer related to the firstname
139
+ // But the firstname can still be filtered by the global filter (search input)
140
+ header: ({ header }) => (
141
+ <DataTableHeader
142
+ onSortClick={header.column.getToggleSortingHandler()}
143
+ sortDirection={header.column.getIsSorted() || "none"}
144
+ text="Firstname"
145
+ />
146
+ ),
147
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
148
+ }),
149
+ // Here is an example using a "FilterInDrawer" component that render a radio group to filter the day
150
+ // Please note the "filterFn" is "arrIncludes" for this king of value (Array<string>)
151
+ // Also note the usage of `satisfies TypeTableColumnWithFilter<User>` to help you type check the column definition (because meta is a custom property)
152
+ // Finally, you will note that the Chips are auto-generated, if you do not want to use them you can set the `skipFilterChips` to true
153
+ columnHelper.accessor("day", {
154
+ id: "Day",
155
+ header: () => <DataTableHeader text="Day" />,
156
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
157
+ filterFn: "equals",
158
+ meta: {
159
+ FilterInDrawer: ({ column }) => (
160
+ <div>
161
+ <b>Filter by day:</b>
162
+
163
+ {DAYS.map((day) => (
164
+ <Radio
165
+ label={day}
166
+ key={day}
167
+ checked={column.getFilterValue() === day}
168
+ onChange={() => column.setFilterValue(day)}
169
+ />
170
+ ))}
171
+ </div>
172
+ ),
173
+ } satisfies TypeTableColumnWithFilter<User>,
174
+ }),
175
+ // Here is another example using a "FilterInDrawer" component that render a checkbox group to filter the food
176
+ // Please note the "filterFn" is "arrIncludesSome" -> we want to include all entries that match at least one food
177
+ // Also note the usage of `satisfies TypeTableColumnWithFilter<User>` to help you type check the column definition (because meta is a custom property)
178
+ // Finally, you will note that the Chips are auto-generated, if you do not want to use them you can set the `skipFilterChips` to true
179
+ columnHelper.accessor("foods", {
180
+ id: "Foods",
181
+ header: () => <DataTableHeader text="Foods" />,
182
+ cell: (props) => (
183
+ <DataTableCell>{props.getValue()?.join(", ")}</DataTableCell>
184
+ ),
185
+ filterFn: "arrIncludesSome",
186
+ meta: {
187
+ FilterInDrawer: ({ column }) => (
188
+ <div style={{ marginTop: 8 }}>
189
+ <b>Filter by food (at least one):</b>
190
+ <CheckboxGroup
191
+ value={(column.getFilterValue() as any) || []}
192
+ onChange={column.setFilterValue}
193
+ aria-label="Filter by food (at least one)"
194
+ >
195
+ {ALL_FOODS.map((food) => (
196
+ <div key={food}>
197
+ <Checkbox label={food} value={food} />
198
+ </div>
199
+ ))}
200
+ </CheckboxGroup>
201
+ </div>
202
+ ),
203
+ } satisfies TypeTableColumnWithFilter<User>,
204
+ }),
205
+ // Here is another example using a "FilterInDrawer" component that render a select to filter the organization
206
+ // Please note the "filterFn" is a custom function that will filter the data based on the organization id
207
+ // Also note the usage of `satisfies TypeTableColumnWithFilter<User>` to help you type check the column definition (because meta is a custom property)
208
+ // And most important here, we have a custom meta property `filterChips` that will generate a chip to display the selected organization
209
+ // The Filter can not handle automatic chips when the data is a complex object, so you have to handle that manually
210
+ columnHelper.accessor("organization", {
211
+ id: "Organization",
212
+ header: () => <DataTableHeader text="Organization" />,
213
+ cell: (props) => (
214
+ <DataTableCell>{props.getValue()?.name}</DataTableCell>
215
+ ),
216
+ filterFn: (row, _, filterValue) => {
217
+ if (!filterValue) return true;
218
+ return row.original.organization?.id === filterValue;
219
+ },
220
+ meta: {
221
+ filterChips: ({ column }) => {
222
+ const value = column.getFilterValue();
223
+ const organization = ORGANIZATIONS.find(
224
+ (org) => org.id === value,
225
+ );
226
+ if (!organization) return [];
227
+ return [
228
+ {
229
+ value: `Organization: ${organization.name}`,
230
+ onClose: () => column.setFilterValue(null),
231
+ },
232
+ ];
233
+ },
234
+ FilterInDrawer: ({ column }) => (
235
+ <Select
236
+ id="organization-select"
237
+ label="Filter by organization:"
238
+ mode="single"
239
+ value={(column.getFilterValue() as any) || null}
240
+ onChange={column.setFilterValue}
241
+ fullWidth
242
+ wrapperProps={{ style: { marginBottom: 8 } }}
243
+ >
244
+ {ORGANIZATIONS.map((organization) => (
245
+ <SelectItem
246
+ text={organization.name}
247
+ id={organization.id}
248
+ key={organization.id}
249
+ />
250
+ ))}
251
+ </Select>
252
+ ),
253
+ } satisfies TypeTableColumnWithFilter<User>,
254
+ }),
255
+ // Here is an example that uses a filter outside of the drawer.
256
+ // See the "extraLeftContent" prop to see how to connect it to react-table filtering features
257
+ columnHelper.accessor("isActive", {
258
+ header: () => <DataTableHeader text="Active" />,
259
+ cell: (props) => (
260
+ <DataTableCell>{props.getValue() ? "✅" : "❌"}</DataTableCell>
261
+ ),
262
+ filterFn: (row, _, filterValue) => {
263
+ if (!filterValue) return true;
264
+ return !!row.original.isActive;
265
+ },
266
+ }),
267
+ // Here is an example of an empty column, for example to display an actions bar
268
+ // You can note that the header is empty, the text is optional
269
+ columnHelper.display({
270
+ id: "actions",
271
+ header: () => <DataTableHeader />,
272
+ cell: () => (
273
+ <DataTableCell>
274
+ <Button mode="secondary" iconLeft="edit" />
275
+ </DataTableCell>
276
+ ),
277
+ }),
278
+ ],
279
+ [ALL_FOODS, DAYS, ORGANIZATIONS, columnHelper],
280
+ );
281
+
282
+ // And here we define the table as usual
283
+ const table = useReactTable({
284
+ data,
285
+ columns,
286
+ initialState: {
287
+ globalFilter: "", // Add this to show the global filter (aka the Search bar)
288
+ pagination: {
289
+ pageSize: 12, // Add this to customize the page size
290
+ },
291
+ },
292
+ getCoreRowModel: getCoreRowModel(),
293
+ getSortedRowModel: getSortedRowModel(),
294
+ getFilteredRowModel: getFilteredRowModel(),
295
+ getPaginationRowModel: getPaginationRowModel(),
296
+ });
297
+
298
+ // Get the pagination state
299
+ const pagination = table.getState().pagination;
300
+
301
+ // Render the table using the base DataTable component
302
+ return (
303
+ <>
304
+ <Filter<User>
305
+ onDownloadClick={() => alert("Download")}
306
+ onSettingsClick={() => alert("Settings")}
307
+ table={table}
308
+ extraLeftContent={
309
+ <>
310
+ <Switch
311
+ label="Only active"
312
+ isSelected={!!table.getColumn("isActive")?.getFilterValue()}
313
+ onChange={() =>
314
+ table
315
+ .getColumn("isActive")
316
+ ?.setFilterValue((prevValue: boolean) => !prevValue)
317
+ }
318
+ />
319
+ </>
320
+ }
321
+ extraRightContent={
322
+ <>
323
+ <Button
324
+ mode="secondary"
325
+ onClick={() => alert("Secondary")}
326
+ text="Secondary"
327
+ />
328
+ <Button
329
+ mode="primary"
330
+ onClick={() => alert("Primary")}
331
+ text="Primary"
332
+ />
333
+ </>
334
+ }
335
+ />
336
+ <DataTable
337
+ table={table}
338
+ pagination={pagination}
339
+ itemsPerPageOptions={[12, 24, 36, 48, 60]} // Add this to customize the page size options available in the select
340
+ />
341
+ </>
342
+ );
343
+ };
344
+
345
+ return <ParentComponent />;
346
+ },
347
+ };
348
+
349
+ type User = {
350
+ firstname: string;
351
+ lastname: string;
352
+ email: string;
353
+ isValidated?: boolean;
354
+ day?: string;
355
+ foods?: Array<string>;
356
+ organization?: { id: string; name: string };
357
+ isActive?: boolean;
358
+ };
359
+
360
+ // Just a fake API to simulate a backend
361
+ async function api(params: {
362
+ page: number;
363
+ pageSize: number;
364
+ sorting: ColumnSort;
365
+ emails?: Array<string>;
366
+ }) {
367
+ await someTime(100);
368
+ const { page, pageSize, sorting, emails } = params;
369
+
370
+ let data: Array<User> = arrayOfLength(300).map((index) => ({
371
+ firstname: `Firstname ${index + 1}`,
372
+ lastname: `Lastname ${index + 1}`,
373
+ email: `john-doe${index + 1}@example.com`,
374
+ }));
375
+
376
+ if (emails && emails.length > 0) {
377
+ data = data.filter((user) => emails.includes(user.email));
378
+ }
379
+
380
+ if (sorting) {
381
+ const key = sorting.id;
382
+
383
+ data.sort((a: any, b: any) => {
384
+ if (sorting.desc) {
385
+ return b[key].localeCompare(a[key], undefined, { numeric: true });
386
+ } else {
387
+ a[key].localeCompare(b[key], undefined, { numeric: true });
388
+ }
389
+ });
390
+ }
391
+
392
+ return {
393
+ data: data.slice(page * pageSize, (page + 1) * pageSize),
394
+ total: data.length,
395
+ };
396
+ }
397
+
398
+ export const WithReactTableBackEndManaged: StoryObj<typeof DataTable> = {
399
+ parameters: {
400
+ chromatic: { disableSnapshot: true },
401
+ layout: "padded",
402
+ },
403
+ globals: {
404
+ backgrounds: { value: "light" },
405
+ },
406
+ render: () => {
407
+ function UsersTable() {
408
+ // Create state for the pagination
409
+ const [pagination, setPagination] = useState({
410
+ pageIndex: 0, //initial page index
411
+ pageSize: 10, //default page size
412
+ });
413
+
414
+ // Create state for the sorting (with an initial state)
415
+ const [sorting, setSorting] = useState<SortingState>([
416
+ { id: "email", desc: false },
417
+ ]);
418
+
419
+ // Create state for the filters
420
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(
421
+ [],
422
+ );
423
+
424
+ // Fetch data using the table state (filters, pagination, sorting, etc.)
425
+ const { data } = useQuery({
426
+ placeholderData: keepPreviousData, // from @tanstack/react-query, recommended to use it in this scenario
427
+ queryKey: ["users", pagination, sorting, columnFilters],
428
+ queryFn: () =>
429
+ // You API call goes here, you have access to the table state to build your query params
430
+ api({
431
+ page: pagination.pageIndex,
432
+ pageSize: pagination.pageSize,
433
+ sorting: sorting[0], // handle only one sorting for this example
434
+ emails: columnFilters.find((filter) => filter.id === "email")
435
+ ?.value as Array<string>,
436
+ }),
437
+ });
438
+
439
+ // You will need to create a column helper
440
+ // ⚠ This should be memoized!
441
+ const columnHelper = useMemo(() => createColumnHelper<User>(), []);
442
+
443
+ // Then, you can define the columns
444
+ // ⚠ This should be memoized!
445
+ const columns = useMemo(
446
+ () => [
447
+ // Example of a custom column for initials (no data model, so no filters, etc)
448
+ columnHelper.display({
449
+ id: "initials",
450
+ header: () => <DataTableHeader text="Initials" />,
451
+ cell: (props) => (
452
+ <DataTableCell>
453
+ {/*Here is an example of how you can access the data model values directly (if needed) */}
454
+ {props.row.original.firstname[0]}
455
+ {props.row.original.lastname[0]}
456
+ </DataTableCell>
457
+ ),
458
+ }),
459
+ // Example of a column with sorting (accessor means it has data model)
460
+ columnHelper.accessor("firstname", {
461
+ header: ({ header }) => (
462
+ <DataTableHeader
463
+ onSortClick={header.column.getToggleSortingHandler()}
464
+ sortDirection={header.column.getIsSorted() || "none"}
465
+ text="Firstname"
466
+ />
467
+ ),
468
+ cell: (props) => (
469
+ <DataTableCell iconLeft="star_outline">
470
+ {/* because the accessor key ("firstname") is a data model object key, we can access it directly with getValue() */}
471
+ {props.getValue()}
472
+ </DataTableCell>
473
+ ),
474
+ }),
475
+ columnHelper.accessor("lastname", {
476
+ header: ({ header }) => (
477
+ <DataTableHeader
478
+ onSortClick={header.column.getToggleSortingHandler()}
479
+ sortDirection={header.column.getIsSorted() || "none"}
480
+ text="Lastname"
481
+ />
482
+ ),
483
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
484
+ }),
485
+ columnHelper.accessor("email", {
486
+ header: ({ header }) => (
487
+ <DataTableHeader
488
+ onSortClick={header.column.getToggleSortingHandler()}
489
+ sortDirection={header.column.getIsSorted() || "none"}
490
+ text="Email"
491
+ iconLeft="send"
492
+ />
493
+ ),
494
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
495
+ meta: {
496
+ FilterInDrawer: ({ column }) => (
497
+ <Select
498
+ id="email-select"
499
+ label="Filter by email:"
500
+ mode="multiple"
501
+ selectedKeys={(column.getFilterValue() as any) || []}
502
+ onSelectionChange={column.setFilterValue}
503
+ fullWidth
504
+ wrapperProps={{ style: { marginBottom: 8 } }}
505
+ >
506
+ {arrayOfLength(10).map((index) => (
507
+ <SelectItem
508
+ text={`john-doe${index + 1}@example.com`}
509
+ id={`john-doe${index + 1}@example.com`}
510
+ key={`john-doe${index + 1}@example.com`}
511
+ />
512
+ ))}
513
+ </Select>
514
+ ),
515
+ } satisfies TypeTableColumnWithFilter<User>,
516
+ }),
517
+ ],
518
+ [columnHelper],
519
+ );
520
+
521
+ // Instantiate the table with @tanstack/react-table
522
+ const table = useReactTable({
523
+ data: data?.data || [],
524
+ columns,
525
+ getCoreRowModel: getCoreRowModel(), // from @tanstack/react-table
526
+ manualPagination: true,
527
+ manualSorting: true,
528
+ manualFiltering: true,
529
+ onPaginationChange: setPagination, //update the pagination state when internal APIs mutate the pagination state
530
+ onSortingChange: setSorting, //update the sorting state when internal APIs mutate the sorting state
531
+ onColumnFiltersChange: setColumnFilters, //update the column filters state when internal APIs mutate the column filters state
532
+ rowCount: data?.total, //total number of rows
533
+ state: {
534
+ pagination,
535
+ sorting,
536
+ columnFilters,
537
+ },
538
+ });
539
+
540
+ // Render the table using the base DataTable component
541
+ return (
542
+ <>
543
+ <Filter<User> table={table} />
544
+ <DataTable table={table} pagination={pagination} />
545
+ </>
546
+ );
547
+ }
548
+
549
+ return <UsersTable />;
550
+ },
551
+ };
552
+
553
+ export const Empty: StoryObj<typeof DataTable> = {
554
+ render: () => {
555
+ const ParentComponent = () => {
556
+ const columnHelper = useMemo(() => createColumnHelper<User>(), []);
557
+
558
+ const data = useMemo(() => [], []);
559
+
560
+ const columns = useMemo(
561
+ () => [
562
+ columnHelper.display({
563
+ id: "example",
564
+ header: () => <DataTableHeader text="Example" />,
565
+ cell: () => <DataTableCell>Example</DataTableCell>,
566
+ }),
567
+ ],
568
+ [columnHelper],
569
+ );
570
+
571
+ const table = useReactTable({
572
+ data,
573
+ columns,
574
+ initialState: {
575
+ globalFilter: "",
576
+ },
577
+ getCoreRowModel: getCoreRowModel(),
578
+ getSortedRowModel: getSortedRowModel(),
579
+ getFilteredRowModel: getFilteredRowModel(),
580
+ getPaginationRowModel: getPaginationRowModel(),
581
+ });
582
+
583
+ const pagination = table.getState().pagination;
584
+
585
+ return (
586
+ <>
587
+ <Filter<User>
588
+ onDownloadClick={() => alert("Download")}
589
+ onSettingsClick={() => alert("Settings")}
590
+ table={table}
591
+ />
592
+ <DataTable table={table} pagination={pagination} />
593
+ </>
594
+ );
595
+ };
596
+
597
+ return <ParentComponent />;
598
+ },
599
+ play: async ({ canvasElement }) => {
600
+ const canvas = within(canvasElement);
601
+ await canvas.findByText("Sorry, there is currently no data to display.");
602
+ },
603
+ };
604
+
605
+ export const WithSubColumns: StoryObj<typeof DataTable> = {
606
+ render: () => {
607
+ const data: Array<User> = arrayOfLength(300).map((index) => ({
608
+ firstname: `Firstname ${index + 1}`,
609
+ lastname: `Lastname ${index + 1}`,
610
+ email: `john-doe${index + 1}@example.com`,
611
+ }));
612
+
613
+ const ParentComponent = () => {
614
+ const columnHelper = useMemo(() => createColumnHelper<User>(), []);
615
+
616
+ const columns = useMemo(
617
+ () => [
618
+ columnHelper.group({
619
+ id: "name",
620
+ header: (group) => (
621
+ <DataTableHeader
622
+ text="Name group"
623
+ colSpan={group.header.colSpan}
624
+ />
625
+ ),
626
+ columns: [
627
+ columnHelper.accessor("firstname", {
628
+ id: "Firstname",
629
+ header: () => <DataTableHeader text="Firstname" />,
630
+ cell: (props) => (
631
+ <DataTableCell>{props.getValue()}</DataTableCell>
632
+ ),
633
+ }),
634
+ columnHelper.accessor("lastname", {
635
+ id: "Lastname",
636
+ header: () => <DataTableHeader text="Lastname" />,
637
+ cell: (props) => (
638
+ <DataTableCell>{props.getValue()}</DataTableCell>
639
+ ),
640
+ }),
641
+ ],
642
+ }),
643
+ columnHelper.group({
644
+ id: "other",
645
+ header: (group) => (
646
+ <DataTableHeader
647
+ text="Other group"
648
+ colSpan={group.header.colSpan}
649
+ border="left"
650
+ />
651
+ ),
652
+ columns: [
653
+ columnHelper.accessor("email", {
654
+ id: "Email",
655
+ header: () => <DataTableHeader border="left" text="Email" />,
656
+ cell: (props) => (
657
+ <DataTableCell border="left">
658
+ {props.getValue()}
659
+ </DataTableCell>
660
+ ),
661
+ }),
662
+ ],
663
+ }),
664
+ ],
665
+ [columnHelper],
666
+ );
667
+
668
+ const table = useReactTable({
669
+ data,
670
+ columns,
671
+ initialState: {
672
+ globalFilter: "",
673
+ },
674
+ getCoreRowModel: getCoreRowModel(),
675
+ getSortedRowModel: getSortedRowModel(),
676
+ getFilteredRowModel: getFilteredRowModel(),
677
+ getPaginationRowModel: getPaginationRowModel(),
678
+ });
679
+
680
+ const pagination = table.getState().pagination;
681
+
682
+ return (
683
+ <>
684
+ <Filter<User> table={table} />
685
+ <DataTable table={table} pagination={pagination} />
686
+ </>
687
+ );
688
+ };
689
+
690
+ return <ParentComponent />;
691
+ },
692
+ };
693
+
694
+ export const WithContentOverflow: StoryObj<typeof DataTable> = {
695
+ render: () => {
696
+ const data: Array<any> = arrayOfLength(300).map((index) => ({
697
+ firstname: `Firstnameverylongthatwilloverflowonthescreensqhjdhqskdhkqshdkhqkdshhqsdhkqhshqhdskqjshdjhqsjkhdkqhskjdhqjkshdqkshdkhqsdkjqhdsjkhqsdk ${index + 1}`,
698
+ }));
699
+
700
+ const ParentComponent = () => {
701
+ const columnHelper = useMemo(() => createColumnHelper<any>(), []);
702
+
703
+ const columns = useMemo(
704
+ () => [
705
+ columnHelper.accessor("firstname", {
706
+ id: "Firstname",
707
+ header: () => <DataTableHeader text="Firstname" />,
708
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
709
+ }),
710
+ ],
711
+ [columnHelper],
712
+ );
713
+
714
+ const table = useReactTable({
715
+ data,
716
+ columns,
717
+ initialState: {
718
+ globalFilter: "",
719
+ },
720
+ getCoreRowModel: getCoreRowModel(),
721
+ getSortedRowModel: getSortedRowModel(),
722
+ getFilteredRowModel: getFilteredRowModel(),
723
+ getPaginationRowModel: getPaginationRowModel(),
724
+ });
725
+
726
+ const pagination = table.getState().pagination;
727
+
728
+ return (
729
+ <>
730
+ <Filter<User> table={table} />
731
+ <DataTable table={table} pagination={pagination} />
732
+ </>
733
+ );
734
+ };
735
+
736
+ return <ParentComponent />;
737
+ },
738
+ decorators: [
739
+ (Story) => (
740
+ <div
741
+ style={{ width: "100%", maxWidth: "700px", border: "1px solid red" }}
742
+ >
743
+ <Story />
744
+ </div>
745
+ ),
746
+ ],
747
+ };
748
+
749
+ export const CustomExpandUi: StoryObj<typeof DataTable> = {
750
+ render: () => {
751
+ const ParentComponent = () => {
752
+ const columnHelper = useMemo(() => createColumnHelper<User>(), []);
753
+
754
+ // Fake data, should come from an API typically
755
+ const data = useMemo(
756
+ () => [
757
+ { firstname: "John", lastname: "Doe", email: "john-doe@example.com" },
758
+ { firstname: "Jane", lastname: "Doe", email: "jane-doe@example.com" },
759
+ ],
760
+ [],
761
+ );
762
+
763
+ const columns = useMemo(
764
+ () => [
765
+ columnHelper.accessor("firstname", {
766
+ header: () => <DataTableHeader text="Firstname" />,
767
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
768
+ }),
769
+ columnHelper.display({
770
+ id: "expander",
771
+ header: () => <DataTableHeader text="" />,
772
+ cell: ({ row }) => (
773
+ <DataTableCell style={{ width: 72 }}>
774
+ <Button
775
+ mode="tertiary"
776
+ iconLeft={row.getIsExpanded() ? "expand_less" : "expand_more"} // Show a different icon when the row is expanded
777
+ onClick={row.getToggleExpandedHandler()} // Here you plug your button with react-table expanded handler
778
+ />
779
+ </DataTableCell>
780
+ ),
781
+ }),
782
+ ],
783
+ [columnHelper],
784
+ );
785
+
786
+ const table = useReactTable({
787
+ data,
788
+ columns,
789
+ getRowCanExpand: () => true, // tell the table that all rows can be expanded
790
+ getCoreRowModel: getCoreRowModel(),
791
+ getSortedRowModel: getSortedRowModel(),
792
+ getExpandedRowModel: getExpandedRowModel(), // from @tanstack/react-table
793
+ });
794
+
795
+ return (
796
+ <>
797
+ <DataTable
798
+ table={table}
799
+ // Here you can tell the table how to render an expanded row
800
+ renderSubComponent={({ row }) => (
801
+ <pre>
802
+ <code>{JSON.stringify(row.original, null, 2)}</code>
803
+ </pre>
804
+ )}
805
+ />
806
+ </>
807
+ );
808
+ };
809
+
810
+ return <ParentComponent />;
811
+ },
812
+ };
813
+
814
+ export const ExpandingSubRows: StoryObj<typeof DataTable> = {
815
+ render: () => {
816
+ type UserWithAffiliates = User & { affiliates?: Array<UserWithAffiliates> };
817
+
818
+ const ParentComponent = () => {
819
+ const columnHelper = useMemo(
820
+ () => createColumnHelper<UserWithAffiliates>(),
821
+ [],
822
+ );
823
+
824
+ // Fake data, should come from an API typically
825
+ const data = useMemo(
826
+ () => [
827
+ {
828
+ firstname: "John",
829
+ lastname: "Doe",
830
+ email: "john-doe@example.com",
831
+ affiliates: [
832
+ {
833
+ firstname: "Marcel",
834
+ lastname: "Potter",
835
+ email: "marcel@example.com",
836
+ },
837
+ {
838
+ firstname: "Willy",
839
+ lastname: "Potter",
840
+ email: "willy@example.com",
841
+ },
842
+ {
843
+ firstname: "Michele",
844
+ lastname: "Potter",
845
+ email: "michele@example.com",
846
+ },
847
+ ],
848
+ },
849
+ {
850
+ firstname: "Jane",
851
+ lastname: "Doe",
852
+ email: "jane-doe@example.com",
853
+ affiliates: [
854
+ {
855
+ firstname: "Marcel",
856
+ lastname: "Potter",
857
+ email: "marcel@example.com",
858
+ },
859
+ ],
860
+ },
861
+ ],
862
+ [],
863
+ );
864
+
865
+ const columns = useMemo(
866
+ () => [
867
+ columnHelper.accessor("firstname", {
868
+ header: () => <DataTableHeader text="Firstname" />,
869
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
870
+ }),
871
+ columnHelper.accessor("lastname", {
872
+ id: "Lastname",
873
+ header: () => <DataTableHeader text="Lastname" />,
874
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
875
+ }),
876
+ columnHelper.display({
877
+ id: "expander",
878
+ header: () => <DataTableHeader text="" />,
879
+ cell: ({ row }) => (
880
+ <DataTableCell style={{ width: 72 }}>
881
+ {row.getCanExpand() && (
882
+ <Button
883
+ mode="tertiary"
884
+ iconLeft={
885
+ // Show a different icon when the row is expanded
886
+ row.getIsExpanded() ? "expand_less" : "expand_more"
887
+ }
888
+ onClick={row.getToggleExpandedHandler()} // Here you plug your button with react-table expanded handler
889
+ />
890
+ )}
891
+ </DataTableCell>
892
+ ),
893
+ }),
894
+ ],
895
+ [columnHelper],
896
+ );
897
+
898
+ const table = useReactTable({
899
+ data,
900
+ columns,
901
+ getSubRows: (row) => row.affiliates,
902
+ getCoreRowModel: getCoreRowModel(),
903
+ getSortedRowModel: getSortedRowModel(),
904
+ getExpandedRowModel: getExpandedRowModel(), // from @tanstack/react-table
905
+ });
906
+
907
+ return <DataTable table={table} />;
908
+ };
909
+
910
+ return <ParentComponent />;
911
+ },
912
+ };
913
+
914
+ export const WithoutReactTable: StoryObj<typeof DataTable> = {
915
+ render: () => {
916
+ const data = [
917
+ { firstname: "John", lastname: "Doe", email: "john-doe@example.com" },
918
+ { firstname: "Jane", lastname: "Doe", email: "jane-doe@example.com" },
919
+ { firstname: "Jim", lastname: "Beam", email: "jim-beam@example.com" },
920
+ ];
921
+
922
+ return (
923
+ <DataTableRoot>
924
+ <thead>
925
+ <tr>
926
+ <DataTableHeader text="Firstname" />
927
+ <DataTableHeader text="Lastname" />
928
+ <DataTableHeader text="Email" />
929
+ </tr>
930
+ </thead>
931
+ <tbody>
932
+ {data.map((item) => (
933
+ <DataTableRow key={item.email}>
934
+ <DataTableCell>{item.firstname}</DataTableCell>
935
+ <DataTableCell>{item.lastname}</DataTableCell>
936
+ <DataTableCell>{item.email}</DataTableCell>
937
+ </DataTableRow>
938
+ ))}
939
+ </tbody>
940
+ </DataTableRoot>
941
+ );
942
+ },
943
+ };
944
+
945
+ export const Sizes: StoryObj<typeof DataTable> = {
946
+ render: () => {
947
+ const ParentComponent = () => {
948
+ const data: Array<User> = useMemo(
949
+ () =>
950
+ arrayOfLength(5).map((index) => ({
951
+ firstname: `Firstname ${index + 1}`,
952
+ lastname: `Lastname ${index + 1}`,
953
+ email: `john-doe${index + 1}@example.com`,
954
+ })),
955
+ [],
956
+ );
957
+
958
+ const columnHelper = useMemo(() => createColumnHelper<User>(), []);
959
+
960
+ const columns = useMemo(
961
+ () => [
962
+ columnHelper.accessor("firstname", {
963
+ header: () => <DataTableHeader text="Firstname" />,
964
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
965
+ }),
966
+ columnHelper.accessor("lastname", {
967
+ header: () => <DataTableHeader text="Lastname" />,
968
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
969
+ }),
970
+ columnHelper.accessor("email", {
971
+ header: () => <DataTableHeader text="Email" />,
972
+ cell: (props) => <DataTableCell>{props.getValue()}</DataTableCell>,
973
+ }),
974
+ ],
975
+ [columnHelper],
976
+ );
977
+
978
+ const TableWithSize = ({
979
+ size,
980
+ }: {
981
+ size: "small" | "default" | "large";
982
+ }) => {
983
+ const table = useReactTable({
984
+ data,
985
+ columns,
986
+ getCoreRowModel: getCoreRowModel(),
987
+ });
988
+
989
+ return (
990
+ <div style={{ marginBottom: 32 }}>
991
+ <h3 style={{ marginBottom: 8 }}>Size: {size}</h3>
992
+ <DataTable table={table} size={size} />
993
+ </div>
994
+ );
995
+ };
996
+
997
+ return (
998
+ <>
999
+ <TableWithSize size="small" />
1000
+ <TableWithSize size="default" />
1001
+ <TableWithSize size="large" />
1002
+ </>
1003
+ );
1004
+ };
1005
+
1006
+ return <ParentComponent />;
1007
+ },
1008
+ };
1009
+ ```
1010
+
1011
+ ## Developer notes
1012
+
1013
+ 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.
1014
+
1015
+ ```mdx
1016
+ import { Meta, DocsStory } from "@storybook/addon-docs/blocks";
1017
+
1018
+ import * as DataTable from "./DataTable.stories";
1019
+ import * as DataTableRoot from "./DataTableRoot/DataTableRoot.stories";
1020
+
1021
+ <Meta of={DataTable} />
1022
+
1023
+ # DataTable
1024
+
1025
+ ## Philosophy and usage
1026
+
1027
+ Creating a table consists mainly of two parts:
1028
+
1029
+ - Creating the UI to display the data
1030
+ - Handling the data manipulation (sorting, filtering, paginating, etc.)
1031
+
1032
+ The Design System will only provide the UI part, and **you will have to handle the data manipulation yourself**.
1033
+
1034
+ But because this is not trivial at all, we encourage you to use a third-party library.
1035
+ Our recommendation is to use the excellent headless library [react-table](https://tanstack.com/table/latest).
1036
+ And because we know your time is precious, **we provide you with a good starting point to create your custom abstraction using `react-table` that you can copy-paste in your project**.
1037
+
1038
+ Feel free to make some adaptations when needed, and to create your own abstraction above this UI component that fits your project's specific constraints and patterns.
1039
+ All those examples are here to help you get started, they are not meant to be a one-size-fits-all solution.
1040
+
1041
+ ## Creating the UI
1042
+
1043
+ You will need quite a few components to create a table:
1044
+
1045
+ - [DataTableRoot](?path=/docs/components-datatable-datatableroot--docs): very basic root to give base styles to your table
1046
+ - [DataTableHeader](?path=/docs/components-datatable-datatableheader--docs): to display the header of your table
1047
+ - [DataTableRow](?path=/docs/components-datatable-datatablerow--docs): to display a row of data
1048
+ - [DataTableCell](?path=/docs/components-datatable-datatablecell--docs): to display a cell of data
1049
+ - [Pagination](?path=/docs/components-pagination--docs): to handle the pagination
1050
+ - [Filter](?path=/docs/components-filter--docs): to handle the filtering
1051
+
1052
+ If you do not have specific needs, **you can use the `DataTable` component**, which is a composition of the above components.
1053
+ This way, you can start creating your table quickly.
1054
+
1055
+ But if one day you need to add a specific feature, you can easily replace the `DataTable` component with your own composition (feel free to copy paste the code of the `DataTable` as a starting point).
1056
+
1057
+ ## Example Integration with react-table
1058
+
1059
+ When manipulating data, there is basically two ways to handle it:
1060
+
1061
+ - Front-end managed: the data is fetched once and all the data manipulation is done on the front-end
1062
+ - Back-end managed: the data is fetched on demand, and the data manipulation is done on the back-end
1063
+
1064
+ Most of the time, just go with front-end managed data, it's easier to implement, and it's faster for the user in most cases.
1065
+
1066
+ The decision to switch to back-end managed data should be made when there are performance issues.
1067
+ Should it be from a too intensive load for the database, a payload so big that the network request becomes very slow, or any other reason.
1068
+
1069
+ Below are two examples of how to integrate the `DataTable` component with `react-table` in both cases.
1070
+
1071
+ ### Front-end managed
1072
+
1073
+ When the data is small, it is better to handle the data manipulation on the front-end.
1074
+
1075
+ Here is a good starting point for creating a front-end managed table, using `react-table`, ready to copy-paste ! (click on "Show code").
1076
+
1077
+ <DocsStory of={DataTable.WithReactTableFrontEndManaged} />
1078
+
1079
+ ### Back-end managed
1080
+
1081
+ Letting the back-end handle the data manipulation requires a bit more work, you will need to handle some parts of the table state manually to properly send the proper parameters to your back-end (like the current page, the sorting, the filtering, etc.).
1082
+
1083
+ Here is a good starting point for creating a back-end managed table, using `react-table`, ready to copy-paste ! (click on "Show code").
1084
+
1085
+ <DocsStory of={DataTable.WithReactTableBackEndManaged} />
1086
+
1087
+ ## With sub columns
1088
+
1089
+ Here is an example of how to create a table with sub columns, **click on "Show code"**.
1090
+
1091
+ Find more explanations on this in the [TanStack Table documentation](https://tanstack.com/table/latest/docs/guide/column-defs) (in the "Grouping" section).
1092
+
1093
+ <DocsStory of={DataTable.WithSubColumns} />
1094
+
1095
+ ## With custom UI to expand in a row
1096
+
1097
+ Here is an example of how to create a table with a custom UI to expand in a row, **click on "Show code"**.
1098
+
1099
+ Find more explanations on this in the [TanStack Table documentation and examples](https://tanstack.com/table/latest/docs/framework/react/examples/sub-components) ("Sub Components" example, and also the "Expanding guide").
1100
+
1101
+ <DocsStory of={DataTable.CustomExpandUi} />
1102
+
1103
+ ## With sub rows
1104
+
1105
+ Here is an example of how to create a table with sub rows, **click on "Show code"**.
1106
+
1107
+ You can have any number of sub row depth, and the table will handle the UI for you (the data schema should be recursive).
1108
+
1109
+ Find more explanations on this in the [TanStack Table documentation and examples](https://tanstack.com/table/v8/docs/framework/react/examples/expanding) ("Expanding" example, and also the "Expanding guide" docs).
1110
+
1111
+ <DocsStory of={DataTable.ExpandingSubRows} />
1112
+
1113
+ ## Available components in Cells
1114
+
1115
+ Our approach to this question is to be flexible.
1116
+
1117
+ We do not want to limit you to a set of predefined components to use in your cells, nor do we expose you with tons of props to handle every possible combination of components.
1118
+
1119
+ The only recommandation we have is **to only use one component per cell**, ideally (though it's not forbidden).
1120
+
1121
+ Here are some examples of components you can use in your cells, click on "Show code" to see how it's done.
1122
+
1123
+ <DocsStory of={DataTableRoot.WithSpecificComponents} />
1124
+ ```