@varialkit/table 0.1.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/docs.md +617 -0
- package/examples/index.tsx +1 -0
- package/examples.tsx +1177 -0
- package/package.json +32 -0
- package/src/Table.scss +531 -0
- package/src/Table.tsx +1380 -0
- package/src/Table.types.ts +158 -0
- package/src/index.ts +2 -0
package/examples.tsx
ADDED
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@solara/button';
|
|
4
|
+
import { Checkbox } from '@solara/checkbox';
|
|
5
|
+
|
|
6
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import type { ReactElement } from 'react';
|
|
8
|
+
import { createColumnHelper, flexRender, getCoreRowModel, getExpandedRowModel, getPaginationRowModel, Row, Table as TanStackTable, ColumnDef } from '@tanstack/react-table';
|
|
9
|
+
import { Table } from './src/Table';
|
|
10
|
+
import type { TableDensity, TableGridType, TableLayoutMode } from './src/Table.types';
|
|
11
|
+
|
|
12
|
+
type PersonRow = {
|
|
13
|
+
avatar: string;
|
|
14
|
+
name: string;
|
|
15
|
+
role: string;
|
|
16
|
+
status: string;
|
|
17
|
+
lastActive: string;
|
|
18
|
+
subRows?: PersonRow[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const columnHelper = createColumnHelper<PersonRow>();
|
|
22
|
+
|
|
23
|
+
const data: PersonRow[] = [
|
|
24
|
+
{ avatar: 'https://i.pravatar.cc/600?img=11', name: 'Avery Miles', role: 'Product Design', status: 'Active', lastActive: '2 hours ago' },
|
|
25
|
+
{ avatar: 'https://i.pravatar.cc/600?img=22', name: 'Ravi Patel', role: 'Engineering', status: 'Active', lastActive: '1 day ago' },
|
|
26
|
+
{ avatar: 'https://i.pravatar.cc/600?img=33', name: 'Jordan Lee', role: 'Operations', status: 'Away', lastActive: '3 days ago' },
|
|
27
|
+
{ avatar: 'https://i.pravatar.cc/600?img=44', name: 'Maya Chen', role: 'Research', status: 'Active', lastActive: 'Just now' },
|
|
28
|
+
{ avatar: 'https://i.pravatar.cc/600?img=55', name: 'Logan Kim', role: 'Sales', status: 'Inactive', lastActive: '2 weeks ago' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const columns = [
|
|
32
|
+
columnHelper.accessor('avatar', {
|
|
33
|
+
header: 'Photo',
|
|
34
|
+
cell: (info) => (
|
|
35
|
+
<img
|
|
36
|
+
src={info.getValue()}
|
|
37
|
+
alt=""
|
|
38
|
+
style={{
|
|
39
|
+
width: '48px',
|
|
40
|
+
height: '48px',
|
|
41
|
+
borderRadius: '999px',
|
|
42
|
+
objectFit: 'cover',
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
),
|
|
46
|
+
}),
|
|
47
|
+
columnHelper.accessor('name', {
|
|
48
|
+
header: 'Name',
|
|
49
|
+
cell: (info) => info.getValue(),
|
|
50
|
+
}),
|
|
51
|
+
columnHelper.accessor('role', {
|
|
52
|
+
header: 'Role',
|
|
53
|
+
cell: (info) => info.getValue(),
|
|
54
|
+
}),
|
|
55
|
+
columnHelper.accessor('status', {
|
|
56
|
+
header: 'Status',
|
|
57
|
+
cell: (info) => info.getValue(),
|
|
58
|
+
}),
|
|
59
|
+
columnHelper.accessor('lastActive', {
|
|
60
|
+
header: 'Last Active',
|
|
61
|
+
cell: (info) => info.getValue(),
|
|
62
|
+
}),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const makeLargeData = (count: number): PersonRow[] =>
|
|
66
|
+
Array.from({ length: count }, (_, index) => ({
|
|
67
|
+
avatar: `https://i.pravatar.cc/600?img=${(index % 70) + 1}`,
|
|
68
|
+
name: `Person ${index + 1}`,
|
|
69
|
+
role: ['Engineering', 'Design', 'Operations', 'Sales'][index % 4],
|
|
70
|
+
status: ['Active', 'Away', 'Inactive'][index % 3],
|
|
71
|
+
lastActive: `${(index % 24) + 1} hours ago`,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
function DensityMenuExample() {
|
|
75
|
+
const [density, setDensity] = useState<TableDensity>('standard');
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Table
|
|
79
|
+
data={data}
|
|
80
|
+
columns={columns as any}
|
|
81
|
+
// Density lives in state so the column controls menu can update it.
|
|
82
|
+
density={density}
|
|
83
|
+
enableColumnControls
|
|
84
|
+
onDensityChange={setDensity}
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function GridDefaultExample() {
|
|
90
|
+
const [layoutMode, setLayoutMode] = useState<TableLayoutMode>('grid');
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Table
|
|
94
|
+
data={data}
|
|
95
|
+
columns={columns as any}
|
|
96
|
+
// Start in grid mode and allow switching to list from the built-in action menu.
|
|
97
|
+
layoutMode={layoutMode}
|
|
98
|
+
onLayoutModeChange={setLayoutMode}
|
|
99
|
+
enableLayoutToggle
|
|
100
|
+
enableColumnControls
|
|
101
|
+
gridType="grid"
|
|
102
|
+
// Grid can treat image/media as a full-bleed top area while details remain toggled fields.
|
|
103
|
+
gridColumnConfig={{
|
|
104
|
+
avatar: { variant: 'media', unpadded: true, showLabel: false },
|
|
105
|
+
name: { hiddenByDefault: true },
|
|
106
|
+
role: { hiddenByDefault: true },
|
|
107
|
+
status: { hiddenByDefault: true },
|
|
108
|
+
lastActive: { hiddenByDefault: true },
|
|
109
|
+
}}
|
|
110
|
+
// Grid renderer map lets each column control its own visual output.
|
|
111
|
+
gridCellRenderers={{
|
|
112
|
+
avatar: (cell, row) => (
|
|
113
|
+
<img
|
|
114
|
+
src={String(cell.getValue())}
|
|
115
|
+
alt={`${String(row.original.name)} profile`}
|
|
116
|
+
style={{ width: '100%', height: '100%', minHeight: '180px', objectFit: 'cover' }}
|
|
117
|
+
/>
|
|
118
|
+
),
|
|
119
|
+
status: (cell) => {
|
|
120
|
+
const value = String(cell.getValue());
|
|
121
|
+
const tone =
|
|
122
|
+
value === 'Active'
|
|
123
|
+
? '#16a34a'
|
|
124
|
+
: value === 'Away'
|
|
125
|
+
? '#f59e0b'
|
|
126
|
+
: '#64748b';
|
|
127
|
+
return (
|
|
128
|
+
<span
|
|
129
|
+
style={{
|
|
130
|
+
display: 'inline-flex',
|
|
131
|
+
alignItems: 'center',
|
|
132
|
+
borderRadius: '999px',
|
|
133
|
+
padding: '2px 10px',
|
|
134
|
+
backgroundColor: `${tone}1A`,
|
|
135
|
+
color: tone,
|
|
136
|
+
fontWeight: 600,
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{value}
|
|
140
|
+
</span>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
lastActive: (cell) => (
|
|
144
|
+
<span style={{ color: 'var(--color-text-secondary)' }}>
|
|
145
|
+
{String(cell.getValue())}
|
|
146
|
+
</span>
|
|
147
|
+
),
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function GridMasonryExample() {
|
|
154
|
+
const [layoutMode, setLayoutMode] = useState<TableLayoutMode>('grid');
|
|
155
|
+
const [gridType, setGridType] = useState<TableGridType>('masonry');
|
|
156
|
+
const masonryImageSizes = [
|
|
157
|
+
{ width: 720, height: 420 },
|
|
158
|
+
{ width: 640, height: 860 },
|
|
159
|
+
{ width: 800, height: 500 },
|
|
160
|
+
{ width: 600, height: 920 },
|
|
161
|
+
{ width: 760, height: 460 },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Table
|
|
166
|
+
data={data}
|
|
167
|
+
columns={columns as any}
|
|
168
|
+
layoutMode={layoutMode}
|
|
169
|
+
onLayoutModeChange={setLayoutMode}
|
|
170
|
+
enableLayoutToggle
|
|
171
|
+
enableColumnControls
|
|
172
|
+
// View menu supports List, Standard grid, and Masonry grid.
|
|
173
|
+
gridType={gridType}
|
|
174
|
+
onGridTypeChange={setGridType}
|
|
175
|
+
gridColumnConfig={{
|
|
176
|
+
avatar: { variant: 'media', unpadded: true, showLabel: false },
|
|
177
|
+
name: { hiddenByDefault: true },
|
|
178
|
+
role: { hiddenByDefault: true },
|
|
179
|
+
status: { hiddenByDefault: true },
|
|
180
|
+
lastActive: { hiddenByDefault: true },
|
|
181
|
+
}}
|
|
182
|
+
gridCellRenderers={{
|
|
183
|
+
avatar: (_cell, row) => {
|
|
184
|
+
// Use intentionally different source dimensions so masonry is obvious in docs/storybook.
|
|
185
|
+
const size = masonryImageSizes[row.index % masonryImageSizes.length];
|
|
186
|
+
// Seed by row id + name for stable-but-varied demo images across reloads.
|
|
187
|
+
const seed = `${row.id}-${String(row.original.name).toLowerCase().replace(/\s+/g, '-')}`;
|
|
188
|
+
const src = `https://picsum.photos/seed/${seed}/${size.width}/${size.height}`;
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<img
|
|
192
|
+
src={src}
|
|
193
|
+
alt={`${String(row.original.name)} profile`}
|
|
194
|
+
// In standard grid, fill the media slot; in masonry, preserve natural aspect ratio.
|
|
195
|
+
style={
|
|
196
|
+
gridType === 'masonry'
|
|
197
|
+
? { width: '100%', height: 'auto', display: 'block' }
|
|
198
|
+
: { width: '100%', height: '100%', display: 'block', objectFit: 'cover' }
|
|
199
|
+
}
|
|
200
|
+
/>
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function ListOnlyLayoutExample() {
|
|
209
|
+
return (
|
|
210
|
+
<Table
|
|
211
|
+
data={data}
|
|
212
|
+
columns={columns as any}
|
|
213
|
+
layoutMode="list"
|
|
214
|
+
// List-only surfaces keep the same controls without exposing grid as a mode.
|
|
215
|
+
layoutModeOptions={['list']}
|
|
216
|
+
enableLayoutToggle
|
|
217
|
+
enableColumnControls
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function SelectableRowsExample() {
|
|
223
|
+
const [rowSelection, setRowSelection] = React.useState({});
|
|
224
|
+
|
|
225
|
+
const columnsWithSelect = useMemo(
|
|
226
|
+
() => [
|
|
227
|
+
{
|
|
228
|
+
id: 'select',
|
|
229
|
+
header: function Header({ table }: { table: TanStackTable<PersonRow> }) {
|
|
230
|
+
// Solara Checkbox supports indeterminate directly, so we can mirror TanStack state without refs.
|
|
231
|
+
const isAllRowsSelected = table.getIsAllRowsSelected();
|
|
232
|
+
const isSomeRowsSelected = table.getIsSomeRowsSelected();
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<Checkbox
|
|
236
|
+
aria-label="Select all rows"
|
|
237
|
+
size="small"
|
|
238
|
+
checked={isAllRowsSelected}
|
|
239
|
+
indeterminate={!isAllRowsSelected && isSomeRowsSelected}
|
|
240
|
+
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
241
|
+
/>
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
cell: ({ row }: { row: Row<PersonRow> }) => (
|
|
245
|
+
<Checkbox
|
|
246
|
+
aria-label={`Select row ${row.id}`}
|
|
247
|
+
size="small"
|
|
248
|
+
checked={row.getIsSelected()}
|
|
249
|
+
disabled={!row.getCanSelect()}
|
|
250
|
+
onChange={row.getToggleSelectedHandler()}
|
|
251
|
+
/>
|
|
252
|
+
),
|
|
253
|
+
},
|
|
254
|
+
...columns,
|
|
255
|
+
],
|
|
256
|
+
[]
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<Table
|
|
261
|
+
data={data}
|
|
262
|
+
columns={columnsWithSelect as any}
|
|
263
|
+
tableOptions={{
|
|
264
|
+
getCoreRowModel: getCoreRowModel(),
|
|
265
|
+
enableRowSelection: true,
|
|
266
|
+
state: { rowSelection },
|
|
267
|
+
onRowSelectionChange: setRowSelection,
|
|
268
|
+
}}
|
|
269
|
+
/>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const dataWithSubRows: PersonRow[] = [
|
|
274
|
+
{
|
|
275
|
+
name: 'Avery Miles',
|
|
276
|
+
avatar: 'https://i.pravatar.cc/600?img=11',
|
|
277
|
+
role: 'Product Design',
|
|
278
|
+
status: 'Active',
|
|
279
|
+
lastActive: '2 hours ago',
|
|
280
|
+
subRows: [
|
|
281
|
+
{ avatar: 'https://i.pravatar.cc/600?img=12', name: 'Sub Avery 1', role: 'Sub Role 1', status: 'Active', lastActive: '2 hours ago' },
|
|
282
|
+
{ avatar: 'https://i.pravatar.cc/600?img=13', name: 'Sub Avery 2', role: 'Sub Role 2', status: 'Away', lastActive: '1 day ago' },
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
{ avatar: 'https://i.pravatar.cc/600?img=22', name: 'Ravi Patel', role: 'Engineering', status: 'Active', lastActive: '1 day ago', subRows: [] },
|
|
286
|
+
{
|
|
287
|
+
avatar: 'https://i.pravatar.cc/600?img=33',
|
|
288
|
+
name: 'Jordan Lee',
|
|
289
|
+
role: 'Operations',
|
|
290
|
+
status: 'Away',
|
|
291
|
+
lastActive: '3 days ago',
|
|
292
|
+
subRows: [
|
|
293
|
+
{ avatar: 'https://i.pravatar.cc/600?img=34', name: 'Sub Jordan 1', role: 'Sub Role 3', status: 'Inactive', lastActive: '2 weeks ago' },
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
{ avatar: 'https://i.pravatar.cc/600?img=44', name: 'Maya Chen', role: 'Research', status: 'Active', lastActive: 'Just now', subRows: [] },
|
|
297
|
+
{ avatar: 'https://i.pravatar.cc/600?img=55', name: 'Logan Kim', role: 'Sales', status: 'Inactive', lastActive: '2 weeks ago', subRows: [] },
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
function ExpandableRowsExample() {
|
|
301
|
+
const [expanded, setExpanded] = React.useState({});
|
|
302
|
+
|
|
303
|
+
const columnsWithToggle = useMemo(
|
|
304
|
+
() => [
|
|
305
|
+
{
|
|
306
|
+
id: 'expand',
|
|
307
|
+
header: () => null,
|
|
308
|
+
cell: ({ row }: { row: Row<PersonRow> }) => {
|
|
309
|
+
return row.getCanExpand() ? (
|
|
310
|
+
<Button
|
|
311
|
+
aria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}
|
|
312
|
+
variant="ghost"
|
|
313
|
+
iconOnly={row.getIsExpanded() ? 'arrow_chevron_down_16' : 'arrow_chevron_right_16'}
|
|
314
|
+
onClick={row.getToggleExpandedHandler()}
|
|
315
|
+
/>
|
|
316
|
+
) : null;
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
...columns,
|
|
320
|
+
],
|
|
321
|
+
[]
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<Table
|
|
326
|
+
data={dataWithSubRows}
|
|
327
|
+
columns={columnsWithToggle as any}
|
|
328
|
+
tableOptions={{
|
|
329
|
+
state: { expanded },
|
|
330
|
+
onExpandedChange: setExpanded,
|
|
331
|
+
getSubRows: (row) => row.subRows,
|
|
332
|
+
getCoreRowModel: getCoreRowModel(),
|
|
333
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
334
|
+
}}
|
|
335
|
+
/>
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function PinnedColumnsExample() {
|
|
340
|
+
const [columnPinning, setColumnPinning] = useState({
|
|
341
|
+
left: ['name'],
|
|
342
|
+
right: [],
|
|
343
|
+
});
|
|
344
|
+
const [density, setDensity] = useState<TableDensity>('standard');
|
|
345
|
+
|
|
346
|
+
const pinnedColumns = useMemo(
|
|
347
|
+
() => [
|
|
348
|
+
columnHelper.accessor('name', {
|
|
349
|
+
header: 'Name',
|
|
350
|
+
cell: (info) => info.getValue(),
|
|
351
|
+
}),
|
|
352
|
+
columnHelper.accessor('role', {
|
|
353
|
+
header: 'Role',
|
|
354
|
+
cell: (info) => info.getValue(),
|
|
355
|
+
}),
|
|
356
|
+
columnHelper.accessor('status', {
|
|
357
|
+
header: 'Status',
|
|
358
|
+
cell: (info) => info.getValue(),
|
|
359
|
+
}),
|
|
360
|
+
columnHelper.accessor('lastActive', {
|
|
361
|
+
header: 'Last Active',
|
|
362
|
+
cell: (info) => info.getValue(),
|
|
363
|
+
}),
|
|
364
|
+
],
|
|
365
|
+
[]
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div style={{ maxWidth: '520px' }}>
|
|
370
|
+
<Table
|
|
371
|
+
data={[...data, ...data, ...data, ...data]}
|
|
372
|
+
columns={pinnedColumns as any}
|
|
373
|
+
transparentSticky
|
|
374
|
+
transparentBackground
|
|
375
|
+
wrapperProps={{ style: { maxHeight: '220px', overflow: 'auto' } }}
|
|
376
|
+
tableProps={{ style: { minWidth: '640px' } }}
|
|
377
|
+
// Built-in menu handles visibility and order; pinning is still controlled via TanStack state.
|
|
378
|
+
enableColumnControls
|
|
379
|
+
density={density}
|
|
380
|
+
onDensityChange={setDensity}
|
|
381
|
+
tableOptions={{
|
|
382
|
+
getCoreRowModel: getCoreRowModel(),
|
|
383
|
+
enableColumnPinning: true,
|
|
384
|
+
state: { columnPinning },
|
|
385
|
+
onColumnPinningChange: setColumnPinning as any,
|
|
386
|
+
}}
|
|
387
|
+
getHeaderProps={(header) => {
|
|
388
|
+
const pinState = header.column.getIsPinned();
|
|
389
|
+
if (!pinState) return {};
|
|
390
|
+
const left = header.column.getStart('left');
|
|
391
|
+
return {
|
|
392
|
+
style: { left },
|
|
393
|
+
className: 'solara-table__header-cell--pinned',
|
|
394
|
+
};
|
|
395
|
+
}}
|
|
396
|
+
getCellProps={(cell) => {
|
|
397
|
+
const pinState = cell.column.getIsPinned();
|
|
398
|
+
if (!pinState) return {};
|
|
399
|
+
const left = cell.column.getStart('left');
|
|
400
|
+
return {
|
|
401
|
+
style: { left },
|
|
402
|
+
className: 'solara-table__cell--pinned',
|
|
403
|
+
};
|
|
404
|
+
}}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function PaginationExample() {
|
|
411
|
+
const [pagination, setPagination] = useState({
|
|
412
|
+
pageIndex: 0,
|
|
413
|
+
pageSize: 5,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
return (
|
|
417
|
+
<Table
|
|
418
|
+
data={data}
|
|
419
|
+
columns={columns as any}
|
|
420
|
+
tableOptions={{
|
|
421
|
+
// Keep pagination state in TanStack so UI stays decoupled.
|
|
422
|
+
state: { pagination },
|
|
423
|
+
onPaginationChange: setPagination,
|
|
424
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
425
|
+
getCoreRowModel: getCoreRowModel(),
|
|
426
|
+
}}
|
|
427
|
+
/>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function PaginationControlsExample() {
|
|
432
|
+
const [pagination, setPagination] = useState({
|
|
433
|
+
pageIndex: 0,
|
|
434
|
+
pageSize: 5,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const largeData = useMemo(() => makeLargeData(2000), []);
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div>
|
|
441
|
+
<Table
|
|
442
|
+
data={largeData}
|
|
443
|
+
columns={columns as any}
|
|
444
|
+
showPagination
|
|
445
|
+
tableOptions={{
|
|
446
|
+
// Pagination is controlled here; footer UI reads from the same state.
|
|
447
|
+
state: { pagination },
|
|
448
|
+
onPaginationChange: setPagination,
|
|
449
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
450
|
+
getCoreRowModel: getCoreRowModel(),
|
|
451
|
+
}}
|
|
452
|
+
/>
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function VirtualizedRowsExample() {
|
|
458
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
459
|
+
const [viewportHeight, setViewportHeight] = useState(320);
|
|
460
|
+
const rowHeight = 40;
|
|
461
|
+
const overscan = 6;
|
|
462
|
+
const largeData = useMemo(() => makeLargeData(2000), []);
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
<Table
|
|
466
|
+
data={largeData}
|
|
467
|
+
columns={columns as any}
|
|
468
|
+
transparentSticky
|
|
469
|
+
transparentBackground
|
|
470
|
+
stickyHeader
|
|
471
|
+
wrapperProps={{
|
|
472
|
+
style: { maxHeight: '320px', overflow: 'auto' },
|
|
473
|
+
onScroll: (event) => {
|
|
474
|
+
const target = event.currentTarget;
|
|
475
|
+
setScrollTop(target.scrollTop);
|
|
476
|
+
setViewportHeight(target.clientHeight);
|
|
477
|
+
},
|
|
478
|
+
}}
|
|
479
|
+
renderBody={(table, rows) => {
|
|
480
|
+
// Virtualize the row model we intend to display (post-sorting/pagination).
|
|
481
|
+
const totalSize = rows.length * rowHeight;
|
|
482
|
+
const startIndex = Math.max(
|
|
483
|
+
0,
|
|
484
|
+
Math.floor(scrollTop / rowHeight) - overscan
|
|
485
|
+
);
|
|
486
|
+
const endIndex = Math.min(
|
|
487
|
+
rows.length,
|
|
488
|
+
Math.ceil((scrollTop + viewportHeight) / rowHeight) + overscan
|
|
489
|
+
);
|
|
490
|
+
const visibleRows = rows.slice(startIndex, endIndex);
|
|
491
|
+
const paddingTop = startIndex * rowHeight;
|
|
492
|
+
const paddingBottom = totalSize - endIndex * rowHeight;
|
|
493
|
+
const colSpan = table.getVisibleLeafColumns().length;
|
|
494
|
+
|
|
495
|
+
return (
|
|
496
|
+
<tbody className="solara-table__body">
|
|
497
|
+
{paddingTop > 0 ? (
|
|
498
|
+
<tr aria-hidden="true">
|
|
499
|
+
<td colSpan={colSpan} style={{ height: `${paddingTop}px`, padding: 0 }} />
|
|
500
|
+
</tr>
|
|
501
|
+
) : null}
|
|
502
|
+
{visibleRows.map((row) => (
|
|
503
|
+
<tr key={row.id} className="solara-table__row" style={{ height: `${rowHeight}px` }}>
|
|
504
|
+
{row.getVisibleCells().map((cell) => (
|
|
505
|
+
<td key={cell.id} className="solara-table__cell">
|
|
506
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
507
|
+
</td>
|
|
508
|
+
))}
|
|
509
|
+
</tr>
|
|
510
|
+
))}
|
|
511
|
+
{paddingBottom > 0 ? (
|
|
512
|
+
<tr aria-hidden="true">
|
|
513
|
+
<td colSpan={colSpan} style={{ height: `${paddingBottom}px`, padding: 0 }} />
|
|
514
|
+
</tr>
|
|
515
|
+
) : null}
|
|
516
|
+
</tbody>
|
|
517
|
+
);
|
|
518
|
+
}}
|
|
519
|
+
/>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function LargeDataPerformanceExample() {
|
|
524
|
+
const [pagination, setPagination] = useState({
|
|
525
|
+
pageIndex: 0,
|
|
526
|
+
pageSize: 25,
|
|
527
|
+
});
|
|
528
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
529
|
+
const largeData = useMemo(() => makeLargeData(5000), []);
|
|
530
|
+
const totalRows = isLoading ? 0 : largeData.length;
|
|
531
|
+
const pageCount = Math.max(1, Math.ceil(totalRows / pagination.pageSize));
|
|
532
|
+
const safePageIndex = Math.min(pagination.pageIndex, pageCount - 1);
|
|
533
|
+
const safePagination = {
|
|
534
|
+
pageIndex: safePageIndex,
|
|
535
|
+
pageSize: pagination.pageSize,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
useEffect(() => {
|
|
539
|
+
// Clamp page index when row count changes (e.g., loading -> empty).
|
|
540
|
+
if (safePageIndex !== pagination.pageIndex) {
|
|
541
|
+
setPagination((prev) => ({ ...prev, pageIndex: safePageIndex }));
|
|
542
|
+
}
|
|
543
|
+
}, [pagination.pageIndex, safePageIndex]);
|
|
544
|
+
|
|
545
|
+
const toggleLoading = () => {
|
|
546
|
+
// Reset to page 0 so we never point past the end during loading.
|
|
547
|
+
setIsLoading((prev) => !prev);
|
|
548
|
+
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<div>
|
|
553
|
+
<div style={{ marginBottom: '12px', display: 'flex', gap: '8px' }}>
|
|
554
|
+
<button type="button" onClick={toggleLoading}>
|
|
555
|
+
Toggle loading
|
|
556
|
+
</button>
|
|
557
|
+
<span>{isLoading ? 'Loading rows...' : 'Loaded rows'}</span>
|
|
558
|
+
</div>
|
|
559
|
+
<Table
|
|
560
|
+
data={isLoading ? [] : largeData}
|
|
561
|
+
columns={columns as any}
|
|
562
|
+
emptyState={isLoading ? 'Loading...' : 'No results.'}
|
|
563
|
+
tableOptions={{
|
|
564
|
+
state: { pagination: safePagination },
|
|
565
|
+
onPaginationChange: setPagination,
|
|
566
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
567
|
+
getCoreRowModel: getCoreRowModel(),
|
|
568
|
+
}}
|
|
569
|
+
/>
|
|
570
|
+
</div>
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
type StoryControlOption = {
|
|
575
|
+
label: string;
|
|
576
|
+
value: string;
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
type StoryControl = {
|
|
580
|
+
name: string;
|
|
581
|
+
label?: string;
|
|
582
|
+
type: 'select' | 'text' | 'boolean' | 'number';
|
|
583
|
+
options?: Array<string | StoryControlOption>;
|
|
584
|
+
min?: number;
|
|
585
|
+
max?: number;
|
|
586
|
+
step?: number;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
type StoryDefinition = {
|
|
590
|
+
title: string;
|
|
591
|
+
description?: string;
|
|
592
|
+
render: (props: Record<string, unknown>) => ReactElement;
|
|
593
|
+
controls?: StoryControl[];
|
|
594
|
+
initialProps?: Record<string, unknown>;
|
|
595
|
+
showProps?: boolean;
|
|
596
|
+
applyPropsToPreview?: boolean;
|
|
597
|
+
code?: string;
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
export const stories: Record<string, StoryDefinition> = {
|
|
601
|
+
playground: {
|
|
602
|
+
title: 'Playground',
|
|
603
|
+
description: 'Adjust the density and behavior of the Table wrapper.',
|
|
604
|
+
render: (props) => (
|
|
605
|
+
<Table
|
|
606
|
+
data={data}
|
|
607
|
+
columns={columns as any}
|
|
608
|
+
transparentSticky
|
|
609
|
+
transparentBackground
|
|
610
|
+
density={props.density as TableDensity}
|
|
611
|
+
showHeader={props.showHeader as boolean}
|
|
612
|
+
layoutMode={props.layoutMode as TableLayoutMode}
|
|
613
|
+
stickyHeader={props.stickyHeader as boolean}
|
|
614
|
+
enableSorting={props.enableSorting as boolean}
|
|
615
|
+
// Column controls menu lives in the header action column.
|
|
616
|
+
enableColumnControls={props.enableColumnControls as boolean}
|
|
617
|
+
enableLayoutToggle={props.enableLayoutToggle as boolean}
|
|
618
|
+
showPagination={props.showPagination as boolean}
|
|
619
|
+
/>
|
|
620
|
+
),
|
|
621
|
+
controls: [
|
|
622
|
+
{
|
|
623
|
+
name: 'density',
|
|
624
|
+
label: 'Density',
|
|
625
|
+
type: 'select',
|
|
626
|
+
options: ['compact', 'standard', 'spacious'],
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: 'layoutMode',
|
|
630
|
+
label: 'Layout Mode',
|
|
631
|
+
type: 'select',
|
|
632
|
+
options: ['list', 'grid'],
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'showHeader',
|
|
636
|
+
label: 'Show Header',
|
|
637
|
+
type: 'boolean',
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
name: 'stickyHeader',
|
|
641
|
+
label: 'Sticky Header',
|
|
642
|
+
type: 'boolean',
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: 'enableSorting',
|
|
646
|
+
label: 'Enable Sorting',
|
|
647
|
+
type: 'boolean',
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: 'enableColumnControls',
|
|
651
|
+
label: 'Column Controls',
|
|
652
|
+
type: 'boolean',
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: 'enableLayoutToggle',
|
|
656
|
+
label: 'Layout Toggle',
|
|
657
|
+
type: 'boolean',
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: 'showPagination',
|
|
661
|
+
label: 'Show Pagination',
|
|
662
|
+
type: 'boolean',
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
initialProps: {
|
|
666
|
+
density: 'standard',
|
|
667
|
+
layoutMode: 'list',
|
|
668
|
+
showHeader: true,
|
|
669
|
+
stickyHeader: false,
|
|
670
|
+
enableSorting: true,
|
|
671
|
+
enableColumnControls: true,
|
|
672
|
+
enableLayoutToggle: true,
|
|
673
|
+
showPagination: false,
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
gridDefault: {
|
|
677
|
+
title: 'Grid Default',
|
|
678
|
+
showProps: true,
|
|
679
|
+
render: () => <GridDefaultExample />,
|
|
680
|
+
code: `import { Table } from "@solara/table";
|
|
681
|
+
import type { TableLayoutMode } from "@solara/table";
|
|
682
|
+
|
|
683
|
+
export function Example() {
|
|
684
|
+
const [layoutMode, setLayoutMode] = useState<TableLayoutMode>("grid");
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
<Table
|
|
688
|
+
data={data}
|
|
689
|
+
columns={columns as any}
|
|
690
|
+
layoutMode={layoutMode}
|
|
691
|
+
onLayoutModeChange={setLayoutMode}
|
|
692
|
+
enableLayoutToggle
|
|
693
|
+
enableColumnControls
|
|
694
|
+
gridColumnConfig={{
|
|
695
|
+
avatar: { variant: "media", unpadded: true, showLabel: false },
|
|
696
|
+
name: { hiddenByDefault: true },
|
|
697
|
+
role: { hiddenByDefault: true },
|
|
698
|
+
status: { hiddenByDefault: true },
|
|
699
|
+
lastActive: { hiddenByDefault: true },
|
|
700
|
+
}}
|
|
701
|
+
gridCellRenderers={{
|
|
702
|
+
avatar: (cell, row) => (
|
|
703
|
+
<img
|
|
704
|
+
src={String(cell.getValue())}
|
|
705
|
+
alt={String(row.original.name) + " profile"}
|
|
706
|
+
style={{ width: "100%", height: "100%", minHeight: "180px", objectFit: "cover" }}
|
|
707
|
+
/>
|
|
708
|
+
),
|
|
709
|
+
status: (cell) => <StatusPill value={String(cell.getValue())} />,
|
|
710
|
+
}}
|
|
711
|
+
/>
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
`,
|
|
715
|
+
},
|
|
716
|
+
gridMasonry: {
|
|
717
|
+
title: 'Grid Masonry',
|
|
718
|
+
showProps: true,
|
|
719
|
+
render: () => <GridMasonryExample />,
|
|
720
|
+
code: `import { Table } from "@solara/table";
|
|
721
|
+
import type { TableGridType, TableLayoutMode } from "@solara/table";
|
|
722
|
+
|
|
723
|
+
export function Example() {
|
|
724
|
+
const [layoutMode, setLayoutMode] = useState<TableLayoutMode>("grid");
|
|
725
|
+
const [gridType, setGridType] = useState<TableGridType>("masonry");
|
|
726
|
+
const masonryImageSizes = [
|
|
727
|
+
{ width: 720, height: 420 },
|
|
728
|
+
{ width: 640, height: 860 },
|
|
729
|
+
{ width: 800, height: 500 },
|
|
730
|
+
{ width: 600, height: 920 },
|
|
731
|
+
{ width: 760, height: 460 },
|
|
732
|
+
];
|
|
733
|
+
|
|
734
|
+
return (
|
|
735
|
+
<Table
|
|
736
|
+
data={data}
|
|
737
|
+
columns={columns as any}
|
|
738
|
+
layoutMode={layoutMode}
|
|
739
|
+
onLayoutModeChange={setLayoutMode}
|
|
740
|
+
enableLayoutToggle
|
|
741
|
+
enableColumnControls
|
|
742
|
+
gridType={gridType}
|
|
743
|
+
onGridTypeChange={setGridType}
|
|
744
|
+
gridColumnConfig={{
|
|
745
|
+
avatar: { variant: "media", unpadded: true, showLabel: false },
|
|
746
|
+
name: { hiddenByDefault: true },
|
|
747
|
+
role: { hiddenByDefault: true },
|
|
748
|
+
status: { hiddenByDefault: true },
|
|
749
|
+
lastActive: { hiddenByDefault: true },
|
|
750
|
+
}}
|
|
751
|
+
gridCellRenderers={{
|
|
752
|
+
avatar: (_cell, row) => {
|
|
753
|
+
const size = masonryImageSizes[row.index % masonryImageSizes.length];
|
|
754
|
+
const seed = \`\${row.id}-\${String(row.original.name).toLowerCase().replace(/\\s+/g, "-")}\`;
|
|
755
|
+
const src = \`https://picsum.photos/seed/\${seed}/\${size.width}/\${size.height}\`;
|
|
756
|
+
return (
|
|
757
|
+
<img
|
|
758
|
+
src={src}
|
|
759
|
+
alt={String(row.original.name) + " profile"}
|
|
760
|
+
style={
|
|
761
|
+
gridType === "masonry"
|
|
762
|
+
? { width: "100%", height: "auto", display: "block" }
|
|
763
|
+
: { width: "100%", height: "100%", display: "block", objectFit: "cover" }
|
|
764
|
+
}
|
|
765
|
+
/>
|
|
766
|
+
);
|
|
767
|
+
},
|
|
768
|
+
}}
|
|
769
|
+
/>
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
`,
|
|
773
|
+
},
|
|
774
|
+
listOnlyLayout: {
|
|
775
|
+
title: 'List Only Layout',
|
|
776
|
+
showProps: true,
|
|
777
|
+
render: () => <ListOnlyLayoutExample />,
|
|
778
|
+
code: `import { Table } from "@solara/table";
|
|
779
|
+
|
|
780
|
+
export function Example() {
|
|
781
|
+
return (
|
|
782
|
+
<Table
|
|
783
|
+
data={data}
|
|
784
|
+
columns={columns}
|
|
785
|
+
layoutMode="list"
|
|
786
|
+
layoutModeOptions={["list"]}
|
|
787
|
+
enableLayoutToggle
|
|
788
|
+
enableColumnControls
|
|
789
|
+
/>
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
`,
|
|
793
|
+
},
|
|
794
|
+
overview: {
|
|
795
|
+
title: 'Overview',
|
|
796
|
+
showProps: true,
|
|
797
|
+
render: () => <DensityMenuExample />,
|
|
798
|
+
code: `import { Table } from "@solara/table";
|
|
799
|
+
import { createColumnHelper } from "@tanstack/react-table";
|
|
800
|
+
|
|
801
|
+
const columnHelper = createColumnHelper();
|
|
802
|
+
|
|
803
|
+
const columns = [
|
|
804
|
+
columnHelper.accessor("name", { header: "Name" }),
|
|
805
|
+
columnHelper.accessor("role", { header: "Role" }),
|
|
806
|
+
columnHelper.accessor("status", { header: "Status" }),
|
|
807
|
+
columnHelper.accessor("lastActive", { header: "Last Active" }),
|
|
808
|
+
];
|
|
809
|
+
|
|
810
|
+
export function Example() {
|
|
811
|
+
const [density, setDensity] = useState("standard");
|
|
812
|
+
|
|
813
|
+
return (
|
|
814
|
+
<Table
|
|
815
|
+
data={data}
|
|
816
|
+
columns={columns}
|
|
817
|
+
density={density}
|
|
818
|
+
enableSorting
|
|
819
|
+
enableColumnControls
|
|
820
|
+
onDensityChange={setDensity}
|
|
821
|
+
/>
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
`,
|
|
825
|
+
},
|
|
826
|
+
stickyHeader: {
|
|
827
|
+
title: 'Sticky Header',
|
|
828
|
+
showProps: true,
|
|
829
|
+
render: () => (
|
|
830
|
+
<Table
|
|
831
|
+
data={[...data, ...data, ...data]}
|
|
832
|
+
columns={columns as any}
|
|
833
|
+
transparentSticky
|
|
834
|
+
transparentBackground
|
|
835
|
+
stickyHeader
|
|
836
|
+
// Scroll container must be on the wrapper for sticky headers to work.
|
|
837
|
+
wrapperProps={{ style: { maxHeight: '220px', overflow: 'auto' } }}
|
|
838
|
+
enableColumnControls
|
|
839
|
+
/>
|
|
840
|
+
),
|
|
841
|
+
code: `import { Table } from "@solara/table";
|
|
842
|
+
|
|
843
|
+
export function Example() {
|
|
844
|
+
return (
|
|
845
|
+
<Table
|
|
846
|
+
data={data}
|
|
847
|
+
columns={columns}
|
|
848
|
+
stickyHeader
|
|
849
|
+
wrapperProps={{ style: { maxHeight: 220, overflow: "auto" } }}
|
|
850
|
+
/>
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
`,
|
|
854
|
+
},
|
|
855
|
+
pinnedColumns: {
|
|
856
|
+
title: 'Pinned Columns',
|
|
857
|
+
showProps: true,
|
|
858
|
+
render: () => <PinnedColumnsExample />,
|
|
859
|
+
code: `import { Table } from "@solara/table";
|
|
860
|
+
import { createColumnHelper } from "@tanstack/react-table";
|
|
861
|
+
|
|
862
|
+
const columnHelper = createColumnHelper();
|
|
863
|
+
|
|
864
|
+
export function Example() {
|
|
865
|
+
const [columnPinning, setColumnPinning] = useState({
|
|
866
|
+
left: ["name"],
|
|
867
|
+
right: [],
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
const columns = [
|
|
871
|
+
columnHelper.accessor("name", { header: "Name" }),
|
|
872
|
+
columnHelper.accessor("role", { header: "Role" }),
|
|
873
|
+
columnHelper.accessor("status", { header: "Status" }),
|
|
874
|
+
columnHelper.accessor("lastActive", { header: "Last Active" }),
|
|
875
|
+
];
|
|
876
|
+
|
|
877
|
+
return (
|
|
878
|
+
<Table
|
|
879
|
+
data={data}
|
|
880
|
+
columns={columns as any}
|
|
881
|
+
tableProps={{ style: { minWidth: 640 } }}
|
|
882
|
+
enableColumnControls
|
|
883
|
+
tableOptions={{
|
|
884
|
+
enableColumnPinning: true,
|
|
885
|
+
state: { columnPinning },
|
|
886
|
+
onColumnPinningChange: setColumnPinning,
|
|
887
|
+
}}
|
|
888
|
+
getHeaderProps={(header) => {
|
|
889
|
+
if (!header.column.getIsPinned()) return {};
|
|
890
|
+
return {
|
|
891
|
+
style: { left: header.column.getStart("left") },
|
|
892
|
+
className: "solara-table__header-cell--pinned",
|
|
893
|
+
};
|
|
894
|
+
}}
|
|
895
|
+
getCellProps={(cell) => {
|
|
896
|
+
if (!cell.column.getIsPinned()) return {};
|
|
897
|
+
return {
|
|
898
|
+
style: { left: cell.column.getStart("left") },
|
|
899
|
+
className: "solara-table__cell--pinned",
|
|
900
|
+
};
|
|
901
|
+
}}
|
|
902
|
+
/>
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
`,
|
|
906
|
+
},
|
|
907
|
+
pagination: {
|
|
908
|
+
title: 'Pagination',
|
|
909
|
+
showProps: true,
|
|
910
|
+
render: () => <PaginationExample />,
|
|
911
|
+
code: `import { Table } from "@solara/table";
|
|
912
|
+
import { getPaginationRowModel } from "@tanstack/react-table";
|
|
913
|
+
|
|
914
|
+
export function Example() {
|
|
915
|
+
const [pagination, setPagination] = useState({
|
|
916
|
+
pageIndex: 0,
|
|
917
|
+
pageSize: 5,
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
return (
|
|
921
|
+
<Table
|
|
922
|
+
data={data}
|
|
923
|
+
columns={columns as any}
|
|
924
|
+
tableOptions={{
|
|
925
|
+
state: { pagination },
|
|
926
|
+
onPaginationChange: setPagination,
|
|
927
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
928
|
+
}}
|
|
929
|
+
/>
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
`,
|
|
933
|
+
},
|
|
934
|
+
paginationControls: {
|
|
935
|
+
title: 'Pagination Controls',
|
|
936
|
+
showProps: true,
|
|
937
|
+
render: () => <PaginationControlsExample />,
|
|
938
|
+
code: `import { Table } from "@solara/table";
|
|
939
|
+
import { getPaginationRowModel } from "@tanstack/react-table";
|
|
940
|
+
|
|
941
|
+
export function Example() {
|
|
942
|
+
const [pagination, setPagination] = useState({
|
|
943
|
+
pageIndex: 0,
|
|
944
|
+
pageSize: 5,
|
|
945
|
+
});
|
|
946
|
+
const largeData = useMemo(() => makeLargeData(2000), []);
|
|
947
|
+
|
|
948
|
+
return (
|
|
949
|
+
<div>
|
|
950
|
+
<Table
|
|
951
|
+
data={largeData}
|
|
952
|
+
columns={columns as any}
|
|
953
|
+
showPagination
|
|
954
|
+
tableOptions={{
|
|
955
|
+
state: { pagination },
|
|
956
|
+
onPaginationChange: setPagination,
|
|
957
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
958
|
+
}}
|
|
959
|
+
/>
|
|
960
|
+
</div>
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
`,
|
|
964
|
+
},
|
|
965
|
+
virtualizedRows: {
|
|
966
|
+
title: 'Virtualized Rows',
|
|
967
|
+
showProps: true,
|
|
968
|
+
render: () => <VirtualizedRowsExample />,
|
|
969
|
+
code: `import { Table } from "@solara/table";
|
|
970
|
+
import { flexRender } from "@tanstack/react-table";
|
|
971
|
+
|
|
972
|
+
export function Example() {
|
|
973
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
974
|
+
const [viewportHeight, setViewportHeight] = useState(320);
|
|
975
|
+
const rowHeight = 40;
|
|
976
|
+
const overscan = 6;
|
|
977
|
+
|
|
978
|
+
return (
|
|
979
|
+
<Table
|
|
980
|
+
data={largeData}
|
|
981
|
+
columns={columns as any}
|
|
982
|
+
stickyHeader
|
|
983
|
+
wrapperProps={{
|
|
984
|
+
style: { maxHeight: 320, overflow: "auto" },
|
|
985
|
+
onScroll: (event) => {
|
|
986
|
+
const target = event.currentTarget;
|
|
987
|
+
setScrollTop(target.scrollTop);
|
|
988
|
+
setViewportHeight(target.clientHeight);
|
|
989
|
+
},
|
|
990
|
+
}}
|
|
991
|
+
renderBody={(table, rows) => {
|
|
992
|
+
const totalSize = rows.length * rowHeight;
|
|
993
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
|
|
994
|
+
const endIndex = Math.min(
|
|
995
|
+
rows.length,
|
|
996
|
+
Math.ceil((scrollTop + viewportHeight) / rowHeight) + overscan
|
|
997
|
+
);
|
|
998
|
+
const visibleRows = rows.slice(startIndex, endIndex);
|
|
999
|
+
const paddingTop = startIndex * rowHeight;
|
|
1000
|
+
const paddingBottom = totalSize - endIndex * rowHeight;
|
|
1001
|
+
const colSpan = table.getVisibleLeafColumns().length;
|
|
1002
|
+
|
|
1003
|
+
return (
|
|
1004
|
+
<tbody className="solara-table__body">
|
|
1005
|
+
{paddingTop > 0 ? (
|
|
1006
|
+
<tr aria-hidden="true">
|
|
1007
|
+
<td colSpan={colSpan} style={{ height: paddingTop, padding: 0 }} />
|
|
1008
|
+
</tr>
|
|
1009
|
+
) : null}
|
|
1010
|
+
{visibleRows.map((row) => (
|
|
1011
|
+
<tr key={row.id} className="solara-table__row">
|
|
1012
|
+
{row.getVisibleCells().map((cell) => (
|
|
1013
|
+
<td key={cell.id} className="solara-table__cell">
|
|
1014
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
1015
|
+
</td>
|
|
1016
|
+
))}
|
|
1017
|
+
</tr>
|
|
1018
|
+
))}
|
|
1019
|
+
{paddingBottom > 0 ? (
|
|
1020
|
+
<tr aria-hidden="true">
|
|
1021
|
+
<td colSpan={colSpan} style={{ height: paddingBottom, padding: 0 }} />
|
|
1022
|
+
</tr>
|
|
1023
|
+
) : null}
|
|
1024
|
+
</tbody>
|
|
1025
|
+
);
|
|
1026
|
+
}}
|
|
1027
|
+
/>
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
`,
|
|
1031
|
+
},
|
|
1032
|
+
expandableRows: {
|
|
1033
|
+
title: 'Expandable Rows',
|
|
1034
|
+
showProps: true,
|
|
1035
|
+
render: () => <ExpandableRowsExample />,
|
|
1036
|
+
code: `import { Table } from "@solara/table";
|
|
1037
|
+
import { createColumnHelper, getCoreRowModel, getExpandedRowModel } from "@tanstack/react-table";
|
|
1038
|
+
|
|
1039
|
+
const columnHelper = createColumnHelper();
|
|
1040
|
+
|
|
1041
|
+
export function Example() {
|
|
1042
|
+
const [expanded, setExpanded] = React.useState({});
|
|
1043
|
+
|
|
1044
|
+
const columnsWithToggle = React.useMemo(
|
|
1045
|
+
() => [
|
|
1046
|
+
{
|
|
1047
|
+
id: 'expand',
|
|
1048
|
+
header: () => null,
|
|
1049
|
+
cell: ({ row }) => {
|
|
1050
|
+
return row.getCanExpand() ? (
|
|
1051
|
+
<button
|
|
1052
|
+
aria-label={row.getIsExpanded() ? 'Collapse row' : 'Expand row'}
|
|
1053
|
+
onClick={row.getToggleExpandedHandler()}
|
|
1054
|
+
>
|
|
1055
|
+
{row.getIsExpanded() ? '👇' : '👉'}
|
|
1056
|
+
</button>
|
|
1057
|
+
) : null;
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
...columns,
|
|
1061
|
+
],
|
|
1062
|
+
[]
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
return (
|
|
1066
|
+
<Table
|
|
1067
|
+
data={dataWithSubRows}
|
|
1068
|
+
columns={columnsWithToggle as any}
|
|
1069
|
+
tableOptions={{
|
|
1070
|
+
state: { expanded },
|
|
1071
|
+
onExpandedChange: setExpanded,
|
|
1072
|
+
getSubRows: (row) => row.subRows,
|
|
1073
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1074
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
1075
|
+
}}
|
|
1076
|
+
/>
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
`,
|
|
1080
|
+
},
|
|
1081
|
+
selectableRows: {
|
|
1082
|
+
title: 'Selectable Rows',
|
|
1083
|
+
showProps: true,
|
|
1084
|
+
render: () => <SelectableRowsExample />,
|
|
1085
|
+
code: `import { Table } from "@solara/table";
|
|
1086
|
+
import { Checkbox } from "@solara/checkbox";
|
|
1087
|
+
import { createColumnHelper } from "@tanstack/react-table";
|
|
1088
|
+
|
|
1089
|
+
const columnHelper = createColumnHelper();
|
|
1090
|
+
|
|
1091
|
+
export function Example() {
|
|
1092
|
+
const [rowSelection, setRowSelection] = React.useState({});
|
|
1093
|
+
|
|
1094
|
+
const columnsWithSelect = React.useMemo(
|
|
1095
|
+
() => [
|
|
1096
|
+
{
|
|
1097
|
+
id: "select",
|
|
1098
|
+
header: ({ table }) => {
|
|
1099
|
+
const isAllRowsSelected = table.getIsAllRowsSelected();
|
|
1100
|
+
const isSomeRowsSelected = table.getIsSomeRowsSelected();
|
|
1101
|
+
return (
|
|
1102
|
+
<Checkbox
|
|
1103
|
+
aria-label="Select all rows"
|
|
1104
|
+
size="small"
|
|
1105
|
+
checked={isAllRowsSelected}
|
|
1106
|
+
indeterminate={!isAllRowsSelected && isSomeRowsSelected}
|
|
1107
|
+
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
1108
|
+
/>
|
|
1109
|
+
);
|
|
1110
|
+
},
|
|
1111
|
+
cell: ({ row }) => (
|
|
1112
|
+
<Checkbox
|
|
1113
|
+
aria-label={"Select row " + row.id}
|
|
1114
|
+
size="small"
|
|
1115
|
+
checked={row.getIsSelected()}
|
|
1116
|
+
disabled={!row.getCanSelect()}
|
|
1117
|
+
onChange={row.getToggleSelectedHandler()}
|
|
1118
|
+
/>
|
|
1119
|
+
),
|
|
1120
|
+
},
|
|
1121
|
+
...columns,
|
|
1122
|
+
],
|
|
1123
|
+
[]
|
|
1124
|
+
);
|
|
1125
|
+
|
|
1126
|
+
return (
|
|
1127
|
+
<Table
|
|
1128
|
+
data={data}
|
|
1129
|
+
columns={columnsWithSelect as any}
|
|
1130
|
+
tableOptions={{
|
|
1131
|
+
enableRowSelection: true,
|
|
1132
|
+
state: { rowSelection },
|
|
1133
|
+
onRowSelectionChange: setRowSelection,
|
|
1134
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1135
|
+
}}
|
|
1136
|
+
/>
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
`,
|
|
1140
|
+
},
|
|
1141
|
+
largeDataPerformance: {
|
|
1142
|
+
title: 'Large Data Performance',
|
|
1143
|
+
showProps: true,
|
|
1144
|
+
render: () => <LargeDataPerformanceExample />,
|
|
1145
|
+
code: `import { Table } from "@solara/table";
|
|
1146
|
+
import { getPaginationRowModel } from "@tanstack/react-table";
|
|
1147
|
+
|
|
1148
|
+
export function Example() {
|
|
1149
|
+
const [pagination, setPagination] = useState({
|
|
1150
|
+
pageIndex: 0,
|
|
1151
|
+
pageSize: 25,
|
|
1152
|
+
});
|
|
1153
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
1154
|
+
const largeData = useMemo(() => makeLargeData(5000), []);
|
|
1155
|
+
|
|
1156
|
+
return (
|
|
1157
|
+
<div>
|
|
1158
|
+
<button type="button" onClick={() => setIsLoading((prev) => !prev)}>
|
|
1159
|
+
Toggle loading
|
|
1160
|
+
</button>
|
|
1161
|
+
<Table
|
|
1162
|
+
data={isLoading ? [] : largeData}
|
|
1163
|
+
columns={columns as any}
|
|
1164
|
+
emptyState={isLoading ? "Loading..." : "No results."}
|
|
1165
|
+
tableOptions={{
|
|
1166
|
+
state: { pagination },
|
|
1167
|
+
onPaginationChange: setPagination,
|
|
1168
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
1169
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1170
|
+
}}
|
|
1171
|
+
/>
|
|
1172
|
+
</div>
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
`,
|
|
1176
|
+
},
|
|
1177
|
+
};
|