@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.
- package/README.md +2 -1
- package/data/component-docs/Accordion.md +291 -0
- package/data/component-docs/Autocomplete.md +525 -0
- package/data/component-docs/Banner.md +231 -0
- package/data/component-docs/Button.md +224 -0
- package/data/component-docs/ButtonDropdown.md +256 -0
- package/data/component-docs/ButtonGroup.md +320 -0
- package/data/component-docs/ButtonToggle.md +397 -0
- package/data/component-docs/Callout.md +268 -0
- package/data/component-docs/CellBasic.md +122 -0
- package/data/component-docs/CellComponent.md +63 -0
- package/data/component-docs/CellHeader.md +128 -0
- package/data/component-docs/ComplexFilter.md +294 -0
- package/data/component-docs/ContentCard.md +406 -0
- package/data/component-docs/Dialog.md +651 -0
- package/data/component-docs/Dropdown.md +155 -0
- package/data/component-docs/DropdownMenu.md +949 -0
- package/data/component-docs/HeatmapChart.md +782 -0
- package/data/component-docs/Hero.md +363 -0
- package/data/component-docs/Icon.md +283 -0
- package/data/component-docs/InputCheckbox.md +205 -0
- package/data/component-docs/InputDropdown.md +482 -0
- package/data/component-docs/InputRadio.md +173 -0
- package/data/component-docs/InputSearch.md +81 -0
- package/data/component-docs/InputSlider.md +189 -0
- package/data/component-docs/InputText.md +110 -0
- package/data/component-docs/InputToggle.md +141 -0
- package/data/component-docs/IntentMessage.md +293 -0
- package/data/component-docs/Legend.md +442 -0
- package/data/component-docs/Link.md +23 -0
- package/data/component-docs/List.md +300 -0
- package/data/component-docs/LoadingIndicator.md +198 -0
- package/data/component-docs/Menu.md +298 -0
- package/data/component-docs/MenuItem.md +100 -0
- package/data/component-docs/NavigationFooter.md +165 -0
- package/data/component-docs/NavigationHeader.md +500 -0
- package/data/component-docs/NavigationJumpTo.md +414 -0
- package/data/component-docs/Notification.md +288 -0
- package/data/component-docs/Pagination.md +67 -0
- package/data/component-docs/Panel.md +636 -0
- package/data/component-docs/Popover.md +448 -0
- package/data/component-docs/PreComposedTable.md +2947 -0
- package/data/component-docs/SegmentedControl.md +396 -0
- package/data/component-docs/StackedBarChart.md +475 -0
- package/data/component-docs/Table.md +2000 -0
- package/data/component-docs/TableHeader.md +53 -0
- package/data/component-docs/TableRow.md +74 -0
- package/data/component-docs/Tabs.md +455 -0
- package/data/component-docs/Tag.md +647 -0
- package/data/component-docs/TagFilter.md +131 -0
- package/data/component-docs/Tooltip.md +561 -0
- package/data/component-docs/TooltipCondensed.md +203 -0
- package/data/component-docs/TooltipTable.md +145 -0
- package/data/component-docs/index.json +262 -0
- package/data/component-list.json +2 -5
- package/data/component-props/Accordion.json +31 -11
- package/data/component-props/Alert.json +3 -3
- package/data/component-props/Autocomplete.json +150 -15
- package/data/component-props/Banner.json +31 -17
- package/data/component-props/Button.json +45 -20
- package/data/component-props/ButtonDropdown.json +36 -17
- package/data/component-props/ButtonDropdownLegacy.json +11 -11
- package/data/component-props/ButtonGroup.json +32 -11
- package/data/component-props/ButtonIcon.json +10 -10
- package/data/component-props/ButtonLegacy.json +19 -19
- package/data/component-props/ButtonToggle.json +32 -19
- package/data/component-props/ButtonToggleLegacy.json +11 -11
- package/data/component-props/Callout.json +58 -17
- package/data/component-props/CellBasic.json +94 -41
- package/data/component-props/CellComponent.json +23 -7
- package/data/component-props/CellHeader.json +62 -19
- package/data/component-props/ComplexFilter.json +52 -21
- package/data/component-props/ContentCard.json +70 -47
- package/data/component-props/Dialog.json +47 -14
- package/data/component-props/Dropdown.json +68 -31
- package/data/component-props/DropdownMenu.json +128 -23
- package/data/component-props/HeatmapChart.json +88 -12
- package/data/component-props/Hero.json +80 -39
- package/data/component-props/Icon.json +20 -9
- package/data/component-props/InputCheckbox.json +36 -14
- package/data/component-props/InputDropdown.json +75 -27
- package/data/component-props/InputRadio.json +31 -13
- package/data/component-props/InputSearch.json +41 -18
- package/data/component-props/InputSlider.json +46 -19
- package/data/component-props/InputText.json +26 -19
- package/data/component-props/InputToggle.json +26 -9
- package/data/component-props/IntentMessage.json +38 -9
- package/data/component-props/Link.json +13 -7
- package/data/component-props/LoadingIndicator.json +11 -3
- package/data/component-props/MenuItem.json +38 -19
- package/data/component-props/MenuSelect.json +8 -8
- package/data/component-props/NavigationFooter.json +49 -19
- package/data/component-props/NavigationHeader.json +150 -37
- package/data/component-props/NavigationJumpTo.json +33 -7
- package/data/component-props/Notification.json +39 -21
- package/data/component-props/Pagination.json +39 -11
- package/data/component-props/Panel.json +62 -9
- package/data/component-props/Popover.json +80 -7
- package/data/component-props/PreComposedTable.json +99 -27
- package/data/component-props/SegmentedControl.json +42 -3
- package/data/component-props/StackedBarChart.json +116 -42
- package/data/component-props/TableRow.json +38 -19
- package/data/component-props/Tabs.json +66 -9
- package/data/component-props/Tag.json +34 -17
- package/data/component-props/TagFilter.json +10 -3
- package/data/component-props/Tooltip.json +49 -18
- package/data/component-props/TooltipCondensed.json +18 -9
- package/data/component-props/TooltipTable.json +17 -7
- package/data/resources/{zeroheight-rules.md → documentation-rules.md} +43 -30
- package/data/resources/figma-rules.md +2 -2
- package/dist/stdio.js +112 -208
- package/package.json +9 -5
- package/data/component-props/ButtonDropdownV2.json +0 -34
- package/data/component-props/ButtonNew.json +0 -47
- package/data/component-props/ButtonToggleV2.json +0 -44
- package/data/component-props/ButtonV2.json +0 -43
- package/data/zeroheight-cache.json +0 -10259
- package/data/zeroheight-exports/Accordion.md +0 -774
- package/data/zeroheight-exports/Banner.md +0 -691
- package/data/zeroheight-exports/Borders.md +0 -117
- package/data/zeroheight-exports/Buttons.md +0 -1854
- package/data/zeroheight-exports/Callout.md +0 -931
- package/data/zeroheight-exports/Colors.md +0 -1755
- package/data/zeroheight-exports/Content-Card.md +0 -309
- package/data/zeroheight-exports/Contributing-to-SDS.md +0 -98
- package/data/zeroheight-exports/Control-Inputs.md +0 -2818
- package/data/zeroheight-exports/Corners.md +0 -104
- package/data/zeroheight-exports/Cover-page.md +0 -3
- package/data/zeroheight-exports/Dialog.md +0 -1367
- package/data/zeroheight-exports/Downloads.md +0 -236
- package/data/zeroheight-exports/Drop-Shadows.md +0 -79
- package/data/zeroheight-exports/Dropdown-Input.md +0 -1776
- package/data/zeroheight-exports/Dropdown-Menus.md +0 -913
- package/data/zeroheight-exports/Element-Status-Tracker.md +0 -267
- package/data/zeroheight-exports/Field-Inputs.md +0 -1405
- package/data/zeroheight-exports/Filters.md +0 -1111
- package/data/zeroheight-exports/Getting-Started.md +0 -1157
- package/data/zeroheight-exports/Hero.md +0 -414
- package/data/zeroheight-exports/Icons.md +0 -2067
- package/data/zeroheight-exports/Intent.md +0 -86
- package/data/zeroheight-exports/Introduction-to-Bases.md +0 -35
- package/data/zeroheight-exports/Introduction-to-Cells.md +0 -35
- package/data/zeroheight-exports/Introduction-to-DNA.md +0 -35
- package/data/zeroheight-exports/Introduction-to-Genes.md +0 -35
- package/data/zeroheight-exports/Introduction.md +0 -115
- package/data/zeroheight-exports/Link.md +0 -432
- package/data/zeroheight-exports/Lists.md +0 -986
- package/data/zeroheight-exports/Loading-Indicator.md +0 -41
- package/data/zeroheight-exports/Navigation.md +0 -1190
- package/data/zeroheight-exports/Notification.md +0 -1038
- package/data/zeroheight-exports/Panel.md +0 -1030
- package/data/zeroheight-exports/README.md +0 -122
- package/data/zeroheight-exports/Responsive-Design.md +0 -330
- package/data/zeroheight-exports/Search-Input.md +0 -91
- package/data/zeroheight-exports/Search.md +0 -822
- package/data/zeroheight-exports/Segmented-Control.md +0 -609
- package/data/zeroheight-exports/Spacing.md +0 -297
- package/data/zeroheight-exports/Table.md +0 -5345
- package/data/zeroheight-exports/Tabs.md +0 -1324
- package/data/zeroheight-exports/Tags.md +0 -1545
- package/data/zeroheight-exports/Text-Link.md +0 -268
- package/data/zeroheight-exports/Theming.md +0 -362
- package/data/zeroheight-exports/Tooltips.md +0 -2015
- 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.
|