-
Notifications
You must be signed in to change notification settings - Fork 58
/
Copy pathModularTable.tsx
294 lines (272 loc) · 9.14 KB
/
ModularTable.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import React, { ReactNode, HTMLProps, useMemo, isValidElement } from "react";
import {
TableCellProps,
TableHeaderProps,
TableRowProps,
useTable,
useSortBy,
} from "react-table";
import type {
Column,
UseTableOptions,
Cell,
Row,
HeaderGroup,
} from "react-table";
import { PropsWithSpread, SortDirection } from "types";
import Table from "../Table";
import TableRow from "../TableRow";
import TableHeader from "../TableHeader";
import TableCell from "../TableCell";
import Icon from "../Icon";
export type Props<D extends Record<string, unknown>> = PropsWithSpread<
{
/**
* The columns of the table.
*/
columns: Column<D>[];
/**
* The data of the table.
*/
data: D[];
/**
* A message to display if data is empty.
*/
emptyMsg?: string;
/**
* Optional extra row to display underneath the main table content.
*/
footer?: ReactNode;
/**
* Optional argument to make the tables be sortable and use the `useSortBy` plugin.
*/
sortable?: boolean;
/**
* This function is used to resolve any props needed for a particular column's header cell.
*/
getHeaderProps?: (
header: HeaderGroup<D>,
) => Partial<TableHeaderProps & HTMLProps<HTMLTableHeaderCellElement>>;
/**
* This function is used to resolve any props needed for a particular row.
*/
getRowProps?: (
row: Row<D>,
) => Partial<TableRowProps & HTMLProps<HTMLTableRowElement>>;
/**
* This function is used to resolve any props needed for a particular cell.
*/
getCellProps?: (
cell: Cell<D>,
) => Partial<TableCellProps & HTMLProps<HTMLTableCellElement>>;
getRowId?: UseTableOptions<D>["getRowId"];
/**
* The column that the table will be sorted by (this should match a cell selector).
*/
initialSortColumn?: string;
/**
* The direction of the initial sort.
*/
initialSortDirection?: "ascending" | "descending";
/**
* Whether the sort by needs to be reset after each data change.
*/
autoResetSortBy?: boolean;
},
HTMLProps<HTMLTableElement>
>;
const generateCell = <D extends Record<string, unknown>>(
cell: Cell<D>,
getCellProps: Props<D>["getCellProps"],
) => {
const hasColumnIcon = cell.column.getCellIcon;
const iconName = hasColumnIcon && cell.column.getCellIcon?.(cell);
return (
<TableCell
{...cell.getCellProps([
{
className: cell.column.className,
},
{
className: hasColumnIcon ? "p-table__cell--icon-placeholder" : "",
},
{ ...getCellProps?.(cell) },
])}
>
{iconName && <Icon name={iconName} />}
{cell.render("Cell")}
</TableCell>
);
};
const generateRows = <D extends Record<string, unknown>>(
rows: Row<D>[],
prepareRow: (row: Row<D>) => void,
getRowProps: Props<D>["getRowProps"],
getCellProps: Props<D>["getCellProps"],
) => {
let tableRows: ReactNode[] = [];
rows.forEach((row) => {
// This function is responsible for lazily preparing a row for rendering.
// Any row that you intend to render in your table needs to be passed to this function before every render.
// see: https://react-table.tanstack.com/docs/api/useTable#instance-properties
prepareRow(row);
tableRows.push(
<TableRow {...row.getRowProps(getRowProps?.(row))}>
{row.cells.map((cell) => generateCell<D>(cell, getCellProps))}
</TableRow>,
);
if (row.subRows?.length) {
tableRows = tableRows.concat(
generateRows<D>(row.subRows, prepareRow, getRowProps, getCellProps),
);
}
});
return tableRows;
};
/**
This is a [React](https://reactjs.org/) component to support many table use cases.
ModularTable components accepts `columns` and `data` arguments in the same format as [`useTable`](https://react-table.tanstack.com/docs/api/useTable) hook of the ReactTable library.
`columns` - The core columns configuration object for the entire table. https://react-table.tanstack.com/docs/api/useTable#column-options
`data` - The data array that you want to display on the table.
### Important note!
Values passed to both of these params have to me memoized (for example via{" "}
<code>React.useMemo</code>). Memoization ensures that our data isn't recreated
on every render. If we didn't use <code>React.useMemo</code>, the table would
think it was receiving new data on every render and attempt to recalulate a
lot of logic every single time.
#### Custom column options
In addition to standard column propeties from [`useTable`](https://react-table.tanstack.com/docs/api/useTable) `ModularTable` accepts some custom properties.
##### Class names
Custom `className` can be used to provide a specific CSS class name that will be added to all cells in given column.
You can also provide `getHeaderProps`, `getRowProps` and `getCellProps` to resolve additional custom props for a specific element. More on this in [`useTable - cell properties`](https://react-table.tanstack.com/docs/api/useTable#cell-properties).
```js
getCellProps={({ value, column }) => ({
className: `table__cell--${column.id} ${value === "1 minute" ? "p-heading--5" : ""}`,
})}
columns = {
Header: "Hidden on mobile",
accessor: "example",
className: "u-hide--small"
}
```
##### Icons
To show an icon in the cells of a column `getCellIcon` function should be defined. The function takes a cell data as an argument and should return one of built in icons (from the `ICONS` const), a string with a custom icon name, or `false` if no icon should be shown.
The `ICONS` const contains all [the Vanilla built in icons](https://vanillaframework.io/docs/patterns/icons) such as "plus", "minus", "success", "error", etc.
Product specific icons can be used as well, assuming that the product provides the necessary CSS styling and the icon follows the Vanilla naming convention `p-icon--{name}`.
```js
columns = {
Header: "With icons",
accessor: "status",
getCellIcon: ({ value }) => {
return value === "released" ? ICONS.success : false;
},
};
```
*/
function ModularTable<D extends Record<string, unknown>>({
data,
columns,
emptyMsg,
footer,
sortable,
getHeaderProps,
getRowProps,
getCellProps,
getRowId,
initialSortColumn,
initialSortDirection,
autoResetSortBy = false,
...props
}: Props<D>): JSX.Element {
const sortBy = useMemo(
() =>
initialSortColumn
? [
{
id: initialSortColumn,
desc: initialSortDirection === "descending",
},
]
: [],
[initialSortColumn, initialSortDirection],
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable<D>(
{
columns,
data,
getRowId: getRowId || undefined,
initialState: {
sortBy,
},
autoResetSortBy,
},
sortable ? useSortBy : undefined,
);
const showEmpty: boolean = !!emptyMsg && (!rows || rows.length === 0);
// Function returns whether table can be sorted by a specific column.
// Returns true if sorting is enabled for the column and there is text
// or JSX provided for the header, otherwise returns false.
const isColumnSortable = (column: HeaderGroup<D>) =>
column.canSort &&
(isValidElement(column.Header) ||
((typeof column.Header === "string" ||
typeof column.Header === "number") &&
!!String(column.Header).trim()));
const getColumnSortDirection = (column: HeaderGroup<D>): SortDirection => {
if (!isColumnSortable(column)) {
return undefined;
}
if (!column.isSorted) {
return "none";
}
return column.isSortedDesc ? "descending" : "ascending";
};
return (
<Table {...getTableProps()} {...props}>
<thead>
{headerGroups.map((headerGroup, i) => (
<TableRow {...headerGroup.getHeaderGroupProps()} key={i}>
{headerGroup.headers.map((column, j) => (
<TableHeader
key={j}
sort={getColumnSortDirection(column)}
{...column.getHeaderProps([
{
className: column.className,
},
{
className: column.getCellIcon
? "p-table__cell--icon-placeholder"
: "",
},
{ ...getHeaderProps?.(column) },
// Only call this if we want it to be sortable too.
sortable && isColumnSortable(column)
? column.getSortByToggleProps({ title: undefined })
: {},
])}
>
{column.render("Header")}
</TableHeader>
))}
</TableRow>
))}
</thead>
<tbody {...getTableBodyProps()}>
{generateRows(rows, prepareRow, getRowProps, getCellProps)}
{showEmpty && (
<TableRow>
<TableCell colSpan={columns.length}>{emptyMsg}</TableCell>
</TableRow>
)}
{footer && (
<TableRow>
<TableCell colSpan={columns.length}>{footer}</TableCell>
</TableRow>
)}
</tbody>
</Table>
);
}
export default ModularTable;