@dxos/react-ui-table 0.4.10-next.71cec10 → 0.5.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/dist/lib/browser/index.mjs +108 -71
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/Table/Table.d.ts +17 -4
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableBody.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableRootContext.d.ts +17 -0
- package/dist/types/src/components/Table/TableRootContext.d.ts.map +1 -0
- package/dist/types/src/components/Table/index.d.ts +1 -0
- package/dist/types/src/components/Table/index.d.ts.map +1 -1
- package/dist/types/src/components/Table/props.d.ts +2 -3
- package/dist/types/src/components/Table/props.d.ts.map +1 -1
- package/dist/types/src/hooks/usePinLastRow.d.ts +1 -1
- package/dist/types/src/hooks/usePinLastRow.d.ts.map +1 -1
- package/dist/types/src/schema/SchemaToTable.stories.d.ts.map +1 -1
- package/package.json +17 -15
- package/src/components/Table/Table.stories.tsx +114 -103
- package/src/components/Table/Table.tsx +59 -50
- package/src/components/Table/TableBody.tsx +6 -1
- package/src/components/Table/TableRootContext.ts +39 -0
- package/src/components/Table/index.ts +1 -0
- package/src/components/Table/props.ts +17 -24
- package/src/hooks/usePinLastRow.ts +10 -10
- package/src/schema/SchemaToTable.stories.tsx +15 -16
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
7
|
import { Plugs, PlugsConnected } from '@phosphor-icons/react';
|
|
8
|
-
import React, { useCallback, useEffect, useMemo,
|
|
8
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
10
|
import { create } from '@dxos/echo-schema';
|
|
11
11
|
import { registerSignalRuntime } from '@dxos/echo-signals/react';
|
|
12
12
|
import { PublicKey } from '@dxos/keys';
|
|
13
13
|
import { faker } from '@dxos/random';
|
|
14
|
-
import {
|
|
14
|
+
import { Button, DensityProvider } from '@dxos/react-ui';
|
|
15
15
|
import { withTheme } from '@dxos/storybook-utils';
|
|
16
16
|
import { range } from '@dxos/util';
|
|
17
17
|
|
|
@@ -116,9 +116,19 @@ const makeColumns = (onUpdate?: ValueUpdater<Item, any>): TableColumnDef<Item, a
|
|
|
116
116
|
// Tests
|
|
117
117
|
//
|
|
118
118
|
|
|
119
|
+
const MinimalTable = (props: any) => {
|
|
120
|
+
return (
|
|
121
|
+
<Table.Root>
|
|
122
|
+
<Table.Viewport classNames='fixed inset-0 overflow-auto'>
|
|
123
|
+
<Table.Table<any> {...props} />
|
|
124
|
+
</Table.Viewport>
|
|
125
|
+
</Table.Root>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
119
129
|
export default {
|
|
120
130
|
title: 'react-ui-table/Table',
|
|
121
|
-
component:
|
|
131
|
+
component: MinimalTable,
|
|
122
132
|
args: {
|
|
123
133
|
header: true,
|
|
124
134
|
keyAccessor: (item: Item) => item.publicKey.toHex(),
|
|
@@ -187,7 +197,6 @@ export const Empty = {
|
|
|
187
197
|
|
|
188
198
|
export const Dynamic = {
|
|
189
199
|
render: () => {
|
|
190
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
191
200
|
const [items, setItems] = useState<Item[]>(createItems(50));
|
|
192
201
|
|
|
193
202
|
useEffect(() => {
|
|
@@ -205,26 +214,27 @@ export const Dynamic = {
|
|
|
205
214
|
const columns = useMemo(() => makeColumns(), []);
|
|
206
215
|
|
|
207
216
|
return (
|
|
208
|
-
<
|
|
209
|
-
<Table
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
<Table.Root>
|
|
218
|
+
<Table.Viewport classNames='fixed inset-0 overflow-auto'>
|
|
219
|
+
<Table.Table<Item>
|
|
220
|
+
role='grid'
|
|
221
|
+
rowsSelectable='multi'
|
|
222
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
223
|
+
columns={columns}
|
|
224
|
+
data={items}
|
|
225
|
+
fullWidth
|
|
226
|
+
stickyHeader
|
|
227
|
+
border
|
|
228
|
+
pinLastRow
|
|
229
|
+
/>
|
|
230
|
+
</Table.Viewport>
|
|
231
|
+
</Table.Root>
|
|
221
232
|
);
|
|
222
233
|
},
|
|
223
234
|
};
|
|
224
235
|
|
|
225
236
|
export const Editable = {
|
|
226
237
|
render: () => {
|
|
227
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
228
238
|
const [items, setItems] = useState<Item[]>(createItems(200));
|
|
229
239
|
|
|
230
240
|
const onUpdate: ValueUpdater<Item, any> = useCallback(
|
|
@@ -235,26 +245,27 @@ export const Editable = {
|
|
|
235
245
|
const columns = useMemo(() => makeColumns(onUpdate), [onUpdate]);
|
|
236
246
|
|
|
237
247
|
return (
|
|
238
|
-
<
|
|
239
|
-
<Table
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
<Table.Root>
|
|
249
|
+
<Table.Viewport classNames='fixed inset-0 overflow-auto'>
|
|
250
|
+
<Table.Table<Item>
|
|
251
|
+
role='grid'
|
|
252
|
+
rowsSelectable='multi'
|
|
253
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
254
|
+
columns={columns}
|
|
255
|
+
data={items}
|
|
256
|
+
fullWidth
|
|
257
|
+
stickyHeader
|
|
258
|
+
border
|
|
259
|
+
pinLastRow
|
|
260
|
+
/>
|
|
261
|
+
</Table.Viewport>
|
|
262
|
+
</Table.Root>
|
|
251
263
|
);
|
|
252
264
|
},
|
|
253
265
|
};
|
|
254
266
|
|
|
255
267
|
export const PinnedLastRow = {
|
|
256
268
|
render: () => {
|
|
257
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
258
269
|
const [items, setItems] = useState<Item[]>(createItems(200));
|
|
259
270
|
|
|
260
271
|
const onUpdate: ValueUpdater<Item, any> = useCallback(
|
|
@@ -265,27 +276,27 @@ export const PinnedLastRow = {
|
|
|
265
276
|
const columns = useMemo(() => makeColumns(onUpdate), [onUpdate]);
|
|
266
277
|
|
|
267
278
|
return (
|
|
268
|
-
<
|
|
269
|
-
<Table
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
<Table.Root>
|
|
280
|
+
<Table.Viewport classNames='fixed inset-0 overflow-auto'>
|
|
281
|
+
<Table.Table<Item>
|
|
282
|
+
role='grid'
|
|
283
|
+
rowsSelectable='multi'
|
|
284
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
285
|
+
columns={columns}
|
|
286
|
+
data={items}
|
|
287
|
+
fullWidth
|
|
288
|
+
stickyHeader
|
|
289
|
+
border
|
|
290
|
+
pinLastRow
|
|
291
|
+
/>
|
|
292
|
+
</Table.Viewport>
|
|
293
|
+
</Table.Root>
|
|
282
294
|
);
|
|
283
295
|
},
|
|
284
296
|
};
|
|
285
297
|
|
|
286
298
|
export const InsertDelete = {
|
|
287
299
|
render: () => {
|
|
288
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
289
300
|
const [items, setItems] = useState<Item[]>(createItems(10));
|
|
290
301
|
|
|
291
302
|
const onUpdate: ValueUpdater<Item, any> = useCallback(
|
|
@@ -319,20 +330,21 @@ export const InsertDelete = {
|
|
|
319
330
|
<Button onClick={onDeleteFirst}>Delete first</Button>
|
|
320
331
|
<Button onClick={onDeleteLast}>Delete last</Button>
|
|
321
332
|
</div>
|
|
322
|
-
<
|
|
323
|
-
<Table
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
<Table.Root>
|
|
334
|
+
<Table.Viewport classNames='fixed inset-0 overflow-auto'>
|
|
335
|
+
<Table.Table<Item>
|
|
336
|
+
role='grid'
|
|
337
|
+
rowsSelectable='multi'
|
|
338
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
339
|
+
columns={columns}
|
|
340
|
+
data={items}
|
|
341
|
+
fullWidth
|
|
342
|
+
stickyHeader
|
|
343
|
+
border
|
|
344
|
+
pinLastRow
|
|
345
|
+
/>
|
|
346
|
+
</Table.Viewport>
|
|
347
|
+
</Table.Root>
|
|
336
348
|
</div>
|
|
337
349
|
);
|
|
338
350
|
},
|
|
@@ -340,7 +352,6 @@ export const InsertDelete = {
|
|
|
340
352
|
|
|
341
353
|
export const Resizable = {
|
|
342
354
|
render: () => {
|
|
343
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
344
355
|
const [items, setItems] = useState<Item[]>(createItems(10));
|
|
345
356
|
|
|
346
357
|
const onUpdate: ValueUpdater<Item, any> = useCallback(
|
|
@@ -349,25 +360,24 @@ export const Resizable = {
|
|
|
349
360
|
);
|
|
350
361
|
|
|
351
362
|
return (
|
|
352
|
-
<
|
|
353
|
-
<Table
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
</
|
|
363
|
+
<Table.Root>
|
|
364
|
+
<Table.Viewport classNames='fixed inset-0 overflow-auto'>
|
|
365
|
+
<Table.Table<Item>
|
|
366
|
+
rowsSelectable='multi'
|
|
367
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
368
|
+
columns={makeColumns(onUpdate)}
|
|
369
|
+
data={items}
|
|
370
|
+
fullWidth
|
|
371
|
+
stickyHeader
|
|
372
|
+
/>
|
|
373
|
+
</Table.Viewport>
|
|
374
|
+
</Table.Root>
|
|
364
375
|
);
|
|
365
376
|
},
|
|
366
377
|
};
|
|
367
378
|
|
|
368
379
|
export const TenThousandRows = {
|
|
369
380
|
render: () => {
|
|
370
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
371
381
|
const [items, setItems] = useState<Item[]>(createItems(10000));
|
|
372
382
|
|
|
373
383
|
const onUpdate: ValueUpdater<Item, any> = useCallback(
|
|
@@ -378,19 +388,20 @@ export const TenThousandRows = {
|
|
|
378
388
|
const columns = useMemo(() => makeColumns(onUpdate), [onUpdate]);
|
|
379
389
|
|
|
380
390
|
return (
|
|
381
|
-
<
|
|
382
|
-
<Table
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
391
|
+
<Table.Root>
|
|
392
|
+
<Table.Viewport classNames='fixed inset-0 overflow-auto'>
|
|
393
|
+
<Table.Table<Item>
|
|
394
|
+
role='grid'
|
|
395
|
+
rowsSelectable='multi'
|
|
396
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
397
|
+
columns={columns}
|
|
398
|
+
data={items}
|
|
399
|
+
fullWidth
|
|
400
|
+
stickyHeader
|
|
401
|
+
border
|
|
402
|
+
/>{' '}
|
|
403
|
+
</Table.Viewport>
|
|
404
|
+
</Table.Root>
|
|
394
405
|
);
|
|
395
406
|
},
|
|
396
407
|
};
|
|
@@ -475,25 +486,25 @@ export const RealTimeUpdates = {
|
|
|
475
486
|
return () => clearInterval(interval);
|
|
476
487
|
}, [periodicDeletions, deletionInterval, state.items]);
|
|
477
488
|
|
|
478
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
479
|
-
|
|
480
489
|
const onUpdate = useCallback((...args: any[]) => {}, []);
|
|
481
490
|
const columns = useMemo(() => makeColumns(onUpdate), []);
|
|
482
491
|
|
|
483
492
|
return (
|
|
484
|
-
<
|
|
485
|
-
<Table
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
493
|
+
<Table.Root>
|
|
494
|
+
<Table.Viewport classNames='fixed inset-0 overflow-auto'>
|
|
495
|
+
<Table.Table<Item>
|
|
496
|
+
role='grid'
|
|
497
|
+
rowsSelectable='multi'
|
|
498
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
499
|
+
columns={columns}
|
|
500
|
+
data={state.items}
|
|
501
|
+
fullWidth
|
|
502
|
+
stickyHeader
|
|
503
|
+
border
|
|
504
|
+
pinLastRow
|
|
505
|
+
/>
|
|
506
|
+
</Table.Viewport>
|
|
507
|
+
</Table.Root>
|
|
497
508
|
);
|
|
498
509
|
},
|
|
499
510
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
//
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
|
+
import { type Primitive } from '@radix-ui/react-primitive';
|
|
5
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
4
6
|
import {
|
|
5
7
|
getCoreRowModel,
|
|
6
8
|
getSortedRowModel,
|
|
@@ -11,21 +13,48 @@ import {
|
|
|
11
13
|
getExpandedRowModel,
|
|
12
14
|
type Row,
|
|
13
15
|
} from '@tanstack/react-table';
|
|
14
|
-
import {
|
|
15
|
-
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
|
16
|
+
import React, { type ComponentPropsWithoutRef, Fragment, useCallback, useEffect, useState, useContext } from 'react';
|
|
16
17
|
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
18
|
+
import { type ThemedClassName, useDefaultValue } from '@dxos/react-ui';
|
|
19
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
19
20
|
|
|
20
21
|
import { TableBody } from './TableBody';
|
|
21
22
|
import { type TypedTableProvider, TableProvider as UntypedTableProvider, useTableContext } from './TableContext';
|
|
22
23
|
import { TableFooter } from './TableFooter';
|
|
23
24
|
import { TableHead } from './TableHead';
|
|
25
|
+
import { TableRootContext, useTableRootContext } from './TableRootContext';
|
|
24
26
|
import { type TableProps } from './props';
|
|
25
27
|
import { useColumnResizing, usePinLastRow, useRowSelection } from '../../hooks';
|
|
26
28
|
import { groupTh, tableRoot } from '../../theme';
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
type TableRootProps = { children: React.ReactNode };
|
|
31
|
+
|
|
32
|
+
const TableRoot = ({ children }: TableRootProps) => {
|
|
33
|
+
const contextValue = useTableRootContext();
|
|
34
|
+
return <TableRootContext.Provider value={contextValue}>{children}</TableRootContext.Provider>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type TableViewportProps = ThemedClassName<ComponentPropsWithoutRef<typeof Primitive.div>> & {
|
|
38
|
+
asChild?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const TableViewport = ({ children, classNames, asChild, ...props }: TableViewportProps) => {
|
|
42
|
+
const { scrollContextRef } = useContext(TableRootContext);
|
|
43
|
+
|
|
44
|
+
const classes = mx(classNames);
|
|
45
|
+
|
|
46
|
+
return asChild ? (
|
|
47
|
+
<Slot ref={scrollContextRef} {...props}>
|
|
48
|
+
{children}
|
|
49
|
+
</Slot>
|
|
50
|
+
) : (
|
|
51
|
+
<div role='none' className={classes} ref={scrollContextRef} {...props}>
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const TablePrimitive = <TData extends RowData>(props: TableProps<TData>) => {
|
|
29
58
|
const {
|
|
30
59
|
role,
|
|
31
60
|
onColumnResize,
|
|
@@ -34,7 +63,6 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
34
63
|
rowsSelectable,
|
|
35
64
|
debug,
|
|
36
65
|
onDataSelectionChange,
|
|
37
|
-
getScrollElement,
|
|
38
66
|
pinLastRow,
|
|
39
67
|
} = props;
|
|
40
68
|
|
|
@@ -111,15 +139,11 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
111
139
|
onDataSelectionChange?.(Object.keys(rowSelection).map((id) => table.getRowModel().rowsById[id].original));
|
|
112
140
|
}, [onDataSelectionChange, rowSelection, table]);
|
|
113
141
|
|
|
114
|
-
usePinLastRow(pinLastRow, table, data
|
|
142
|
+
usePinLastRow(pinLastRow, table, data);
|
|
115
143
|
|
|
116
144
|
// Create additional expansion column if all columns have fixed width.
|
|
117
145
|
const expand = false; // columns.map((column) => column.size).filter(Boolean).length === columns?.length;
|
|
118
146
|
|
|
119
|
-
if (!getScrollElement) {
|
|
120
|
-
log.warn('Table: getScrollElement is not set. This is required for virtualized tables.');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
147
|
return (
|
|
124
148
|
<TableProvider
|
|
125
149
|
{...props}
|
|
@@ -128,16 +152,17 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
128
152
|
expand={expand}
|
|
129
153
|
isGrid={role === 'grid' || role === 'treegrid'}
|
|
130
154
|
>
|
|
131
|
-
<TableImpl<TData> debug={false}
|
|
155
|
+
<TableImpl<TData> debug={false} {...props} />
|
|
132
156
|
</TableProvider>
|
|
133
157
|
);
|
|
134
158
|
};
|
|
135
159
|
|
|
160
|
+
// TODO(Zan): Smush this into the Table component.
|
|
136
161
|
/**
|
|
137
162
|
* Pure implementation of table outside of context set-up.
|
|
138
163
|
*/
|
|
139
164
|
const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
140
|
-
const { debug, classNames,
|
|
165
|
+
const { debug, classNames, role, footer, grouping, fullWidth } = props;
|
|
141
166
|
const { table } = useTableContext<TData>();
|
|
142
167
|
|
|
143
168
|
if (debug) {
|
|
@@ -148,9 +173,10 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
148
173
|
);
|
|
149
174
|
}
|
|
150
175
|
|
|
151
|
-
const virtualisable = getScrollElement !== undefined;
|
|
152
176
|
const isResizingColumn = table.getState().columnSizingInfo.isResizingColumn;
|
|
153
177
|
|
|
178
|
+
const TableComponent = isResizingColumn ? MemoizedVirtualisedTableContent : VirtualizedTableContent;
|
|
179
|
+
|
|
154
180
|
return (
|
|
155
181
|
<table
|
|
156
182
|
role={role}
|
|
@@ -159,44 +185,24 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
159
185
|
>
|
|
160
186
|
<TableHead />
|
|
161
187
|
|
|
162
|
-
{grouping?.length !== 0 ?
|
|
163
|
-
virtualisable ? (
|
|
164
|
-
isResizingColumn ? (
|
|
165
|
-
<MemoizedVirtualisedTableContent getScrollElement={getScrollElement} />
|
|
166
|
-
) : (
|
|
167
|
-
<VirtualizedTableContent getScrollElement={getScrollElement} />
|
|
168
|
-
)
|
|
169
|
-
) : (
|
|
170
|
-
<UnvirtualizedTableContent />
|
|
171
|
-
)
|
|
172
|
-
) : (
|
|
173
|
-
<GroupedTableContent />
|
|
174
|
-
)}
|
|
188
|
+
{grouping?.length !== 0 ? <TableComponent /> : <GroupedTableContent />}
|
|
175
189
|
|
|
176
190
|
{footer && <TableFooter />}
|
|
177
191
|
</table>
|
|
178
192
|
);
|
|
179
193
|
};
|
|
180
194
|
|
|
181
|
-
const
|
|
182
|
-
const { table } = useTableContext();
|
|
183
|
-
|
|
184
|
-
const centerRows = table.getCenterRows();
|
|
185
|
-
const pinnedRows = table.getBottomRows();
|
|
186
|
-
|
|
187
|
-
const rows = [...centerRows, ...pinnedRows];
|
|
188
|
-
|
|
189
|
-
return <TableBody rows={rows} />;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const VirtualizedTableContent = ({
|
|
193
|
-
getScrollElement,
|
|
194
|
-
}: Pick<VirtualizerOptions<Element, Element>, 'getScrollElement'>) => {
|
|
195
|
+
const VirtualizedTableContent = () => {
|
|
195
196
|
const { table } = useTableContext();
|
|
197
|
+
const { virtualizer, dispatch } = useContext(TableRootContext);
|
|
196
198
|
|
|
197
199
|
const centerRows = table.getCenterRows();
|
|
198
200
|
let pinnedRows = [] as Row<unknown>[];
|
|
199
201
|
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
dispatch({ type: 'updateTableCount', count: centerRows.length });
|
|
204
|
+
}, [centerRows.length]);
|
|
205
|
+
|
|
200
206
|
try {
|
|
201
207
|
// TODO(zan): Work out how to sync the row pinning with rendering
|
|
202
208
|
// This is a hack because the first call to this function after a row is deleted will throw an error
|
|
@@ -205,12 +211,7 @@ const VirtualizedTableContent = ({
|
|
|
205
211
|
pinnedRows = table.getBottomRows();
|
|
206
212
|
} catch (_) {} // Ignore error
|
|
207
213
|
|
|
208
|
-
const { getTotalSize, getVirtualItems } =
|
|
209
|
-
getScrollElement,
|
|
210
|
-
count: centerRows.length,
|
|
211
|
-
overscan: 8,
|
|
212
|
-
estimateSize: () => 40,
|
|
213
|
-
});
|
|
214
|
+
const { getTotalSize, getVirtualItems } = virtualizer;
|
|
214
215
|
|
|
215
216
|
const virtualRows = getVirtualItems();
|
|
216
217
|
const totalSize = getTotalSize();
|
|
@@ -218,10 +219,12 @@ const VirtualizedTableContent = ({
|
|
|
218
219
|
const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0;
|
|
219
220
|
const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0;
|
|
220
221
|
|
|
221
|
-
const rowsToRender = [...virtualRows.map((virtualRow) => centerRows[virtualRow.index]), ...pinnedRows]
|
|
222
|
+
const rowsToRender = [...virtualRows.map((virtualRow) => centerRows[virtualRow.index]), ...pinnedRows].filter(
|
|
223
|
+
(r) => !!r,
|
|
224
|
+
);
|
|
222
225
|
|
|
223
226
|
return (
|
|
224
|
-
|
|
227
|
+
<Fragment>
|
|
225
228
|
{paddingTop > 0 && (
|
|
226
229
|
<tbody role='none'>
|
|
227
230
|
<tr role='none'>
|
|
@@ -237,7 +240,7 @@ const VirtualizedTableContent = ({
|
|
|
237
240
|
</tr>
|
|
238
241
|
</tbody>
|
|
239
242
|
)}
|
|
240
|
-
|
|
243
|
+
</Fragment>
|
|
241
244
|
);
|
|
242
245
|
};
|
|
243
246
|
|
|
@@ -274,3 +277,9 @@ const GroupedTableContent = () => {
|
|
|
274
277
|
</>
|
|
275
278
|
);
|
|
276
279
|
};
|
|
280
|
+
|
|
281
|
+
export const Table = {
|
|
282
|
+
Root: TableRoot,
|
|
283
|
+
Table: TablePrimitive,
|
|
284
|
+
Viewport: TableViewport,
|
|
285
|
+
};
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
|
|
6
6
|
import { flexRender, type Row, type RowData } from '@tanstack/react-table';
|
|
7
|
-
import React from 'react';
|
|
7
|
+
import React, { useContext } from 'react';
|
|
8
8
|
|
|
9
9
|
import { useTableContext } from './TableContext';
|
|
10
|
+
import { TableRootContext } from './TableRootContext';
|
|
10
11
|
import { tbodyTr } from '../../theme';
|
|
11
12
|
import { Cell } from '../Cell/Cell';
|
|
12
13
|
|
|
@@ -23,6 +24,8 @@ const TableBody = ({ rows }: TableBodyProps) => {
|
|
|
23
24
|
const domAttributes = useArrowNavigationGroup({ axis: 'grid' });
|
|
24
25
|
const canBeCurrent = !isGrid && !!onDatumClick;
|
|
25
26
|
|
|
27
|
+
const { virtualizer } = useContext(TableRootContext);
|
|
28
|
+
|
|
26
29
|
return (
|
|
27
30
|
<tbody {...(isGrid && domAttributes)}>
|
|
28
31
|
{rows.map((row) => {
|
|
@@ -37,6 +40,8 @@ const TableBody = ({ rows }: TableBodyProps) => {
|
|
|
37
40
|
<tr
|
|
38
41
|
key={keyAccessor ? keyAccessor(row.original) : row.id}
|
|
39
42
|
className={classNames}
|
|
43
|
+
data-index={row.index}
|
|
44
|
+
ref={virtualizer.measureElement}
|
|
40
45
|
{...(isCurrent && { 'aria-current': 'location' })}
|
|
41
46
|
{...(isSelected && { 'aria-selected': 'true' })}
|
|
42
47
|
{...(canBeCurrent && {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
6
|
+
import { createContext, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
type TableRootContextActions = { type: 'updateTableCount'; count: number };
|
|
9
|
+
|
|
10
|
+
export const useTableRootContext = () => {
|
|
11
|
+
const scrollContextRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
const [tableCount, setTableCount] = useState(0);
|
|
13
|
+
|
|
14
|
+
const virtualizer = useVirtualizer({
|
|
15
|
+
getScrollElement: () => scrollContextRef?.current ?? null,
|
|
16
|
+
count: tableCount,
|
|
17
|
+
overscan: 8,
|
|
18
|
+
estimateSize: () => 40,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const dispatch = (action: TableRootContextActions) => {
|
|
22
|
+
switch (action.type) {
|
|
23
|
+
case 'updateTableCount':
|
|
24
|
+
setTableCount(action.count);
|
|
25
|
+
break;
|
|
26
|
+
|
|
27
|
+
default:
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return { scrollContextRef, virtualizer, dispatch };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type TableRootContextValue = ReturnType<typeof useTableRootContext>;
|
|
36
|
+
|
|
37
|
+
// Create the context without a default value.
|
|
38
|
+
// The expectation is that the provider will always supply a value, so no need to define a default here.
|
|
39
|
+
export const TableRootContext = createContext<TableRootContextValue>({} as TableRootContextValue);
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type RowData, type RowSelectionState, type Table, type VisibilityState } from '@tanstack/react-table';
|
|
6
|
-
import { type VirtualizerOptions } from '@tanstack/react-virtual';
|
|
7
6
|
|
|
8
7
|
import { type ClassNameValue } from '@dxos/react-ui-types';
|
|
9
8
|
|
|
@@ -31,30 +30,24 @@ export type TableCurrent<TData extends RowData> = Partial<{
|
|
|
31
30
|
onDatumClick: (datum: TData) => void;
|
|
32
31
|
}>;
|
|
33
32
|
|
|
34
|
-
export type TableProps<
|
|
35
|
-
TData extends RowData,
|
|
36
|
-
ScrollElement extends Element | Window = Element,
|
|
37
|
-
ItemElement extends Element = HTMLTableRowElement,
|
|
38
|
-
> = TableFlags &
|
|
33
|
+
export type TableProps<TData extends RowData> = TableFlags &
|
|
39
34
|
TableCurrent<TData> &
|
|
40
|
-
Partial<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} & Pick<VirtualizerOptions<ScrollElement, ItemElement>, 'getScrollElement'>
|
|
57
|
-
>;
|
|
35
|
+
Partial<{
|
|
36
|
+
keyAccessor: KeyValue<TData>;
|
|
37
|
+
data: TData[];
|
|
38
|
+
columns: TableColumnDef<TData>[];
|
|
39
|
+
onColumnResize: (state: Record<string, number>) => void;
|
|
40
|
+
columnVisibility: VisibilityState;
|
|
41
|
+
// Controllable row selection
|
|
42
|
+
rowSelection: RowSelectionState;
|
|
43
|
+
defaultRowSelection: RowSelectionState;
|
|
44
|
+
onRowSelectionChange: (rowSelection: RowSelectionState) => void;
|
|
45
|
+
// Derived from row selection
|
|
46
|
+
onDataSelectionChange: (dataSelection: TData[]) => void;
|
|
47
|
+
// `table` element props
|
|
48
|
+
classNames: ClassNameValue;
|
|
49
|
+
pinLastRow: boolean;
|
|
50
|
+
}>;
|
|
58
51
|
|
|
59
52
|
export type { RowSelectionState, VisibilityState, TableColumnDef, KeyValue };
|
|
60
53
|
|