@dxos/react-ui-table 0.4.9 → 0.4.10-main.05b9ab6
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 +786 -367
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/ColumnMenu/ColumnMenu.d.ts +19 -0
- package/dist/types/src/components/ColumnMenu/ColumnMenu.d.ts.map +1 -0
- package/dist/types/src/components/{ColumnMenu.stories.d.ts → ColumnMenu/ColumnMenu.stories.d.ts} +10 -2
- package/dist/types/src/components/ColumnMenu/ColumnMenu.stories.d.ts.map +1 -0
- package/dist/types/src/components/ColumnMenu/ColumnSettingsForm.d.ts +11 -0
- package/dist/types/src/components/ColumnMenu/ColumnSettingsForm.d.ts.map +1 -0
- package/dist/types/src/components/ColumnMenu/index.d.ts +3 -0
- package/dist/types/src/components/ColumnMenu/index.d.ts.map +1 -0
- 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 +35 -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/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/helpers.d.ts +4 -0
- package/dist/types/src/helpers.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +5 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useColumnSizing.d.ts +12 -0
- package/dist/types/src/hooks/useColumnSizing.d.ts.map +1 -0
- package/dist/types/src/hooks/useColumnSorting.d.ts +9 -0
- package/dist/types/src/hooks/useColumnSorting.d.ts.map +1 -0
- package/dist/types/src/hooks/usePinLastRow.d.ts +3 -0
- package/dist/types/src/hooks/usePinLastRow.d.ts.map +1 -0
- package/dist/types/src/hooks/useRowSelection.d.ts +7 -0
- package/dist/types/src/hooks/useRowSelection.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/schema/SchemaToTable.stories.d.ts +11 -0
- package/dist/types/src/schema/SchemaToTable.stories.d.ts.map +1 -0
- package/dist/types/src/schema/index.d.ts +3 -0
- package/dist/types/src/schema/index.d.ts.map +1 -0
- package/dist/types/src/schema/schema.d.ts +5 -0
- package/dist/types/src/schema/schema.d.ts.map +1 -0
- package/dist/types/src/schema/schema.test.d.ts +2 -0
- package/dist/types/src/schema/schema.test.d.ts.map +1 -0
- package/dist/types/src/schema/schemaToColumns.d.ts +4 -0
- package/dist/types/src/schema/schemaToColumns.d.ts.map +1 -0
- package/dist/types/src/{schema.d.ts → schema/types.d.ts} +1 -1
- package/dist/types/src/schema/types.d.ts.map +1 -0
- package/dist/types/src/theme.d.ts +1 -0
- package/dist/types/src/theme.d.ts.map +1 -1
- package/package.json +22 -16
- package/src/components/ColumnMenu/ColumnMenu.stories.tsx +127 -0
- package/src/components/ColumnMenu/ColumnMenu.tsx +168 -0
- package/src/components/ColumnMenu/ColumnSettingsForm.tsx +214 -0
- package/src/components/ColumnMenu/index.ts +6 -0
- package/src/components/Table/Table.stories.tsx +260 -59
- package/src/components/Table/Table.tsx +82 -107
- package/src/components/Table/TableBody.tsx +12 -2
- 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/components/index.ts +1 -1
- package/src/helpers.tsx +32 -8
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useColumnSizing.ts +45 -0
- package/src/hooks/useColumnSorting.ts +54 -0
- package/src/hooks/usePinLastRow.ts +64 -0
- package/src/hooks/useRowSelection.ts +39 -0
- package/src/index.ts +3 -1
- package/src/schema/SchemaToTable.stories.tsx +71 -0
- package/src/schema/index.ts +6 -0
- package/src/schema/schema.test.ts +148 -0
- package/src/schema/schema.ts +92 -0
- package/src/schema/schemaToColumns.ts +48 -0
- package/src/theme.ts +3 -2
- package/dist/types/src/components/ColumnMenu.d.ts +0 -13
- package/dist/types/src/components/ColumnMenu.d.ts.map +0 -1
- package/dist/types/src/components/ColumnMenu.stories.d.ts.map +0 -1
- package/dist/types/src/components/SortButton.d.ts +0 -8
- package/dist/types/src/components/SortButton.d.ts.map +0 -1
- package/dist/types/src/schema.d.ts.map +0 -1
- package/src/components/ColumnMenu.stories.tsx +0 -46
- package/src/components/ColumnMenu.tsx +0 -248
- package/src/components/SortButton.tsx +0 -34
- /package/src/{schema.ts → schema/types.ts} +0 -0
|
@@ -1,34 +1,60 @@
|
|
|
1
1
|
//
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
|
-
import {
|
|
4
|
+
import { type Primitive } from '@radix-ui/react-primitive';
|
|
5
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
5
6
|
import {
|
|
6
7
|
getCoreRowModel,
|
|
7
8
|
getSortedRowModel,
|
|
8
9
|
useReactTable,
|
|
9
|
-
type ColumnSizingInfoState,
|
|
10
10
|
type GroupingState,
|
|
11
11
|
type RowData,
|
|
12
|
-
type ColumnSizingState,
|
|
13
|
-
type OnChangeFn,
|
|
14
|
-
type RowSelectionState,
|
|
15
12
|
getGroupedRowModel,
|
|
16
13
|
getExpandedRowModel,
|
|
14
|
+
type Row,
|
|
17
15
|
} from '@tanstack/react-table';
|
|
18
|
-
import {
|
|
19
|
-
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
|
16
|
+
import React, { type ComponentPropsWithoutRef, Fragment, useCallback, useEffect, useState, useContext } from 'react';
|
|
20
17
|
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
18
|
+
import { type ThemedClassName, useDefaultValue } from '@dxos/react-ui';
|
|
19
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
23
20
|
|
|
24
21
|
import { TableBody } from './TableBody';
|
|
25
22
|
import { type TypedTableProvider, TableProvider as UntypedTableProvider, useTableContext } from './TableContext';
|
|
26
23
|
import { TableFooter } from './TableFooter';
|
|
27
24
|
import { TableHead } from './TableHead';
|
|
25
|
+
import { TableRootContext, useTableRootContext } from './TableRootContext';
|
|
28
26
|
import { type TableProps } from './props';
|
|
27
|
+
import { useColumnResizing, usePinLastRow, useRowSelection } from '../../hooks';
|
|
29
28
|
import { groupTh, tableRoot } from '../../theme';
|
|
30
29
|
|
|
31
|
-
|
|
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>) => {
|
|
32
58
|
const {
|
|
33
59
|
role,
|
|
34
60
|
onColumnResize,
|
|
@@ -37,62 +63,26 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
37
63
|
rowsSelectable,
|
|
38
64
|
debug,
|
|
39
65
|
onDataSelectionChange,
|
|
40
|
-
getScrollElement,
|
|
41
66
|
pinLastRow,
|
|
42
67
|
} = props;
|
|
43
68
|
|
|
44
69
|
const columns = useDefaultValue(props.columns, []);
|
|
45
|
-
const
|
|
70
|
+
const incomingData = useDefaultValue(props.data, []);
|
|
46
71
|
|
|
47
|
-
const
|
|
72
|
+
const [data, setData] = useState([...incomingData]);
|
|
48
73
|
|
|
49
|
-
|
|
50
|
-
const
|
|
74
|
+
// Reactivity workaround: https://github.com/dxos/dxos/issues/6376.
|
|
75
|
+
const forceUpdate = useCallback(() => setData([...incomingData]), [incomingData]);
|
|
76
|
+
useEffect(() => forceUpdate(), [JSON.stringify(incomingData), forceUpdate]);
|
|
51
77
|
|
|
52
|
-
|
|
53
|
-
if (columnsInitialised) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
setColumnSizing(
|
|
58
|
-
columns
|
|
59
|
-
.filter((column) => !!column.size && (column as any).prop !== undefined)
|
|
60
|
-
.reduce<ColumnSizingState>((state, column) => {
|
|
61
|
-
state[(column as any).prop] = column.size!;
|
|
62
|
-
return state;
|
|
63
|
-
}, {}),
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
setColumnsInitialised(true);
|
|
67
|
-
}, [columns, setColumnSizing]);
|
|
68
|
-
|
|
69
|
-
const [columnSizingInfo, setColumnSizingInfo] = useState<ColumnSizingInfoState>({} as ColumnSizingInfoState);
|
|
70
|
-
|
|
71
|
-
// Notify on column resize.
|
|
72
|
-
const notifyColumnResize = useCallback(() => onColumnResize?.(columnSizing), [onColumnResize, columnSizing]);
|
|
73
|
-
useOnTransition(columnSizingInfo.isResizingColumn, (v) => typeof v === 'string', false, notifyColumnResize);
|
|
78
|
+
const TableProvider = UntypedTableProvider as TypedTableProvider<TData>;
|
|
74
79
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
defaultProp: props.defaultRowSelection,
|
|
80
|
+
const { columnSizing, setColumnSizing, columnSizingInfo, setColumnSizingInfo } = useColumnResizing({
|
|
81
|
+
columns,
|
|
82
|
+
onColumnResize,
|
|
79
83
|
});
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
const handleRowSelectionChange = useCallback<OnChangeFn<RowSelectionState>>(
|
|
83
|
-
(updaterOrValue) => {
|
|
84
|
-
const nextRowSelection = typeof updaterOrValue === 'function' ? updaterOrValue(rowSelection) : updaterOrValue;
|
|
85
|
-
if (rowsSelectable === 'multi') {
|
|
86
|
-
setRowSelection(nextRowSelection);
|
|
87
|
-
} else if (rowsSelectable) {
|
|
88
|
-
const nextRowSelectionKey = Object.keys(nextRowSelection).filter((id) => !rowSelection[id])[0];
|
|
89
|
-
setRowSelection(nextRowSelectionKey ? { [nextRowSelectionKey]: true } : {});
|
|
90
|
-
} else {
|
|
91
|
-
setRowSelection({});
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
[rowsSelectable, setRowSelection, rowSelection],
|
|
95
|
-
);
|
|
85
|
+
const { rowSelection, handleRowSelectionChange } = useRowSelection(props);
|
|
96
86
|
|
|
97
87
|
const [grouping, handleGroupingChange] = useState<GroupingState>(props.grouping ?? []);
|
|
98
88
|
useEffect(() => handleGroupingChange(props.grouping ?? []), [handleGroupingChange, props.grouping]);
|
|
@@ -149,25 +139,11 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
149
139
|
onDataSelectionChange?.(Object.keys(rowSelection).map((id) => table.getRowModel().rowsById[id].original));
|
|
150
140
|
}, [onDataSelectionChange, rowSelection, table]);
|
|
151
141
|
|
|
152
|
-
|
|
153
|
-
if (!pinLastRow) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Clear row pinning
|
|
158
|
-
table.resetRowPinning();
|
|
159
|
-
|
|
160
|
-
const rows = table.getRowModel().rows;
|
|
161
|
-
rows[rows.length - 1].pin('bottom');
|
|
162
|
-
}, [pinLastRow, table, data]);
|
|
142
|
+
usePinLastRow(pinLastRow, table, data);
|
|
163
143
|
|
|
164
144
|
// Create additional expansion column if all columns have fixed width.
|
|
165
145
|
const expand = false; // columns.map((column) => column.size).filter(Boolean).length === columns?.length;
|
|
166
146
|
|
|
167
|
-
if (!getScrollElement) {
|
|
168
|
-
log.warn('Table: getScrollElement is not set. This is required for virtualized tables.');
|
|
169
|
-
}
|
|
170
|
-
|
|
171
147
|
return (
|
|
172
148
|
<TableProvider
|
|
173
149
|
{...props}
|
|
@@ -176,22 +152,18 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
176
152
|
expand={expand}
|
|
177
153
|
isGrid={role === 'grid' || role === 'treegrid'}
|
|
178
154
|
>
|
|
179
|
-
<TableImpl<TData> debug={false}
|
|
155
|
+
<TableImpl<TData> debug={false} {...props} />
|
|
180
156
|
</TableProvider>
|
|
181
157
|
);
|
|
182
158
|
};
|
|
183
159
|
|
|
160
|
+
// TODO(Zan): Smush this into the Table component.
|
|
184
161
|
/**
|
|
185
162
|
* Pure implementation of table outside of context set-up.
|
|
186
163
|
*/
|
|
187
164
|
const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
188
|
-
const { debug, classNames,
|
|
189
|
-
const { table } = useTableContext();
|
|
190
|
-
|
|
191
|
-
const centerRows = table.getCenterRows();
|
|
192
|
-
const bottomRows = table.getBottomRows();
|
|
193
|
-
|
|
194
|
-
const rows = [...centerRows, ...bottomRows];
|
|
165
|
+
const { debug, classNames, role, footer, grouping, fullWidth } = props;
|
|
166
|
+
const { table } = useTableContext<TData>();
|
|
195
167
|
|
|
196
168
|
if (debug) {
|
|
197
169
|
return (
|
|
@@ -201,9 +173,10 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
201
173
|
);
|
|
202
174
|
}
|
|
203
175
|
|
|
204
|
-
const virtualisable = getScrollElement !== undefined;
|
|
205
176
|
const isResizingColumn = table.getState().columnSizingInfo.isResizingColumn;
|
|
206
177
|
|
|
178
|
+
const TableComponent = isResizingColumn ? MemoizedVirtualisedTableContent : VirtualizedTableContent;
|
|
179
|
+
|
|
207
180
|
return (
|
|
208
181
|
<table
|
|
209
182
|
role={role}
|
|
@@ -212,41 +185,33 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
212
185
|
>
|
|
213
186
|
<TableHead />
|
|
214
187
|
|
|
215
|
-
{grouping?.length !== 0 ?
|
|
216
|
-
virtualisable ? (
|
|
217
|
-
isResizingColumn ? (
|
|
218
|
-
<MemoizedVirtualisedTableContent getScrollElement={getScrollElement} />
|
|
219
|
-
) : (
|
|
220
|
-
<VirtualizedTableContent getScrollElement={getScrollElement} />
|
|
221
|
-
)
|
|
222
|
-
) : (
|
|
223
|
-
<TableBody rows={rows} />
|
|
224
|
-
)
|
|
225
|
-
) : (
|
|
226
|
-
<GroupedTableContent />
|
|
227
|
-
)}
|
|
188
|
+
{grouping?.length !== 0 ? <TableComponent /> : <GroupedTableContent />}
|
|
228
189
|
|
|
229
190
|
{footer && <TableFooter />}
|
|
230
191
|
</table>
|
|
231
192
|
);
|
|
232
193
|
};
|
|
233
194
|
|
|
234
|
-
const VirtualizedTableContent = ({
|
|
235
|
-
getScrollElement,
|
|
236
|
-
}: Pick<VirtualizerOptions<Element, Element>, 'getScrollElement'>) => {
|
|
195
|
+
const VirtualizedTableContent = () => {
|
|
237
196
|
const { table } = useTableContext();
|
|
197
|
+
const { virtualizer, dispatch } = useContext(TableRootContext);
|
|
238
198
|
|
|
239
199
|
const centerRows = table.getCenterRows();
|
|
240
|
-
|
|
200
|
+
let pinnedRows = [] as Row<unknown>[];
|
|
241
201
|
|
|
242
|
-
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
dispatch({ type: 'updateTableCount', count: centerRows.length });
|
|
204
|
+
}, [centerRows.length]);
|
|
243
205
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
206
|
+
try {
|
|
207
|
+
// TODO(zan): Work out how to sync the row pinning with rendering
|
|
208
|
+
// This is a hack because the first call to this function after a row is deleted will throw an error
|
|
209
|
+
// because the row index is no longer in the table
|
|
210
|
+
// The pin last row hook updates the pinned row key after the row is deleted
|
|
211
|
+
pinnedRows = table.getBottomRows();
|
|
212
|
+
} catch (_) {} // Ignore error
|
|
213
|
+
|
|
214
|
+
const { getTotalSize, getVirtualItems } = virtualizer;
|
|
250
215
|
|
|
251
216
|
const virtualRows = getVirtualItems();
|
|
252
217
|
const totalSize = getTotalSize();
|
|
@@ -254,8 +219,12 @@ const VirtualizedTableContent = ({
|
|
|
254
219
|
const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0;
|
|
255
220
|
const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0;
|
|
256
221
|
|
|
222
|
+
const rowsToRender = [...virtualRows.map((virtualRow) => centerRows[virtualRow.index]), ...pinnedRows].filter(
|
|
223
|
+
(r) => !!r,
|
|
224
|
+
);
|
|
225
|
+
|
|
257
226
|
return (
|
|
258
|
-
|
|
227
|
+
<Fragment>
|
|
259
228
|
{paddingTop > 0 && (
|
|
260
229
|
<tbody role='none'>
|
|
261
230
|
<tr role='none'>
|
|
@@ -263,7 +232,7 @@ const VirtualizedTableContent = ({
|
|
|
263
232
|
</tr>
|
|
264
233
|
</tbody>
|
|
265
234
|
)}
|
|
266
|
-
<TableBody rows={
|
|
235
|
+
<TableBody rows={rowsToRender} />
|
|
267
236
|
{paddingBottom > 0 && (
|
|
268
237
|
<tbody role='none'>
|
|
269
238
|
<tr role='none'>
|
|
@@ -271,7 +240,7 @@ const VirtualizedTableContent = ({
|
|
|
271
240
|
</tr>
|
|
272
241
|
</tbody>
|
|
273
242
|
)}
|
|
274
|
-
|
|
243
|
+
</Fragment>
|
|
275
244
|
);
|
|
276
245
|
};
|
|
277
246
|
|
|
@@ -308,3 +277,9 @@ const GroupedTableContent = () => {
|
|
|
308
277
|
</>
|
|
309
278
|
);
|
|
310
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,15 +24,24 @@ 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) => {
|
|
29
32
|
const isCurrent = currentDatum === row.original;
|
|
30
33
|
const isSelected = rowSelection?.[row.id];
|
|
34
|
+
|
|
35
|
+
const isPinned = row.getIsPinned();
|
|
36
|
+
|
|
37
|
+
const classNames = tbodyTr({ canBeCurrent, isPinned: isPinned !== false });
|
|
38
|
+
|
|
31
39
|
return (
|
|
32
40
|
<tr
|
|
33
41
|
key={keyAccessor ? keyAccessor(row.original) : row.id}
|
|
34
|
-
className={
|
|
42
|
+
className={classNames}
|
|
43
|
+
data-index={row.index}
|
|
44
|
+
ref={virtualizer.measureElement}
|
|
35
45
|
{...(isCurrent && { 'aria-current': 'location' })}
|
|
36
46
|
{...(isSelected && { 'aria-selected': 'true' })}
|
|
37
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
|
|
package/src/components/index.ts
CHANGED
package/src/helpers.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from '@tanstack/react-table';
|
|
15
15
|
import { format } from 'date-fns/format';
|
|
16
16
|
import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
|
|
17
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
17
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
18
18
|
|
|
19
19
|
import { type PublicKey } from '@dxos/keys';
|
|
20
20
|
import { log } from '@dxos/log';
|
|
@@ -102,14 +102,16 @@ const StringBuilderCell = <TData extends RowData>(cellContext: CellContext<TData
|
|
|
102
102
|
|
|
103
103
|
// https://tanstack.com/table/v8/docs/examples/react/editable-data
|
|
104
104
|
const initialValue = cellContext.getValue();
|
|
105
|
-
|
|
105
|
+
|
|
106
|
+
const getInitialValue = useCallback(() => {
|
|
106
107
|
// TODO(burdon): Temporary shim for automerge.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
return (initialValue as any)?.content ?? initialValue;
|
|
109
|
+
}, [initialValue]);
|
|
110
|
+
|
|
111
|
+
const [value, setValue] = useState(getInitialValue);
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
// Update value if initialValue changes externally (cell reactivity).
|
|
114
|
+
useEffect(() => setValue(getInitialValue()), [getInitialValue]);
|
|
113
115
|
|
|
114
116
|
const handleSave = () => {
|
|
115
117
|
ref.current && document.activeElement === ref.current && findPrevFocusable(ref.current)?.focus();
|
|
@@ -340,7 +342,7 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
340
342
|
...props.meta,
|
|
341
343
|
onUpdate,
|
|
342
344
|
digits,
|
|
343
|
-
header: {
|
|
345
|
+
header: { ...props.meta?.header },
|
|
344
346
|
cell: { ...props.meta?.cell, classNames },
|
|
345
347
|
},
|
|
346
348
|
};
|
|
@@ -498,4 +500,26 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
498
500
|
...props,
|
|
499
501
|
};
|
|
500
502
|
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* This display column type is for nested/unhandled data to appear as JSON.
|
|
506
|
+
*/
|
|
507
|
+
display({ label, classNames, ...props }: KeyColumnOptions<TData> = {}): Partial<ColumnDef<TData, PublicKey>> {
|
|
508
|
+
return {
|
|
509
|
+
size: 120,
|
|
510
|
+
minSize: 120,
|
|
511
|
+
header: (cell) => label ?? cell.header.id,
|
|
512
|
+
cell: (cell) => {
|
|
513
|
+
const value = cell.getValue();
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<div className='overflow-scroll'>
|
|
517
|
+
<code className='text-xs'>{JSON.stringify(value)}</code>
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
},
|
|
521
|
+
...props,
|
|
522
|
+
meta: { ...props.meta, cell: { ...props.meta?.cell, classNames: ['font-mono', textPadding, classNames] } },
|
|
523
|
+
};
|
|
524
|
+
}
|
|
501
525
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { type ColumnSizingInfoState, type ColumnSizingState } from '@tanstack/react-table';
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useOnTransition } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type TableColumnDef } from '../components/Table';
|
|
10
|
+
|
|
11
|
+
export const useColumnResizing = ({
|
|
12
|
+
columns,
|
|
13
|
+
onColumnResize,
|
|
14
|
+
}: {
|
|
15
|
+
columns: TableColumnDef<any>[];
|
|
16
|
+
onColumnResize: ((state: Record<string, number>) => void) | undefined;
|
|
17
|
+
}) => {
|
|
18
|
+
const [columnsInitialised, setColumnsInitialised] = useState(false);
|
|
19
|
+
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({});
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (columnsInitialised) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setColumnSizing(
|
|
27
|
+
columns
|
|
28
|
+
.filter((column) => !!column.size && (column as any).prop !== undefined)
|
|
29
|
+
.reduce<ColumnSizingState>((state, column) => {
|
|
30
|
+
state[(column as any).prop] = column.size!;
|
|
31
|
+
return state;
|
|
32
|
+
}, {}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
setColumnsInitialised(true);
|
|
36
|
+
}, [columns, setColumnSizing]);
|
|
37
|
+
|
|
38
|
+
const [columnSizingInfo, setColumnSizingInfo] = useState<ColumnSizingInfoState>({} as ColumnSizingInfoState);
|
|
39
|
+
|
|
40
|
+
// Notify on column resize.
|
|
41
|
+
const notifyColumnResize = useCallback(() => onColumnResize?.(columnSizing), [onColumnResize, columnSizing]);
|
|
42
|
+
useOnTransition(columnSizingInfo.isResizingColumn, (v) => typeof v === 'string', false, notifyColumnResize);
|
|
43
|
+
|
|
44
|
+
return { columnSizing, columnSizingInfo, setColumnSizing, setColumnSizingInfo };
|
|
45
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type SortDirection, type Column } from '@tanstack/react-table';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
export const useColumnSorting = (column: Column<any>) => {
|
|
9
|
+
const [sortDirection, setSortDirection] = useState<SortDirection | false>(column.getIsSorted());
|
|
10
|
+
const [firstSort, setFirstSort] = useState<SortDirection | undefined>();
|
|
11
|
+
|
|
12
|
+
const canSort = column.getCanSort();
|
|
13
|
+
|
|
14
|
+
const updateColumnSort = (direction: SortDirection | false) => {
|
|
15
|
+
setSortDirection(direction);
|
|
16
|
+
|
|
17
|
+
switch (direction) {
|
|
18
|
+
case false:
|
|
19
|
+
column.clearSorting();
|
|
20
|
+
break;
|
|
21
|
+
case 'asc':
|
|
22
|
+
column.toggleSorting(false); // descending false
|
|
23
|
+
break;
|
|
24
|
+
case 'desc':
|
|
25
|
+
column.toggleSorting(true); // descending true
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Whichever sort is explicitly set first, will be the first sort direction
|
|
31
|
+
// When toggling, we always go to the other direction, and then to unsorted.
|
|
32
|
+
const onToggleSort = () => {
|
|
33
|
+
if (sortDirection === false) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (sortDirection === firstSort) {
|
|
38
|
+
updateColumnSort(firstSort === 'asc' ? 'desc' : 'asc');
|
|
39
|
+
} else {
|
|
40
|
+
updateColumnSort(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const onSelectSort = (direction: SortDirection) => {
|
|
45
|
+
setFirstSort(direction);
|
|
46
|
+
updateColumnSort(direction);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const onClearSort = () => {
|
|
50
|
+
updateColumnSort(false);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return { canSort, sortDirection, onSelectSort, onToggleSort, onClearSort };
|
|
54
|
+
};
|