@czi-sds/mcp 0.16.0 → 0.17.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 (164) hide show
  1. package/README.md +2 -1
  2. package/data/component-docs/Accordion.md +291 -0
  3. package/data/component-docs/Autocomplete.md +525 -0
  4. package/data/component-docs/Banner.md +231 -0
  5. package/data/component-docs/Button.md +224 -0
  6. package/data/component-docs/ButtonDropdown.md +256 -0
  7. package/data/component-docs/ButtonGroup.md +320 -0
  8. package/data/component-docs/ButtonToggle.md +397 -0
  9. package/data/component-docs/Callout.md +268 -0
  10. package/data/component-docs/CellBasic.md +122 -0
  11. package/data/component-docs/CellComponent.md +63 -0
  12. package/data/component-docs/CellHeader.md +128 -0
  13. package/data/component-docs/ComplexFilter.md +294 -0
  14. package/data/component-docs/ContentCard.md +406 -0
  15. package/data/component-docs/Dialog.md +651 -0
  16. package/data/component-docs/Dropdown.md +155 -0
  17. package/data/component-docs/DropdownMenu.md +949 -0
  18. package/data/component-docs/HeatmapChart.md +782 -0
  19. package/data/component-docs/Hero.md +363 -0
  20. package/data/component-docs/Icon.md +283 -0
  21. package/data/component-docs/InputCheckbox.md +205 -0
  22. package/data/component-docs/InputDropdown.md +482 -0
  23. package/data/component-docs/InputRadio.md +173 -0
  24. package/data/component-docs/InputSearch.md +81 -0
  25. package/data/component-docs/InputSlider.md +189 -0
  26. package/data/component-docs/InputText.md +110 -0
  27. package/data/component-docs/InputToggle.md +141 -0
  28. package/data/component-docs/IntentMessage.md +293 -0
  29. package/data/component-docs/Legend.md +442 -0
  30. package/data/component-docs/Link.md +23 -0
  31. package/data/component-docs/List.md +300 -0
  32. package/data/component-docs/LoadingIndicator.md +198 -0
  33. package/data/component-docs/Menu.md +298 -0
  34. package/data/component-docs/MenuItem.md +100 -0
  35. package/data/component-docs/NavigationFooter.md +165 -0
  36. package/data/component-docs/NavigationHeader.md +500 -0
  37. package/data/component-docs/NavigationJumpTo.md +414 -0
  38. package/data/component-docs/Notification.md +288 -0
  39. package/data/component-docs/Pagination.md +67 -0
  40. package/data/component-docs/Panel.md +636 -0
  41. package/data/component-docs/Popover.md +448 -0
  42. package/data/component-docs/PreComposedTable.md +2947 -0
  43. package/data/component-docs/SegmentedControl.md +396 -0
  44. package/data/component-docs/StackedBarChart.md +475 -0
  45. package/data/component-docs/Table.md +2000 -0
  46. package/data/component-docs/TableHeader.md +53 -0
  47. package/data/component-docs/TableRow.md +74 -0
  48. package/data/component-docs/Tabs.md +455 -0
  49. package/data/component-docs/Tag.md +647 -0
  50. package/data/component-docs/TagFilter.md +131 -0
  51. package/data/component-docs/Tooltip.md +561 -0
  52. package/data/component-docs/TooltipCondensed.md +203 -0
  53. package/data/component-docs/TooltipTable.md +145 -0
  54. package/data/component-docs/index.json +262 -0
  55. package/data/component-list.json +2 -5
  56. package/data/component-props/Accordion.json +31 -11
  57. package/data/component-props/Alert.json +3 -3
  58. package/data/component-props/Autocomplete.json +150 -15
  59. package/data/component-props/Banner.json +31 -17
  60. package/data/component-props/Button.json +45 -20
  61. package/data/component-props/ButtonDropdown.json +36 -17
  62. package/data/component-props/ButtonDropdownLegacy.json +11 -11
  63. package/data/component-props/ButtonGroup.json +32 -11
  64. package/data/component-props/ButtonIcon.json +10 -10
  65. package/data/component-props/ButtonLegacy.json +19 -19
  66. package/data/component-props/ButtonToggle.json +32 -19
  67. package/data/component-props/ButtonToggleLegacy.json +11 -11
  68. package/data/component-props/Callout.json +58 -17
  69. package/data/component-props/CellBasic.json +94 -41
  70. package/data/component-props/CellComponent.json +23 -7
  71. package/data/component-props/CellHeader.json +62 -19
  72. package/data/component-props/ComplexFilter.json +52 -21
  73. package/data/component-props/ContentCard.json +70 -47
  74. package/data/component-props/Dialog.json +47 -14
  75. package/data/component-props/Dropdown.json +68 -31
  76. package/data/component-props/DropdownMenu.json +128 -23
  77. package/data/component-props/HeatmapChart.json +88 -12
  78. package/data/component-props/Hero.json +80 -39
  79. package/data/component-props/Icon.json +20 -9
  80. package/data/component-props/InputCheckbox.json +36 -14
  81. package/data/component-props/InputDropdown.json +75 -27
  82. package/data/component-props/InputRadio.json +31 -13
  83. package/data/component-props/InputSearch.json +41 -18
  84. package/data/component-props/InputSlider.json +46 -19
  85. package/data/component-props/InputText.json +26 -19
  86. package/data/component-props/InputToggle.json +26 -9
  87. package/data/component-props/IntentMessage.json +38 -9
  88. package/data/component-props/Link.json +13 -7
  89. package/data/component-props/LoadingIndicator.json +11 -3
  90. package/data/component-props/MenuItem.json +38 -19
  91. package/data/component-props/MenuSelect.json +8 -8
  92. package/data/component-props/NavigationFooter.json +49 -19
  93. package/data/component-props/NavigationHeader.json +150 -37
  94. package/data/component-props/NavigationJumpTo.json +33 -7
  95. package/data/component-props/Notification.json +39 -21
  96. package/data/component-props/Pagination.json +39 -11
  97. package/data/component-props/Panel.json +62 -9
  98. package/data/component-props/Popover.json +80 -7
  99. package/data/component-props/PreComposedTable.json +99 -27
  100. package/data/component-props/SegmentedControl.json +42 -3
  101. package/data/component-props/StackedBarChart.json +116 -42
  102. package/data/component-props/TableRow.json +38 -19
  103. package/data/component-props/Tabs.json +66 -9
  104. package/data/component-props/Tag.json +34 -17
  105. package/data/component-props/TagFilter.json +10 -3
  106. package/data/component-props/Tooltip.json +49 -18
  107. package/data/component-props/TooltipCondensed.json +18 -9
  108. package/data/component-props/TooltipTable.json +17 -7
  109. package/data/resources/{zeroheight-rules.md → documentation-rules.md} +43 -30
  110. package/data/resources/figma-rules.md +2 -2
  111. package/dist/stdio.js +112 -208
  112. package/package.json +9 -5
  113. package/data/component-props/ButtonDropdownV2.json +0 -34
  114. package/data/component-props/ButtonNew.json +0 -47
  115. package/data/component-props/ButtonToggleV2.json +0 -44
  116. package/data/component-props/ButtonV2.json +0 -43
  117. package/data/zeroheight-cache.json +0 -10259
  118. package/data/zeroheight-exports/Accordion.md +0 -774
  119. package/data/zeroheight-exports/Banner.md +0 -691
  120. package/data/zeroheight-exports/Borders.md +0 -117
  121. package/data/zeroheight-exports/Buttons.md +0 -1854
  122. package/data/zeroheight-exports/Callout.md +0 -931
  123. package/data/zeroheight-exports/Colors.md +0 -1755
  124. package/data/zeroheight-exports/Content-Card.md +0 -309
  125. package/data/zeroheight-exports/Contributing-to-SDS.md +0 -98
  126. package/data/zeroheight-exports/Control-Inputs.md +0 -2818
  127. package/data/zeroheight-exports/Corners.md +0 -104
  128. package/data/zeroheight-exports/Cover-page.md +0 -3
  129. package/data/zeroheight-exports/Dialog.md +0 -1367
  130. package/data/zeroheight-exports/Downloads.md +0 -236
  131. package/data/zeroheight-exports/Drop-Shadows.md +0 -79
  132. package/data/zeroheight-exports/Dropdown-Input.md +0 -1776
  133. package/data/zeroheight-exports/Dropdown-Menus.md +0 -913
  134. package/data/zeroheight-exports/Element-Status-Tracker.md +0 -267
  135. package/data/zeroheight-exports/Field-Inputs.md +0 -1405
  136. package/data/zeroheight-exports/Filters.md +0 -1111
  137. package/data/zeroheight-exports/Getting-Started.md +0 -1157
  138. package/data/zeroheight-exports/Hero.md +0 -414
  139. package/data/zeroheight-exports/Icons.md +0 -2067
  140. package/data/zeroheight-exports/Intent.md +0 -86
  141. package/data/zeroheight-exports/Introduction-to-Bases.md +0 -35
  142. package/data/zeroheight-exports/Introduction-to-Cells.md +0 -35
  143. package/data/zeroheight-exports/Introduction-to-DNA.md +0 -35
  144. package/data/zeroheight-exports/Introduction-to-Genes.md +0 -35
  145. package/data/zeroheight-exports/Introduction.md +0 -115
  146. package/data/zeroheight-exports/Link.md +0 -432
  147. package/data/zeroheight-exports/Lists.md +0 -986
  148. package/data/zeroheight-exports/Loading-Indicator.md +0 -41
  149. package/data/zeroheight-exports/Navigation.md +0 -1190
  150. package/data/zeroheight-exports/Notification.md +0 -1038
  151. package/data/zeroheight-exports/Panel.md +0 -1030
  152. package/data/zeroheight-exports/README.md +0 -122
  153. package/data/zeroheight-exports/Responsive-Design.md +0 -330
  154. package/data/zeroheight-exports/Search-Input.md +0 -91
  155. package/data/zeroheight-exports/Search.md +0 -822
  156. package/data/zeroheight-exports/Segmented-Control.md +0 -609
  157. package/data/zeroheight-exports/Spacing.md +0 -297
  158. package/data/zeroheight-exports/Table.md +0 -5345
  159. package/data/zeroheight-exports/Tabs.md +0 -1324
  160. package/data/zeroheight-exports/Tags.md +0 -1545
  161. package/data/zeroheight-exports/Text-Link.md +0 -268
  162. package/data/zeroheight-exports/Theming.md +0 -362
  163. package/data/zeroheight-exports/Tooltips.md +0 -2015
  164. package/data/zeroheight-exports/Typography.md +0 -800
@@ -0,0 +1,2000 @@
1
+ # Table
2
+
3
+ ## Source Code
4
+
5
+ The component's source code in the SDS codebase can be found [here](https://github.com/chanzuckerberg/sci-components/blob/main/packages/components/src/core/Table/index.tsx).
6
+
7
+ ## Import
8
+
9
+ **React TypeScript**
10
+
11
+ ```tsx
12
+ import { Table } from "@czi-sds/components";
13
+ ```
14
+
15
+ ## Code examples
16
+
17
+ ### **Default Table**
18
+
19
+ This example demonstrates a Table that utilizes all SDS Table-related components, providing a comprehensive showcase of their functionalities and features.
20
+
21
+ **Example: DefaultTable**
22
+
23
+ ```tsx
24
+ import {
25
+ CellBasic,
26
+ CellComponent,
27
+ CellHeader,
28
+ Icon,
29
+ InputRadio,
30
+ Tag,
31
+ Table,
32
+ TableRow,
33
+ TableHeader,
34
+ SdsTagColorType,
35
+ } from "@czi-sds/components";
36
+ import { styled, RadioGroup } from "@mui/material";
37
+
38
+ function App() {
39
+ const StyledIconCell = styled("div")`
40
+ align-items: center;
41
+ display: flex;
42
+ flex-direction: column;
43
+ justify-content: center;
44
+ `;
45
+
46
+ return (
47
+ <div className="app">
48
+ <Table>
49
+ <TableHeader>
50
+ <CellHeader horizontalAlign="center" hideSortIcon>
51
+ Category
52
+ </CellHeader>
53
+ {/* hover is what puts the sort chevron in the DOM; without it a header
54
+ is inert and shows no sorting affordance at all. */}
55
+ <CellHeader active hover>
56
+ Active Header
57
+ </CellHeader>
58
+ <CellHeader hover>
59
+ A very long table header title to test sort icon positioning
60
+ </CellHeader>
61
+ <CellHeader hideSortIcon>Component</CellHeader>
62
+ <CellHeader horizontalAlign="right" hideSortIcon>
63
+ Right Aligned and Not sortable
64
+ </CellHeader>
65
+ </TableHeader>
66
+ <tbody>
67
+ <TableRow>
68
+ <CellComponent verticalAlign="center" horizontalAlign="center">
69
+ <StyledIconCell>
70
+ <Icon sdsSize="xl" sdsIcon="Flask" />
71
+ <Tag
72
+ color="info"
73
+ label="Chemistry"
74
+ sdsStyle="rounded"
75
+ sdsType="secondary"
76
+ />
77
+ </StyledIconCell>
78
+ </CellComponent>
79
+ <CellBasic
80
+ primaryText="Primary Text"
81
+ secondaryText="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
82
+ secondaryTextWrapLineCount={2}
83
+ shouldTextWrap
84
+ />
85
+ <CellBasic
86
+ primaryText="Primary Text"
87
+ secondaryText="Secondary Text"
88
+ tertiaryText="Tertiary Text"
89
+ shouldShowTooltipOnHover={false}
90
+ />
91
+ <CellComponent>
92
+ {["info", "notice", "beta", "negative", "positive"].map(
93
+ (item) => (
94
+ <Tag
95
+ key={item}
96
+ color={item as SdsTagColorType}
97
+ label={item as string}
98
+ sdsStyle="rounded"
99
+ sdsType="secondary"
100
+ />
101
+ )
102
+ )}
103
+ </CellComponent>
104
+ <CellBasic
105
+ primaryText="356"
106
+ horizontalAlign="right"
107
+ shouldShowTooltipOnHover={false}
108
+ />
109
+ </TableRow>
110
+ <TableRow>
111
+ <CellComponent verticalAlign="center" horizontalAlign="center">
112
+ <StyledIconCell>
113
+ <Icon sdsSize="xl" sdsIcon="Download" />
114
+ <Tag
115
+ color="info"
116
+ label="Downloadable Content"
117
+ sdsStyle="rounded"
118
+ sdsType="secondary"
119
+ />
120
+ </StyledIconCell>
121
+ </CellComponent>
122
+ <CellBasic
123
+ primaryText="Primary Text"
124
+ secondaryText="Secondary Text"
125
+ tertiaryText="Tertiary Text"
126
+ shouldShowTooltipOnHover={false}
127
+ />
128
+ <CellBasic
129
+ primaryText="Primary Text"
130
+ secondaryText="Secondary Text"
131
+ shouldShowTooltipOnHover={false}
132
+ />
133
+ <CellComponent>
134
+ <RadioGroup
135
+ aria-labelledby="demo-radio-buttons-group-label"
136
+ name="radio-buttons-group"
137
+ defaultValue="1"
138
+ >
139
+ <InputRadio label="Option I" caption="Caption I" value="1" />
140
+ <InputRadio label="Option II" caption="Caption II" value="2" />
141
+ </RadioGroup>
142
+ </CellComponent>
143
+ <CellBasic
144
+ primaryText="1,234"
145
+ secondaryText="2,344,000"
146
+ tertiaryText="12.5%"
147
+ horizontalAlign="right"
148
+ shouldShowTooltipOnHover={false}
149
+ />
150
+ </TableRow>
151
+ <TableRow
152
+ disabled
153
+ shouldShowTooltipOnHover
154
+ tooltipText="This row is DISABLED!"
155
+ tooltipSubtitle="Tooltip subtitle"
156
+ >
157
+ <CellComponent verticalAlign="center" horizontalAlign="center">
158
+ <StyledIconCell>
159
+ <Icon sdsSize="xl" sdsIcon="Bacteria" />
160
+ <Tag
161
+ color="negative"
162
+ label="Disease"
163
+ sdsStyle="rounded"
164
+ sdsType="secondary"
165
+ />
166
+ </StyledIconCell>
167
+ </CellComponent>
168
+ <CellBasic
169
+ primaryText="Primary Text"
170
+ secondaryText="Secondary Text"
171
+ tertiaryText="Tertiary Text"
172
+ shouldShowTooltipOnHover={false}
173
+ verticalAlign="center"
174
+ />
175
+ <CellBasic
176
+ primaryText="Primary Text"
177
+ shouldShowTooltipOnHover={false}
178
+ verticalAlign="center"
179
+ />
180
+ <CellComponent verticalAlign="center">
181
+ <Icon sdsSize="l" sdsIcon="Virus" />
182
+ </CellComponent>
183
+ <CellBasic
184
+ primaryText="0.4"
185
+ horizontalAlign="right"
186
+ verticalAlign="center"
187
+ shouldShowTooltipOnHover={false}
188
+ />
189
+ </TableRow>
190
+ </tbody>
191
+ </Table>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ export default App;
197
+ ```
198
+
199
+ ## Recipes
200
+
201
+ These recipes show a few of the more complex approaches available for customizing Tables.
202
+
203
+ > **Note:** The Table components are presentational: they render the markup and the styling, and leave sorting, filtering and paging to whatever manages your data. The recipes below use [TanStack Table](https://tanstack.com/table/latest) for that, as does PreComposedTable internally, and two of them add [TanStack Query](https://tanstack.com/query/latest) and [TanStack Virtual](https://tanstack.com/virtual/latest) for fetching and virtualization. None of them ship with SDS. Install the ones you need alongside it.
204
+
205
+ ### Text-only Table
206
+
207
+ A table made up entirely of CellBasics, with headers that sort nothing and so set `hideSortIcon`.
208
+
209
+ **Example: TextOnlyTable**
210
+
211
+ ```tsx
212
+ import * as React from "react";
213
+ import {
214
+ CellBasic,
215
+ CellHeader,
216
+ TableHeader,
217
+ TableRow,
218
+ Table,
219
+ } from "@czi-sds/components";
220
+
221
+ import {
222
+ CellContext,
223
+ HeaderContext,
224
+ createColumnHelper,
225
+ flexRender,
226
+ getCoreRowModel,
227
+ useReactTable,
228
+ } from "@tanstack/react-table";
229
+
230
+ type Person = {
231
+ firstName: string;
232
+ lastName: string;
233
+ age: number;
234
+ birthdate: string;
235
+ visits: number;
236
+ status: string;
237
+ progress: number;
238
+ };
239
+
240
+ const defaultData: Person[] = [
241
+ {
242
+ firstName: "Tanner",
243
+ lastName: "Linsley",
244
+ age: 24,
245
+ birthdate: "26 October 1997",
246
+ visits: 100,
247
+ status: "In Relationship",
248
+ progress: 50,
249
+ },
250
+ {
251
+ firstName: "Tandy",
252
+ lastName: "Miller",
253
+ age: 40,
254
+ birthdate: "14 July 1982",
255
+ visits: 40,
256
+ status: "Single",
257
+ progress: 80,
258
+ },
259
+ {
260
+ firstName: "Joe",
261
+ lastName: "Dirte",
262
+ age: 45,
263
+ birthdate: "25 February 1977",
264
+ visits: 20,
265
+ status: "Complicated",
266
+ progress: 10,
267
+ },
268
+ ];
269
+
270
+ function createHeaderCell(
271
+ header: HeaderContext<Person, unknown>,
272
+ name: string
273
+ ) {
274
+ return (
275
+ <CellHeader
276
+ hideSortIcon
277
+ horizontalAlign={header.header.id === "progress" ? "right" : "left"}
278
+ >
279
+ {header.header.isPlaceholder ? "" : name}
280
+ </CellHeader>
281
+ );
282
+ }
283
+
284
+ function createBodyCell(cell: CellContext<Person, unknown>) {
285
+ return (
286
+ <CellBasic
287
+ primaryText={cell.getValue() as string}
288
+ shouldShowTooltipOnHover={false}
289
+ />
290
+ );
291
+ }
292
+
293
+ const columnHelper = createColumnHelper<Person>();
294
+
295
+ const columns = [
296
+ columnHelper.accessor("firstName", {
297
+ cell: (cell) => createBodyCell(cell),
298
+ header: (header) => createHeaderCell(header, "First Name"),
299
+ }),
300
+ columnHelper.accessor((row) => row.lastName, {
301
+ id: "lastName",
302
+ cell: (cell) => createBodyCell(cell),
303
+ header: (header) => createHeaderCell(header, "Last Name"),
304
+ }),
305
+ columnHelper.accessor("age", {
306
+ cell: (cell) => (
307
+ <CellBasic
308
+ key={cell.cell.id}
309
+ primaryText={cell.getValue().toString()}
310
+ secondaryText={cell.row.original.birthdate}
311
+ shouldShowTooltipOnHover={false}
312
+ />
313
+ ),
314
+ header: (header) => createHeaderCell(header, "Age"),
315
+ }),
316
+ columnHelper.accessor("visits", {
317
+ cell: (cell) => createBodyCell(cell),
318
+ header: (header) => createHeaderCell(header, "Visits"),
319
+ }),
320
+ columnHelper.accessor("status", {
321
+ cell: (cell) => createBodyCell(cell),
322
+ header: (header) => createHeaderCell(header, "Status"),
323
+ }),
324
+ columnHelper.accessor("progress", {
325
+ cell: (cell) => (
326
+ <CellBasic
327
+ key={cell.cell.id}
328
+ primaryText={cell.getValue().toString()}
329
+ secondaryText={`${cell.getValue()} ± 5%`}
330
+ shouldShowTooltipOnHover={false}
331
+ horizontalAlign="right"
332
+ />
333
+ ),
334
+ header: (header) => createHeaderCell(header, "Profile Progress"),
335
+ }),
336
+ ];
337
+
338
+ function App() {
339
+ const [data] = React.useState(() => [...defaultData]);
340
+
341
+ const table = useReactTable({
342
+ data,
343
+ columns,
344
+ getCoreRowModel: getCoreRowModel(),
345
+ });
346
+
347
+ return (
348
+ <div className="app">
349
+ <Table>
350
+ <TableHeader>
351
+ {table.getFlatHeaders().map((header) => (
352
+ <React.Fragment key={header.id}>
353
+ {flexRender(header.column.columnDef.header, header.getContext())}
354
+ </React.Fragment>
355
+ ))}
356
+ </TableHeader>
357
+ <tbody>
358
+ {table.getRowModel().rows.map((row) => (
359
+ <TableRow key={row.id}>
360
+ {row.getVisibleCells().map((cell) => (
361
+ <React.Fragment key={cell.id}>
362
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
363
+ </React.Fragment>
364
+ ))}
365
+ </TableRow>
366
+ ))}
367
+ </tbody>
368
+ </Table>
369
+ </div>
370
+ );
371
+ }
372
+
373
+ export default App;
374
+ ```
375
+
376
+ ```css
377
+ .app {
378
+ padding: 20px;
379
+ width: 90%;
380
+ margin: auto;
381
+ }
382
+ ```
383
+
384
+ ### Table with a custom sorting function
385
+
386
+ Every column here sorts on click through TanStack Table's own comparators, except Last Name, which is given a `sortingFn` of its own that orders names by length rather than alphabetically. Note that each sortable header sets `hover`, without which CellHeader draws no sort chevron.
387
+
388
+ **Example: TableWithCustomSorting**
389
+
390
+ ```tsx
391
+ // Sorting is TanStack Table's job, not the Table component's: the header cell only
392
+ // draws the state it is handed. active turns on the accent styling, direction picks
393
+ // the chevron, and onClick is wired to the column's own toggle handler.
394
+ //
395
+ // CellHeader draws its sort icon and its tooltip only while hover is true, so every
396
+ // sortable header below sets it. Without it the header sorts on click but never
397
+ // shows a chevron.
398
+ //
399
+ // The Last Name column sorts by string length instead of alphabetically, which is
400
+ // all a custom sortingFn is: a comparator handed to the column.
401
+
402
+ import {
403
+ Button,
404
+ CellBasic,
405
+ CellComponent,
406
+ CellHeader,
407
+ Icon,
408
+ Table,
409
+ TableHeader,
410
+ TableRow,
411
+ Tag,
412
+ getSpaces,
413
+ type CellHeaderDirection,
414
+ type CommonThemeProps,
415
+ } from "@czi-sds/components";
416
+ import styled from "@emotion/styled";
417
+ import { faker } from "@faker-js/faker";
418
+ import {
419
+ flexRender,
420
+ getCoreRowModel,
421
+ getSortedRowModel,
422
+ useReactTable,
423
+ type CellContext,
424
+ type ColumnDef,
425
+ type HeaderContext,
426
+ type Row,
427
+ type SortingState,
428
+ } from "@tanstack/react-table";
429
+ import * as React from "react";
430
+
431
+ type Person = {
432
+ age: number;
433
+ createdAt: Date;
434
+ firstName: string;
435
+ lastName: string;
436
+ status: "relationship" | "complicated" | "single";
437
+ };
438
+
439
+ const STATUSES: Person["status"][] = ["relationship", "complicated", "single"];
440
+
441
+ const STATUS_COLORS = {
442
+ complicated: "negative",
443
+ relationship: "positive",
444
+ single: "notice",
445
+ } as const;
446
+
447
+ function makeData(count: number): Person[] {
448
+ return Array.from({ length: count }, () => ({
449
+ age: faker.number.int({ max: 96, min: 18 }),
450
+ createdAt: faker.date.between({
451
+ from: new Date("2001-01-01"),
452
+ to: new Date(),
453
+ }),
454
+ firstName: faker.person.firstName(),
455
+ lastName: faker.person.lastName(),
456
+ status: faker.helpers.shuffle(STATUSES)[0]!,
457
+ }));
458
+ }
459
+
460
+ const ActionList = styled.ul<CommonThemeProps>`
461
+ ${(props) => {
462
+ const spaces = getSpaces(props);
463
+
464
+ return `
465
+ display: flex;
466
+ gap: ${spaces?.xxs}px;
467
+ justify-content: flex-end;
468
+ list-style: none;
469
+ margin: 0;
470
+ padding: 0;
471
+ `;
472
+ }}
473
+ `;
474
+
475
+ const ACTIONS = [
476
+ { icon: "TreeHorizontal", label: "View lineage" },
477
+ { icon: "BarChartVertical3", label: "View chart" },
478
+ { icon: "Download", label: "Download" },
479
+ { icon: "DotsHorizontal", label: "More actions" },
480
+ ] as const;
481
+
482
+ function sortableHeader(header: HeaderContext<Person, unknown>, name: string) {
483
+ const sorted = header.column.getIsSorted();
484
+
485
+ return (
486
+ <CellHeader
487
+ active={!!sorted}
488
+ colSpan={header.header.colSpan}
489
+ direction={sorted ? (sorted as CellHeaderDirection) : undefined}
490
+ hover
491
+ onClick={header.column.getToggleSortingHandler()}
492
+ >
493
+ {name}
494
+ </CellHeader>
495
+ );
496
+ }
497
+
498
+ function textCell(cell: CellContext<Person, unknown>) {
499
+ return (
500
+ <CellBasic
501
+ primaryText={String(cell.getValue())}
502
+ shouldShowTooltipOnHover={false}
503
+ verticalAlign="center"
504
+ />
505
+ );
506
+ }
507
+
508
+ const columns: ColumnDef<Person>[] = [
509
+ {
510
+ accessorKey: "firstName",
511
+ cell: (cell) => (
512
+ <CellBasic
513
+ icon={<Icon sdsIcon="Person" sdsSize="s" />}
514
+ iconVerticalAlign="center"
515
+ primaryText={cell.getValue() as string}
516
+ shouldShowTooltipOnHover={false}
517
+ verticalAlign="center"
518
+ />
519
+ ),
520
+ header: (header) => sortableHeader(header, "First Name"),
521
+ },
522
+ {
523
+ accessorKey: "lastName",
524
+ cell: textCell,
525
+ header: (header) => {
526
+ const sorted = header.column.getIsSorted();
527
+
528
+ return (
529
+ <CellHeader
530
+ active={!!sorted}
531
+ colSpan={header.header.colSpan}
532
+ direction={sorted ? (sorted as CellHeaderDirection) : undefined}
533
+ hover
534
+ onClick={header.column.getToggleSortingHandler()}
535
+ shouldShowTooltipOnHover
536
+ tooltipText="This column uses a custom sorting function."
537
+ tooltipSubtitle="Sorts names by how long they are."
538
+ >
539
+ Last Name
540
+ </CellHeader>
541
+ );
542
+ },
543
+ sortingFn: (rowA: Row<Person>, rowB: Row<Person>, columnId: string) => {
544
+ const a = rowA.getValue<string>(columnId).length;
545
+ const b = rowB.getValue<string>(columnId).length;
546
+
547
+ return a === b ? 0 : a > b ? 1 : -1;
548
+ },
549
+ },
550
+ {
551
+ accessorKey: "age",
552
+ cell: textCell,
553
+ header: (header) => sortableHeader(header, "Age"),
554
+ },
555
+ {
556
+ accessorKey: "status",
557
+ cell: (cell) => {
558
+ const status = cell.getValue() as Person["status"];
559
+
560
+ return (
561
+ <CellComponent verticalAlign="center">
562
+ <Tag
563
+ color={STATUS_COLORS[status]}
564
+ hover={false}
565
+ label={status}
566
+ sdsStyle="rounded"
567
+ sdsType="secondary"
568
+ />
569
+ </CellComponent>
570
+ );
571
+ },
572
+ header: (header) => sortableHeader(header, "Status"),
573
+ },
574
+ {
575
+ accessorKey: "createdAt",
576
+ cell: (cell) => {
577
+ const createdAt = cell.getValue() as Date;
578
+
579
+ return (
580
+ <CellBasic
581
+ primaryText={createdAt.getFullYear().toString()}
582
+ secondaryText={createdAt.toLocaleString("en-us", {
583
+ day: "numeric",
584
+ month: "long",
585
+ })}
586
+ tooltipProps={{ title: createdAt.toLocaleString("en-us") }}
587
+ verticalAlign="center"
588
+ />
589
+ );
590
+ },
591
+ header: (header) => sortableHeader(header, "Created At"),
592
+ },
593
+ {
594
+ accessorKey: "actions",
595
+ cell: () => (
596
+ <CellComponent horizontalAlign="right" verticalAlign="center">
597
+ <ActionList>
598
+ {ACTIONS.map((action) => (
599
+ <li key={action.icon}>
600
+ <Button
601
+ aria-label={action.label}
602
+ sdsStyle="minimal"
603
+ sdsType="secondary"
604
+ >
605
+ <Icon sdsIcon={action.icon} sdsSize="s" />
606
+ </Button>
607
+ </li>
608
+ ))}
609
+ </ActionList>
610
+ </CellComponent>
611
+ ),
612
+ enableSorting: false,
613
+ header: () => (
614
+ <CellHeader hideSortIcon horizontalAlign="right">
615
+ Actions
616
+ </CellHeader>
617
+ ),
618
+ id: "actions",
619
+ },
620
+ ];
621
+
622
+ function App() {
623
+ const [sorting, setSorting] = React.useState<SortingState>([]);
624
+ const [data] = React.useState(() => makeData(10));
625
+
626
+ const table = useReactTable({
627
+ columns,
628
+ data,
629
+ getCoreRowModel: getCoreRowModel(),
630
+ getSortedRowModel: getSortedRowModel(),
631
+ onSortingChange: setSorting,
632
+ state: { sorting },
633
+ });
634
+
635
+ return (
636
+ <div className="app">
637
+ <Table>
638
+ <TableHeader>
639
+ {table.getFlatHeaders().map((header) => (
640
+ <React.Fragment key={header.id}>
641
+ {flexRender(header.column.columnDef.header, header.getContext())}
642
+ </React.Fragment>
643
+ ))}
644
+ </TableHeader>
645
+ <tbody>
646
+ {table.getRowModel().rows.map((row) => (
647
+ <TableRow key={row.id}>
648
+ {row.getVisibleCells().map((cell) => (
649
+ <React.Fragment key={cell.id}>
650
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
651
+ </React.Fragment>
652
+ ))}
653
+ </TableRow>
654
+ ))}
655
+ </tbody>
656
+ </Table>
657
+ </div>
658
+ );
659
+ }
660
+
661
+ export default App;
662
+ ```
663
+
664
+ ### Table with custom Tooltips
665
+
666
+ This shows a Table with custom Tooltips.
667
+
668
+ **Example: TableWithCustomTooltips**
669
+
670
+ ```tsx
671
+ import * as React from "react";
672
+ import {
673
+ CellBasic,
674
+ CellHeader,
675
+ TableHeader,
676
+ TableRow,
677
+ Table,
678
+ CellComponent,
679
+ Tag,
680
+ } from "@czi-sds/components";
681
+ import {
682
+ CellContext,
683
+ HeaderContext,
684
+ createColumnHelper,
685
+ flexRender,
686
+ getCoreRowModel,
687
+ useReactTable,
688
+ } from "@tanstack/react-table";
689
+
690
+ type Person = {
691
+ firstName: string;
692
+ lastName: string;
693
+ age: number;
694
+ birthdate: string;
695
+ visits: number;
696
+ status: string;
697
+ progress: number;
698
+ };
699
+
700
+ const defaultData: Person[] = [
701
+ {
702
+ firstName: "Tanner",
703
+ lastName: "Linsley",
704
+ age: 24,
705
+ birthdate: "26 October 1997",
706
+ visits: 100,
707
+ status: "In Relationship",
708
+ progress: 50,
709
+ },
710
+ {
711
+ firstName: "Tandy",
712
+ lastName: "Miller",
713
+ age: 40,
714
+ birthdate: "14 July 1982",
715
+ visits: 40,
716
+ status: "Single",
717
+ progress: 80,
718
+ },
719
+ {
720
+ firstName: "Joe",
721
+ lastName: "Dirte",
722
+ age: 45,
723
+ birthdate: "25 February 1977",
724
+ visits: 20,
725
+ status: "Complicated",
726
+ progress: 10,
727
+ },
728
+ ];
729
+
730
+ function createHeaderCell(
731
+ header: HeaderContext<Person, unknown>,
732
+ name: string
733
+ ) {
734
+ return (
735
+ <CellHeader
736
+ hideSortIcon
737
+ horizontalAlign={header.header.id === "progress" ? "right" : "left"}
738
+ >
739
+ {header.header.isPlaceholder ? "" : name}
740
+ </CellHeader>
741
+ );
742
+ }
743
+
744
+ function createBodyCell(info: CellContext<Person, unknown>) {
745
+ return (
746
+ <CellBasic
747
+ primaryText={info.getValue() as string}
748
+ shouldShowTooltipOnHover={false}
749
+ />
750
+ );
751
+ }
752
+
753
+ const columnHelper = createColumnHelper<Person>();
754
+
755
+ const columns = [
756
+ columnHelper.accessor("firstName", {
757
+ cell: (info) => createBodyCell(info),
758
+ header: (header) => createHeaderCell(header, "First Name"),
759
+ }),
760
+ columnHelper.accessor("lastName", {
761
+ cell: (info) => createBodyCell(info),
762
+ header: (header) => createHeaderCell(header, "Last Name"),
763
+ }),
764
+ columnHelper.accessor("age", {
765
+ cell: (info) => (
766
+ <CellBasic
767
+ key={info.cell.id}
768
+ primaryText={info.cell.getValue().toString()}
769
+ secondaryText={info.cell.row.original.birthdate}
770
+ tooltipProps={{
771
+ leaveTouchDelay: 0,
772
+ leaveDelay: 0,
773
+ subtitle: "",
774
+ title: info.cell.row.original.birthdate,
775
+ }}
776
+ />
777
+ ),
778
+ header: (header) => createHeaderCell(header, "Age"),
779
+ }),
780
+ columnHelper.accessor("visits", {
781
+ cell: (info) => createBodyCell(info),
782
+ header: (header) => createHeaderCell(header, "Visits"),
783
+ }),
784
+ columnHelper.accessor("status", {
785
+ cell: (info) => (
786
+ <CellComponent verticalAlign="center" key={info.cell.id}>
787
+ <Tag
788
+ label={info.cell.getValue() as string}
789
+ color={
790
+ info.cell.getValue() === "Single"
791
+ ? "notice"
792
+ : info.cell.getValue() === "In Relationship"
793
+ ? "positive"
794
+ : "negative"
795
+ }
796
+ sdsStyle="rounded"
797
+ sdsType="secondary"
798
+ hover={false}
799
+ />
800
+ </CellComponent>
801
+ ),
802
+ header: (header) => createHeaderCell(header, "Status"),
803
+ }),
804
+ columnHelper.accessor("progress", {
805
+ cell: (info) => (
806
+ <CellBasic
807
+ key={info.cell.id}
808
+ primaryText={info.cell.getValue().toString()}
809
+ secondaryText={`${info.cell.getValue()} ± 5%`}
810
+ horizontalAlign="right"
811
+ tooltipProps={{
812
+ leaveTouchDelay: 0,
813
+ leaveDelay: 0,
814
+ title: `Profile Progress`,
815
+ }}
816
+ />
817
+ ),
818
+ header: (header) => createHeaderCell(header, "Profile Progress"),
819
+ }),
820
+ ];
821
+
822
+ function App() {
823
+ const [data] = React.useState(() => [...defaultData]);
824
+
825
+ const table = useReactTable({
826
+ data,
827
+ columns,
828
+ getCoreRowModel: getCoreRowModel(),
829
+ });
830
+
831
+ return (
832
+ <div className="app">
833
+ <Table>
834
+ <TableHeader>
835
+ {table.getFlatHeaders().map((header) => (
836
+ <React.Fragment key={header.id}>
837
+ {flexRender(header.column.columnDef.header, header.getContext())}
838
+ </React.Fragment>
839
+ ))}
840
+ </TableHeader>
841
+ <tbody>
842
+ {table.getRowModel().rows.map((row) => (
843
+ <TableRow key={row.id}>
844
+ {row.getVisibleCells().map((cell) => (
845
+ <React.Fragment key={cell.id}>
846
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
847
+ </React.Fragment>
848
+ ))}
849
+ </TableRow>
850
+ ))}
851
+ </tbody>
852
+ </Table>
853
+ </div>
854
+ );
855
+ }
856
+
857
+ export default App;
858
+ ```
859
+
860
+ ```css
861
+ .app {
862
+ padding: 16px;
863
+ }
864
+ ```
865
+
866
+ ### Table with virtualized rows
867
+
868
+ A thousand rows in a scroll container with only the visible ones in the DOM, using `@tanstack/react-virtual` to decide which rows to draw and `@tanstack/react-query` to fetch the next batch as the scroller nears its end. The spacer rows above and below the window are what keep the scrollbar proportional.
869
+
870
+ **Example: TableWithVirtualizedRows**
871
+
872
+ ```tsx
873
+ // A thousand rows behind a scroll container, with only the visible ones in the DOM.
874
+ // Two libraries do the work: @tanstack/react-virtual measures the scroller and
875
+ // reports which rows to draw, and @tanstack/react-query fetches the next batch as
876
+ // the scroller nears its end. The Table components themselves are unchanged: the
877
+ // spacer rows above and below the window are what keep the scrollbar honest.
878
+ //
879
+ // estimateSize has to be close to a real row's height or the scrollbar drifts as it
880
+ // measures; an SDS row of single-line CellBasics is about 45px.
881
+ //
882
+ // react-query needs a QueryClientProvider above anything that queries, which in an
883
+ // application belongs near the root rather than inside a component like this.
884
+
885
+ import {
886
+ CellBasic,
887
+ CellComponent,
888
+ CellHeader,
889
+ Table,
890
+ TableHeader,
891
+ TableRow,
892
+ Tag,
893
+ fontBodyXs,
894
+ getSemanticColors,
895
+ getSpaces,
896
+ type CellHeaderDirection,
897
+ type CommonThemeProps,
898
+ } from "@czi-sds/components";
899
+ import styled from "@emotion/styled";
900
+ import { faker } from "@faker-js/faker";
901
+ import {
902
+ QueryClient,
903
+ QueryClientProvider,
904
+ useInfiniteQuery,
905
+ } from "@tanstack/react-query";
906
+ import {
907
+ flexRender,
908
+ getCoreRowModel,
909
+ getSortedRowModel,
910
+ useReactTable,
911
+ type CellContext,
912
+ type ColumnDef,
913
+ type HeaderContext,
914
+ type SortingState,
915
+ } from "@tanstack/react-table";
916
+ import { useVirtualizer } from "@tanstack/react-virtual";
917
+ import * as React from "react";
918
+
919
+ const FETCH_SIZE = 25;
920
+ const ROW_HEIGHT = 48;
921
+
922
+ type Person = {
923
+ age: number;
924
+ firstName: string;
925
+ id: number;
926
+ lastName: string;
927
+ status: "relationship" | "complicated" | "single";
928
+ };
929
+
930
+ const STATUSES: Person["status"][] = ["relationship", "complicated", "single"];
931
+
932
+ const STATUS_COLORS = {
933
+ complicated: "negative",
934
+ relationship: "positive",
935
+ single: "notice",
936
+ } as const;
937
+
938
+ const ROWS: Person[] = Array.from({ length: 1000 }, (_unused, index) => ({
939
+ age: faker.number.int({ max: 80, min: 18 }),
940
+ firstName: faker.person.firstName(),
941
+ id: index + 1,
942
+ lastName: faker.person.lastName(),
943
+ status: faker.helpers.shuffle(STATUSES)[0]!,
944
+ }));
945
+
946
+ type RowPage = {
947
+ rows: Person[];
948
+ totalRowCount: number;
949
+ };
950
+
951
+ // Stands in for a paginated endpoint: sorts the whole set, then returns one slice.
952
+ async function fetchRows(
953
+ start: number,
954
+ size: number,
955
+ sorting: SortingState
956
+ ): Promise<RowPage> {
957
+ const rows = [...ROWS];
958
+ const sort = sorting[0];
959
+
960
+ if (sort) {
961
+ const key = sort.id as keyof Person;
962
+ rows.sort((a, b) => {
963
+ if (a[key] === b[key]) return 0;
964
+ const ascending = a[key] > b[key] ? 1 : -1;
965
+ return sort.desc ? -ascending : ascending;
966
+ });
967
+ }
968
+
969
+ return {
970
+ rows: rows.slice(start, start + size),
971
+ totalRowCount: rows.length,
972
+ };
973
+ }
974
+
975
+ const Scroller = styled.div<CommonThemeProps>`
976
+ ${(props) => {
977
+ const semanticColors = getSemanticColors(props);
978
+
979
+ return `
980
+ border: 1px solid ${semanticColors?.base?.divider};
981
+ height: 400px;
982
+ overflow: auto;
983
+
984
+ /* The header has to be pinned by hand; the Table components do not do it. */
985
+ thead th {
986
+ background: ${semanticColors?.base?.backgroundPrimary};
987
+ position: sticky;
988
+ top: 0;
989
+ z-index: 1;
990
+ }
991
+ `;
992
+ }}
993
+ `;
994
+
995
+ const Readout = styled.p<CommonThemeProps>`
996
+ ${fontBodyXs}
997
+
998
+ ${(props) => {
999
+ const semanticColors = getSemanticColors(props);
1000
+ const spaces = getSpaces(props);
1001
+
1002
+ return `
1003
+ color: ${semanticColors?.base?.textSecondary};
1004
+ margin: ${spaces?.s}px 0 0;
1005
+ `;
1006
+ }}
1007
+ `;
1008
+
1009
+ function sortableHeader(header: HeaderContext<Person, unknown>, name: string) {
1010
+ const sorted = header.column.getIsSorted();
1011
+
1012
+ return (
1013
+ <CellHeader
1014
+ active={!!sorted}
1015
+ direction={sorted ? (sorted as CellHeaderDirection) : undefined}
1016
+ hover
1017
+ onClick={header.column.getToggleSortingHandler()}
1018
+ >
1019
+ {name}
1020
+ </CellHeader>
1021
+ );
1022
+ }
1023
+
1024
+ function textCell(cell: CellContext<Person, unknown>) {
1025
+ return (
1026
+ <CellBasic
1027
+ primaryText={String(cell.getValue())}
1028
+ shouldShowTooltipOnHover={false}
1029
+ verticalAlign="center"
1030
+ />
1031
+ );
1032
+ }
1033
+
1034
+ const columns: ColumnDef<Person>[] = [
1035
+ {
1036
+ accessorKey: "id",
1037
+ cell: textCell,
1038
+ header: (header) => sortableHeader(header, "ID"),
1039
+ },
1040
+ {
1041
+ accessorKey: "firstName",
1042
+ cell: textCell,
1043
+ header: (header) => sortableHeader(header, "First Name"),
1044
+ },
1045
+ {
1046
+ accessorKey: "lastName",
1047
+ cell: textCell,
1048
+ header: (header) => sortableHeader(header, "Last Name"),
1049
+ },
1050
+ {
1051
+ accessorKey: "age",
1052
+ cell: textCell,
1053
+ header: (header) => sortableHeader(header, "Age"),
1054
+ },
1055
+ {
1056
+ accessorKey: "status",
1057
+ cell: (cell) => {
1058
+ const status = cell.getValue() as Person["status"];
1059
+
1060
+ return (
1061
+ <CellComponent verticalAlign="center">
1062
+ <Tag
1063
+ color={STATUS_COLORS[status]}
1064
+ hover={false}
1065
+ label={status}
1066
+ sdsStyle="rounded"
1067
+ sdsType="secondary"
1068
+ />
1069
+ </CellComponent>
1070
+ );
1071
+ },
1072
+ header: (header) => sortableHeader(header, "Status"),
1073
+ },
1074
+ ];
1075
+
1076
+ function VirtualizedTable() {
1077
+ const scrollerRef = React.useRef<HTMLDivElement>(null);
1078
+ const [sorting, setSorting] = React.useState<SortingState>([]);
1079
+
1080
+ // Sorting is part of the query key, so changing it starts the list over.
1081
+ const { data, fetchNextPage, isFetching, isLoading } = useInfiniteQuery({
1082
+ getNextPageParam: (_lastPage: RowPage, pages: RowPage[]) => pages.length,
1083
+ initialPageParam: 0,
1084
+ queryFn: ({ pageParam }) =>
1085
+ fetchRows(pageParam * FETCH_SIZE, FETCH_SIZE, sorting),
1086
+ queryKey: ["virtualized-table-rows", sorting],
1087
+ });
1088
+
1089
+ const rows = React.useMemo(
1090
+ () => data?.pages.flatMap((page) => page.rows) ?? [],
1091
+ [data]
1092
+ );
1093
+ const totalRowCount = data?.pages[0]?.totalRowCount ?? 0;
1094
+
1095
+ const table = useReactTable({
1096
+ columns,
1097
+ data: rows,
1098
+ getCoreRowModel: getCoreRowModel(),
1099
+ getSortedRowModel: getSortedRowModel(),
1100
+ manualSorting: true,
1101
+ onSortingChange: setSorting,
1102
+ state: { sorting },
1103
+ });
1104
+ const tableRows = table.getRowModel().rows;
1105
+
1106
+ const virtualizer = useVirtualizer({
1107
+ count: tableRows.length,
1108
+ estimateSize: () => ROW_HEIGHT,
1109
+ getScrollElement: () => scrollerRef.current,
1110
+ overscan: 10,
1111
+ });
1112
+ const virtualRows = virtualizer.getVirtualItems();
1113
+ const paddingTop = virtualRows[0]?.start ?? 0;
1114
+ const paddingBottom =
1115
+ virtualizer.getTotalSize() -
1116
+ (virtualRows[virtualRows.length - 1]?.end ?? 0);
1117
+
1118
+ const fetchMoreOnBottomReached = React.useCallback(() => {
1119
+ const scroller = scrollerRef.current;
1120
+ if (!scroller || isFetching || rows.length >= totalRowCount) return;
1121
+
1122
+ const remaining =
1123
+ scroller.scrollHeight - scroller.scrollTop - scroller.clientHeight;
1124
+ if (remaining < 300) {
1125
+ fetchNextPage();
1126
+ }
1127
+ }, [fetchNextPage, isFetching, rows.length, totalRowCount]);
1128
+
1129
+ // The first batch may not fill the scroller, in which case no scroll event fires.
1130
+ React.useEffect(fetchMoreOnBottomReached, [fetchMoreOnBottomReached]);
1131
+
1132
+ if (isLoading) return <p>Loading…</p>;
1133
+
1134
+ return (
1135
+ <>
1136
+ <Scroller onScroll={fetchMoreOnBottomReached} ref={scrollerRef}>
1137
+ <Table>
1138
+ <TableHeader>
1139
+ {table.getFlatHeaders().map((header) => (
1140
+ <React.Fragment key={header.id}>
1141
+ {flexRender(
1142
+ header.column.columnDef.header,
1143
+ header.getContext()
1144
+ )}
1145
+ </React.Fragment>
1146
+ ))}
1147
+ </TableHeader>
1148
+ <tbody>
1149
+ {paddingTop > 0 && (
1150
+ <tr>
1151
+ <td colSpan={columns.length} style={{ height: paddingTop }} />
1152
+ </tr>
1153
+ )}
1154
+ {virtualRows.map((virtualRow) => {
1155
+ const row = tableRows[virtualRow.index]!;
1156
+
1157
+ return (
1158
+ <TableRow key={row.id}>
1159
+ {row.getVisibleCells().map((cell) => (
1160
+ <React.Fragment key={cell.id}>
1161
+ {flexRender(
1162
+ cell.column.columnDef.cell,
1163
+ cell.getContext()
1164
+ )}
1165
+ </React.Fragment>
1166
+ ))}
1167
+ </TableRow>
1168
+ );
1169
+ })}
1170
+ {paddingBottom > 0 && (
1171
+ <tr>
1172
+ <td
1173
+ colSpan={columns.length}
1174
+ style={{ height: paddingBottom }}
1175
+ />
1176
+ </tr>
1177
+ )}
1178
+ </tbody>
1179
+ </Table>
1180
+ </Scroller>
1181
+
1182
+ <Readout>
1183
+ Showing {virtualRows.length} of {rows.length} loaded rows, out of{" "}
1184
+ {totalRowCount}. Scroll to load more.
1185
+ </Readout>
1186
+ </>
1187
+ );
1188
+ }
1189
+
1190
+ const queryClient = new QueryClient();
1191
+
1192
+ function App() {
1193
+ return (
1194
+ <div className="app">
1195
+ <QueryClientProvider client={queryClient}>
1196
+ <VirtualizedTable />
1197
+ </QueryClientProvider>
1198
+ </div>
1199
+ );
1200
+ }
1201
+
1202
+ export default App;
1203
+ ```
1204
+
1205
+ ### Editable Table Cells with Access to Internal Data
1206
+
1207
+ Editing reaches the data above the table through TanStack Table's `meta` object. Click a cell to edit it; the change is written back on blur, and the log below records what moved. Note that the editable cells are CellComponents rather than CellBasics, since CellBasic renders text rather than `children`.
1208
+
1209
+ **Example: TableWithEditableCells**
1210
+
1211
+ ```tsx
1212
+ // Editing goes through TanStack Table's meta object, which is the supported way to
1213
+ // reach application state from inside a cell renderer. Each cell keeps the value
1214
+ // being typed in its own state and calls meta.updateData on blur; the table itself
1215
+ // stays a pure view of the data above it.
1216
+ //
1217
+ // An interactive cell belongs in a CellComponent, not a CellBasic: CellBasic takes
1218
+ // text through primaryText and renders it as text, while CellComponent takes
1219
+ // children.
1220
+
1221
+ import {
1222
+ Button,
1223
+ CellComponent,
1224
+ CellHeader,
1225
+ Icon,
1226
+ Table,
1227
+ TableHeader,
1228
+ TableRow,
1229
+ fontBodyS,
1230
+ fontBodyXs,
1231
+ fontHeaderS,
1232
+ getCorners,
1233
+ getSemanticColors,
1234
+ getSpaces,
1235
+ type CommonThemeProps,
1236
+ } from "@czi-sds/components";
1237
+ import styled from "@emotion/styled";
1238
+ import { faker } from "@faker-js/faker";
1239
+ import {
1240
+ flexRender,
1241
+ getCoreRowModel,
1242
+ useReactTable,
1243
+ type CellContext,
1244
+ type ColumnDef,
1245
+ type RowData,
1246
+ } from "@tanstack/react-table";
1247
+ import * as React from "react";
1248
+
1249
+ type Person = {
1250
+ age: number;
1251
+ firstName: string;
1252
+ lastName: string;
1253
+ status: string;
1254
+ visits: number;
1255
+ };
1256
+
1257
+ // Declaring the meta shape is what types table.options.meta at the call site. The
1258
+ // type parameter is unused here but has to match TanStack's own declaration.
1259
+ declare module "@tanstack/react-table" {
1260
+ interface TableMeta<TData extends RowData> {
1261
+ updateData: (rowIndex: number, columnId: string, value: string) => void;
1262
+ }
1263
+ }
1264
+
1265
+ function makeData(count: number): Person[] {
1266
+ return Array.from({ length: count }, () => ({
1267
+ age: faker.number.int({ max: 80, min: 18 }),
1268
+ firstName: faker.person.firstName(),
1269
+ lastName: faker.person.lastName(),
1270
+ status: faker.helpers.shuffle([
1271
+ "relationship",
1272
+ "complicated",
1273
+ "single",
1274
+ ])[0]!,
1275
+ visits: faker.number.int(1000),
1276
+ }));
1277
+ }
1278
+
1279
+ const EditableInput = styled.input<CommonThemeProps>`
1280
+ ${fontBodyS}
1281
+
1282
+ ${(props) => {
1283
+ const corners = getCorners(props);
1284
+ const semanticColors = getSemanticColors(props);
1285
+ const spaces = getSpaces(props);
1286
+
1287
+ return `
1288
+ background: transparent;
1289
+ border: 1px solid transparent;
1290
+ border-radius: ${corners?.m}px;
1291
+ color: ${semanticColors?.base?.textPrimary};
1292
+ padding: ${spaces?.xxs}px ${spaces?.xs}px;
1293
+ width: 100%;
1294
+
1295
+ &:hover {
1296
+ border-color: ${semanticColors?.base?.borderPrimary};
1297
+ }
1298
+
1299
+ &:focus {
1300
+ border-color: ${semanticColors?.accent?.borderSelected};
1301
+ outline: none;
1302
+ }
1303
+ `;
1304
+ }}
1305
+ `;
1306
+
1307
+ const Toolbar = styled.div<CommonThemeProps>`
1308
+ ${(props) => {
1309
+ const spaces = getSpaces(props);
1310
+
1311
+ return `
1312
+ display: flex;
1313
+ margin-top: ${spaces?.l}px;
1314
+ `;
1315
+ }}
1316
+ `;
1317
+
1318
+ const Log = styled.div<CommonThemeProps>`
1319
+ ${(props) => {
1320
+ const spaces = getSpaces(props);
1321
+
1322
+ return `
1323
+ margin-top: ${spaces?.l}px;
1324
+ `;
1325
+ }}
1326
+ `;
1327
+
1328
+ const LogTitle = styled.h4<CommonThemeProps>`
1329
+ ${fontHeaderS}
1330
+ margin: 0;
1331
+ `;
1332
+
1333
+ const LogBody = styled.pre<CommonThemeProps>`
1334
+ ${fontBodyXs}
1335
+
1336
+ ${(props) => {
1337
+ const corners = getCorners(props);
1338
+ const semanticColors = getSemanticColors(props);
1339
+ const spaces = getSpaces(props);
1340
+
1341
+ return `
1342
+ background: ${semanticColors?.base?.backgroundSecondary};
1343
+ border-radius: ${corners?.m}px;
1344
+ color: ${semanticColors?.base?.textSecondary};
1345
+ margin: ${spaces?.xs}px 0 0;
1346
+ max-height: 160px;
1347
+ overflow: auto;
1348
+ padding: ${spaces?.s}px;
1349
+ `;
1350
+ }}
1351
+ `;
1352
+
1353
+ function CellEditor(cell: CellContext<Person, unknown>) {
1354
+ const initialValue = String(cell.getValue());
1355
+ const [value, setValue] = React.useState(initialValue);
1356
+
1357
+ // Regenerating the data resets every cell back to what the table now holds.
1358
+ React.useEffect(() => setValue(initialValue), [initialValue]);
1359
+
1360
+ return (
1361
+ <CellComponent verticalAlign="center">
1362
+ <EditableInput
1363
+ aria-label={`${cell.column.id}, row ${cell.row.index + 1}`}
1364
+ onBlur={() =>
1365
+ cell.table.options.meta?.updateData(
1366
+ cell.row.index,
1367
+ cell.column.id,
1368
+ value
1369
+ )
1370
+ }
1371
+ onChange={(event) => setValue(event.target.value)}
1372
+ value={value}
1373
+ />
1374
+ </CellComponent>
1375
+ );
1376
+ }
1377
+
1378
+ const columns: ColumnDef<Person>[] = [
1379
+ { accessorKey: "firstName", header: "First Name" },
1380
+ { accessorKey: "lastName", header: "Last Name" },
1381
+ { accessorKey: "age", header: "Age" },
1382
+ { accessorKey: "visits", header: "Visits" },
1383
+ { accessorKey: "status", header: "Status" },
1384
+ ];
1385
+
1386
+ function App() {
1387
+ const [data, setData] = React.useState(() => makeData(3));
1388
+ const [log, setLog] = React.useState<string[]>([]);
1389
+
1390
+ const table = useReactTable({
1391
+ columns,
1392
+ data,
1393
+ // Every column without its own cell renderer gets the editor.
1394
+ defaultColumn: { cell: CellEditor },
1395
+ getCoreRowModel: getCoreRowModel(),
1396
+ meta: {
1397
+ updateData: (rowIndex, columnId, value) => {
1398
+ setData((rows) =>
1399
+ rows.map((row, index) => {
1400
+ if (index !== rowIndex) return row;
1401
+
1402
+ const previous = String(row[columnId as keyof Person]);
1403
+ if (previous === value) return row;
1404
+
1405
+ setLog((entries) => [
1406
+ `row ${rowIndex + 1}, ${columnId}: "${previous}" → "${value}"`,
1407
+ ...entries,
1408
+ ]);
1409
+
1410
+ return { ...row, [columnId]: value };
1411
+ })
1412
+ );
1413
+ },
1414
+ },
1415
+ });
1416
+
1417
+ return (
1418
+ <div className="app">
1419
+ <Table>
1420
+ <TableHeader>
1421
+ {table.getFlatHeaders().map((header) => (
1422
+ <CellHeader hideSortIcon key={header.id}>
1423
+ {flexRender(header.column.columnDef.header, header.getContext())}
1424
+ </CellHeader>
1425
+ ))}
1426
+ </TableHeader>
1427
+ <tbody>
1428
+ {table.getRowModel().rows.map((row) => (
1429
+ <TableRow key={row.id} shouldShowTooltipOnHover={false}>
1430
+ {row.getVisibleCells().map((cell) => (
1431
+ <React.Fragment key={cell.id}>
1432
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
1433
+ </React.Fragment>
1434
+ ))}
1435
+ </TableRow>
1436
+ ))}
1437
+ </tbody>
1438
+ </Table>
1439
+
1440
+ <Toolbar>
1441
+ <Button
1442
+ onClick={() => {
1443
+ setData(makeData(3));
1444
+ setLog([]);
1445
+ }}
1446
+ sdsStyle="solid"
1447
+ sdsType="primary"
1448
+ startIcon={<Icon sdsIcon="Refresh" sdsSize="s" />}
1449
+ >
1450
+ Replace the data
1451
+ </Button>
1452
+ </Toolbar>
1453
+
1454
+ <Log>
1455
+ <LogTitle>Changes written back to the data</LogTitle>
1456
+ <LogBody>
1457
+ {log.length ? log.join("\n") : "Edit a cell, then click outside it."}
1458
+ </LogBody>
1459
+ </Log>
1460
+ </div>
1461
+ );
1462
+ }
1463
+
1464
+ export default App;
1465
+ ```
1466
+
1467
+ ### Table with Front-End Pagination
1468
+
1469
+ All the rows are in memory and TanStack Table slices them, so paging is only a change of `pageIndex`. Pagination counts pages from 1 while TanStack counts from 0, which is the one thing to keep straight when wiring the two together.
1470
+
1471
+ **Example: TableWithFrontEndPagination**
1472
+
1473
+ ```tsx
1474
+ // All 99 rows are in memory and TanStack Table slices them, so paging is only a
1475
+ // change of pageIndex. The SDS Pagination component is a controlled view of that
1476
+ // state: it reports which page was asked for and the table decides what that means.
1477
+ //
1478
+ // Pagination counts pages from 1 and TanStack counts from 0, which is the only
1479
+ // thing to keep straight when wiring the two together.
1480
+
1481
+ import {
1482
+ CellBasic,
1483
+ CellComponent,
1484
+ CellHeader,
1485
+ Pagination,
1486
+ Table,
1487
+ TableHeader,
1488
+ TableRow,
1489
+ Tag,
1490
+ getSpaces,
1491
+ type CellHeaderDirection,
1492
+ type CommonThemeProps,
1493
+ } from "@czi-sds/components";
1494
+ import styled from "@emotion/styled";
1495
+ import { faker } from "@faker-js/faker";
1496
+ import {
1497
+ flexRender,
1498
+ getCoreRowModel,
1499
+ getPaginationRowModel,
1500
+ getSortedRowModel,
1501
+ useReactTable,
1502
+ type CellContext,
1503
+ type ColumnDef,
1504
+ type HeaderContext,
1505
+ type PaginationState,
1506
+ type SortingState,
1507
+ } from "@tanstack/react-table";
1508
+ import * as React from "react";
1509
+
1510
+ const DATA_COUNT = 99;
1511
+ const PAGE_SIZE = 5;
1512
+
1513
+ type Person = {
1514
+ age: number;
1515
+ firstName: string;
1516
+ lastName: string;
1517
+ progress: number;
1518
+ status: "relationship" | "complicated" | "single";
1519
+ visits: number;
1520
+ };
1521
+
1522
+ const STATUSES: Person["status"][] = ["relationship", "complicated", "single"];
1523
+
1524
+ const STATUS_COLORS = {
1525
+ complicated: "negative",
1526
+ relationship: "positive",
1527
+ single: "notice",
1528
+ } as const;
1529
+
1530
+ function makeData(count: number): Person[] {
1531
+ return Array.from({ length: count }, () => ({
1532
+ age: faker.number.int({ max: 96, min: 18 }),
1533
+ firstName: faker.person.firstName(),
1534
+ lastName: faker.person.lastName(),
1535
+ progress: faker.number.int(100),
1536
+ status: faker.helpers.shuffle(STATUSES)[0]!,
1537
+ visits: faker.number.int(1000),
1538
+ }));
1539
+ }
1540
+
1541
+ const PaginationWrapper = styled.div<CommonThemeProps>`
1542
+ ${(props) => {
1543
+ const spaces = getSpaces(props);
1544
+
1545
+ return `
1546
+ display: flex;
1547
+ justify-content: flex-end;
1548
+ margin-top: ${spaces?.l}px;
1549
+ `;
1550
+ }}
1551
+ `;
1552
+
1553
+ function sortableHeader(header: HeaderContext<Person, unknown>, name: string) {
1554
+ const sorted = header.column.getIsSorted();
1555
+ const isNumeric = header.column.id === "progress";
1556
+
1557
+ return (
1558
+ <CellHeader
1559
+ active={!!sorted}
1560
+ colSpan={header.header.colSpan}
1561
+ direction={sorted ? (sorted as CellHeaderDirection) : undefined}
1562
+ horizontalAlign={isNumeric ? "right" : "left"}
1563
+ hover
1564
+ onClick={header.column.getToggleSortingHandler()}
1565
+ >
1566
+ {name}
1567
+ </CellHeader>
1568
+ );
1569
+ }
1570
+
1571
+ function textCell(cell: CellContext<Person, unknown>) {
1572
+ return (
1573
+ <CellBasic
1574
+ horizontalAlign={cell.column.id === "progress" ? "right" : "left"}
1575
+ primaryText={String(cell.getValue())}
1576
+ shouldShowTooltipOnHover={false}
1577
+ verticalAlign="center"
1578
+ />
1579
+ );
1580
+ }
1581
+
1582
+ const columns: ColumnDef<Person>[] = [
1583
+ {
1584
+ accessorKey: "firstName",
1585
+ cell: textCell,
1586
+ header: (header) => sortableHeader(header, "First Name"),
1587
+ },
1588
+ {
1589
+ accessorKey: "lastName",
1590
+ cell: textCell,
1591
+ header: (header) => sortableHeader(header, "Last Name"),
1592
+ },
1593
+ {
1594
+ accessorKey: "age",
1595
+ cell: textCell,
1596
+ header: (header) => sortableHeader(header, "Age"),
1597
+ },
1598
+ {
1599
+ accessorKey: "visits",
1600
+ cell: textCell,
1601
+ header: (header) => sortableHeader(header, "Visits"),
1602
+ },
1603
+ {
1604
+ accessorKey: "status",
1605
+ cell: (cell) => {
1606
+ const status = cell.getValue() as Person["status"];
1607
+
1608
+ return (
1609
+ <CellComponent verticalAlign="center">
1610
+ <Tag
1611
+ color={STATUS_COLORS[status]}
1612
+ hover={false}
1613
+ label={status}
1614
+ sdsStyle="rounded"
1615
+ sdsType="secondary"
1616
+ />
1617
+ </CellComponent>
1618
+ );
1619
+ },
1620
+ header: (header) => sortableHeader(header, "Status"),
1621
+ },
1622
+ {
1623
+ accessorKey: "progress",
1624
+ cell: textCell,
1625
+ header: (header) => sortableHeader(header, "Profile Progress"),
1626
+ },
1627
+ ];
1628
+
1629
+ function App() {
1630
+ const [data] = React.useState(() => makeData(DATA_COUNT));
1631
+ const [sorting, setSorting] = React.useState<SortingState>([]);
1632
+ const [pagination, setPagination] = React.useState<PaginationState>({
1633
+ pageIndex: 0,
1634
+ pageSize: PAGE_SIZE,
1635
+ });
1636
+
1637
+ const table = useReactTable({
1638
+ columns,
1639
+ data,
1640
+ getCoreRowModel: getCoreRowModel(),
1641
+ getPaginationRowModel: getPaginationRowModel(),
1642
+ getSortedRowModel: getSortedRowModel(),
1643
+ onPaginationChange: setPagination,
1644
+ onSortingChange: setSorting,
1645
+ state: { pagination, sorting },
1646
+ });
1647
+
1648
+ return (
1649
+ <div className="app">
1650
+ <Table>
1651
+ <TableHeader>
1652
+ {table.getFlatHeaders().map((header) => (
1653
+ <React.Fragment key={header.id}>
1654
+ {flexRender(header.column.columnDef.header, header.getContext())}
1655
+ </React.Fragment>
1656
+ ))}
1657
+ </TableHeader>
1658
+ <tbody>
1659
+ {table.getRowModel().rows.map((row) => (
1660
+ <TableRow key={row.id} shouldShowTooltipOnHover={false}>
1661
+ {row.getVisibleCells().map((cell) => (
1662
+ <React.Fragment key={cell.id}>
1663
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
1664
+ </React.Fragment>
1665
+ ))}
1666
+ </TableRow>
1667
+ ))}
1668
+ </tbody>
1669
+ </Table>
1670
+
1671
+ <PaginationWrapper>
1672
+ <Pagination
1673
+ currentPage={pagination.pageIndex + 1}
1674
+ onNextPage={() => table.nextPage()}
1675
+ onPageChange={(page) => table.setPageIndex(page - 1)}
1676
+ onPreviousPage={() => table.previousPage()}
1677
+ pageSize={pagination.pageSize}
1678
+ sdsStyle="round"
1679
+ totalCount={data.length}
1680
+ />
1681
+ </PaginationWrapper>
1682
+ </div>
1683
+ );
1684
+ }
1685
+
1686
+ export default App;
1687
+ ```
1688
+
1689
+ ### Table with Back-End Pagination
1690
+
1691
+ Only one page of rows exists on the client, so `manualPagination` stops the table from slicing and the row count arrives with the data. The request is faked with a delay, and `keepPreviousData` holds the current page on screen while the next one loads.
1692
+
1693
+ **Example: TableWithBackEndPagination**
1694
+
1695
+ ```tsx
1696
+ // Here the rows for one page are all that exist on the client, so the table cannot
1697
+ // work out how many pages there are: manualPagination tells it not to slice, and the
1698
+ // row count comes back with the data. Pagination is driven by that count instead.
1699
+ //
1700
+ // The fetch is faked with a delay. keepPreviousData holds the previous page on
1701
+ // screen while the next one loads, which is what stops the table from collapsing to
1702
+ // nothing between pages; the body is dimmed while a fetch is in flight.
1703
+
1704
+ import {
1705
+ CellBasic,
1706
+ CellComponent,
1707
+ CellHeader,
1708
+ Icon,
1709
+ Pagination,
1710
+ Table,
1711
+ TableHeader,
1712
+ TableRow,
1713
+ Tag,
1714
+ fontBodyXs,
1715
+ getSemanticColors,
1716
+ getSpaces,
1717
+ type CellHeaderDirection,
1718
+ type CommonThemeProps,
1719
+ } from "@czi-sds/components";
1720
+ import styled from "@emotion/styled";
1721
+ import { faker } from "@faker-js/faker";
1722
+ import {
1723
+ QueryClient,
1724
+ QueryClientProvider,
1725
+ keepPreviousData,
1726
+ useQuery,
1727
+ } from "@tanstack/react-query";
1728
+ import {
1729
+ flexRender,
1730
+ getCoreRowModel,
1731
+ useReactTable,
1732
+ type CellContext,
1733
+ type ColumnDef,
1734
+ type HeaderContext,
1735
+ type PaginationState,
1736
+ type SortingState,
1737
+ } from "@tanstack/react-table";
1738
+ import * as React from "react";
1739
+
1740
+ const DATA_COUNT = 198;
1741
+ const PAGE_SIZE = 5;
1742
+
1743
+ type Person = {
1744
+ age: number;
1745
+ firstName: string;
1746
+ lastName: string;
1747
+ progress: number;
1748
+ status: "relationship" | "complicated" | "single";
1749
+ visits: number;
1750
+ };
1751
+
1752
+ const STATUSES: Person["status"][] = ["relationship", "complicated", "single"];
1753
+
1754
+ const STATUS_COLORS = {
1755
+ complicated: "negative",
1756
+ relationship: "positive",
1757
+ single: "notice",
1758
+ } as const;
1759
+
1760
+ const ROWS: Person[] = Array.from({ length: DATA_COUNT }, () => ({
1761
+ age: faker.number.int({ max: 80, min: 18 }),
1762
+ firstName: faker.person.firstName(),
1763
+ lastName: faker.person.lastName(),
1764
+ progress: faker.number.int(100),
1765
+ status: faker.helpers.shuffle(STATUSES)[0]!,
1766
+ visits: faker.number.int(1000),
1767
+ }));
1768
+
1769
+ // Stands in for an endpoint that takes a page and sorts server-side.
1770
+ async function fetchPage({
1771
+ pageIndex,
1772
+ pageSize,
1773
+ sorting,
1774
+ }: PaginationState & { sorting: SortingState }) {
1775
+ await new Promise((resolve) => setTimeout(resolve, 600));
1776
+
1777
+ const rows = [...ROWS];
1778
+ const sort = sorting[0];
1779
+
1780
+ if (sort) {
1781
+ const key = sort.id as keyof Person;
1782
+ rows.sort((a, b) => {
1783
+ if (a[key] === b[key]) return 0;
1784
+ const ascending = a[key] > b[key] ? 1 : -1;
1785
+ return sort.desc ? -ascending : ascending;
1786
+ });
1787
+ }
1788
+
1789
+ return {
1790
+ rows: rows.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize),
1791
+ totalRowCount: rows.length,
1792
+ };
1793
+ }
1794
+
1795
+ const TableBody = styled.tbody<{ isFetching: boolean }>`
1796
+ ${(props) => (props.isFetching ? "filter: grayscale(1); opacity: 0.4;" : "")}
1797
+ transition: opacity 150ms ease-out;
1798
+ `;
1799
+
1800
+ const Footer = styled.div<CommonThemeProps>`
1801
+ ${(props) => {
1802
+ const spaces = getSpaces(props);
1803
+
1804
+ return `
1805
+ align-items: center;
1806
+ display: flex;
1807
+ gap: ${spaces?.m}px;
1808
+ justify-content: flex-end;
1809
+ margin-top: ${spaces?.l}px;
1810
+ `;
1811
+ }}
1812
+ `;
1813
+
1814
+ // A div rather than a p, because Icon renders a div and a p cannot contain one.
1815
+ const FetchingNote = styled.div<CommonThemeProps>`
1816
+ ${fontBodyXs}
1817
+
1818
+ ${(props) => {
1819
+ const semanticColors = getSemanticColors(props);
1820
+ const spaces = getSpaces(props);
1821
+
1822
+ return `
1823
+ align-items: center;
1824
+ color: ${semanticColors?.base?.textSecondary};
1825
+ display: flex;
1826
+ gap: ${spaces?.xxs}px;
1827
+ margin: 0;
1828
+ margin-right: auto;
1829
+ `;
1830
+ }}
1831
+ `;
1832
+
1833
+ function sortableHeader(header: HeaderContext<Person, unknown>, name: string) {
1834
+ const sorted = header.column.getIsSorted();
1835
+ const isNumeric = header.column.id === "progress";
1836
+
1837
+ return (
1838
+ <CellHeader
1839
+ active={!!sorted}
1840
+ colSpan={header.header.colSpan}
1841
+ direction={sorted ? (sorted as CellHeaderDirection) : undefined}
1842
+ horizontalAlign={isNumeric ? "right" : "left"}
1843
+ hover
1844
+ onClick={header.column.getToggleSortingHandler()}
1845
+ shouldShowTooltipOnHover
1846
+ tooltipProps={{ enterDelay: 750 }}
1847
+ tooltipText={`Sort by ${name.toLowerCase()}`}
1848
+ >
1849
+ {name}
1850
+ </CellHeader>
1851
+ );
1852
+ }
1853
+
1854
+ function textCell(cell: CellContext<Person, unknown>) {
1855
+ return (
1856
+ <CellBasic
1857
+ horizontalAlign={cell.column.id === "progress" ? "right" : "left"}
1858
+ primaryText={String(cell.getValue())}
1859
+ shouldShowTooltipOnHover={false}
1860
+ verticalAlign="center"
1861
+ />
1862
+ );
1863
+ }
1864
+
1865
+ const columns: ColumnDef<Person>[] = [
1866
+ {
1867
+ accessorKey: "firstName",
1868
+ cell: textCell,
1869
+ header: (header) => sortableHeader(header, "First Name"),
1870
+ },
1871
+ {
1872
+ accessorKey: "lastName",
1873
+ cell: textCell,
1874
+ header: (header) => sortableHeader(header, "Last Name"),
1875
+ },
1876
+ {
1877
+ accessorKey: "age",
1878
+ cell: textCell,
1879
+ header: (header) => sortableHeader(header, "Age"),
1880
+ },
1881
+ {
1882
+ accessorKey: "visits",
1883
+ cell: textCell,
1884
+ header: (header) => sortableHeader(header, "Visits"),
1885
+ },
1886
+ {
1887
+ accessorKey: "status",
1888
+ cell: (cell) => {
1889
+ const status = cell.getValue() as Person["status"];
1890
+
1891
+ return (
1892
+ <CellComponent verticalAlign="center">
1893
+ <Tag
1894
+ color={STATUS_COLORS[status]}
1895
+ hover={false}
1896
+ label={status}
1897
+ sdsStyle="rounded"
1898
+ sdsType="secondary"
1899
+ />
1900
+ </CellComponent>
1901
+ );
1902
+ },
1903
+ header: (header) => sortableHeader(header, "Status"),
1904
+ },
1905
+ {
1906
+ accessorKey: "progress",
1907
+ cell: textCell,
1908
+ header: (header) => sortableHeader(header, "Profile Progress"),
1909
+ },
1910
+ ];
1911
+
1912
+ const EMPTY_ROWS: Person[] = [];
1913
+
1914
+ function PagedTable() {
1915
+ const [sorting, setSorting] = React.useState<SortingState>([]);
1916
+ const [pagination, setPagination] = React.useState<PaginationState>({
1917
+ pageIndex: 0,
1918
+ pageSize: PAGE_SIZE,
1919
+ });
1920
+
1921
+ const { data, isFetching } = useQuery({
1922
+ placeholderData: keepPreviousData,
1923
+ queryFn: () => fetchPage({ ...pagination, sorting }),
1924
+ queryKey: ["table-page", pagination, sorting],
1925
+ });
1926
+
1927
+ const table = useReactTable({
1928
+ columns,
1929
+ data: data?.rows ?? EMPTY_ROWS,
1930
+ getCoreRowModel: getCoreRowModel(),
1931
+ manualPagination: true,
1932
+ manualSorting: true,
1933
+ onPaginationChange: setPagination,
1934
+ onSortingChange: setSorting,
1935
+ rowCount: data?.totalRowCount ?? 0,
1936
+ state: { pagination, sorting },
1937
+ });
1938
+
1939
+ return (
1940
+ <>
1941
+ <Table>
1942
+ <TableHeader>
1943
+ {table.getFlatHeaders().map((header) => (
1944
+ <React.Fragment key={header.id}>
1945
+ {flexRender(header.column.columnDef.header, header.getContext())}
1946
+ </React.Fragment>
1947
+ ))}
1948
+ </TableHeader>
1949
+ <TableBody isFetching={isFetching}>
1950
+ {table.getRowModel().rows.map((row) => (
1951
+ <TableRow key={row.id} shouldShowTooltipOnHover={false}>
1952
+ {row.getVisibleCells().map((cell) => (
1953
+ <React.Fragment key={cell.id}>
1954
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
1955
+ </React.Fragment>
1956
+ ))}
1957
+ </TableRow>
1958
+ ))}
1959
+ </TableBody>
1960
+ </Table>
1961
+
1962
+ <Footer>
1963
+ {isFetching && (
1964
+ <FetchingNote>
1965
+ <Icon sdsIcon="Loading" sdsSize="s" />
1966
+ Fetching page {pagination.pageIndex + 1}…
1967
+ </FetchingNote>
1968
+ )}
1969
+ <Pagination
1970
+ currentPage={pagination.pageIndex + 1}
1971
+ onNextPage={() => table.nextPage()}
1972
+ onPageChange={(page) => table.setPageIndex(page - 1)}
1973
+ onPreviousPage={() => table.previousPage()}
1974
+ pageSize={pagination.pageSize}
1975
+ sdsStyle="round"
1976
+ totalCount={data?.totalRowCount ?? 0}
1977
+ />
1978
+ </Footer>
1979
+ </>
1980
+ );
1981
+ }
1982
+
1983
+ const queryClient = new QueryClient();
1984
+
1985
+ function App() {
1986
+ return (
1987
+ <div className="app">
1988
+ <QueryClientProvider client={queryClient}>
1989
+ <PagedTable />
1990
+ </QueryClientProvider>
1991
+ </div>
1992
+ );
1993
+ }
1994
+
1995
+ export default App;
1996
+ ```
1997
+
1998
+ ## Props
1999
+
2000
+ Table is a styled table element: full width, collapsed borders, automatic layout. Its only prop is `children`, and anything else you pass lands on the table element. Compose it the way you would plain HTML: a TableHeader for the head, a `tbody` of TableRows for the body.