@dxos/react-ui-table 0.4.10-main.3e0c8a5 → 0.4.10-main.3f5e2d2
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 +676 -397
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/{ColumnMenu.d.ts → ColumnMenu/ColumnMenu.d.ts} +1 -1
- 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} +6 -2
- package/dist/types/src/components/ColumnMenu/ColumnMenu.stories.d.ts.map +1 -0
- package/dist/types/src/components/{ColumnSettingsForm.d.ts → ColumnMenu/ColumnSettingsForm.d.ts} +5 -4
- 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 +32 -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.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/package.json +23 -17
- package/src/components/{ColumnMenu.stories.tsx → ColumnMenu/ColumnMenu.stories.tsx} +31 -1
- package/src/components/{ColumnMenu.tsx → ColumnMenu/ColumnMenu.tsx} +36 -34
- package/src/components/{ColumnSettingsForm.tsx → ColumnMenu/ColumnSettingsForm.tsx} +83 -75
- package/src/components/ColumnMenu/index.ts +6 -0
- package/src/components/Table/Table.stories.tsx +244 -74
- package/src/components/Table/Table.tsx +81 -97
- 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/components/index.ts +1 -1
- package/src/helpers.tsx +31 -7
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useColumnSizing.ts +45 -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/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/ColumnSettingsForm.d.ts.map +0 -1
- package/dist/types/src/components/hooks/useColumnSorting.d.ts.map +0 -1
- package/dist/types/src/components/hooks/usePinLastRow.d.ts +0 -3
- package/dist/types/src/components/hooks/usePinLastRow.d.ts.map +0 -1
- package/dist/types/src/schema.d.ts.map +0 -1
- package/src/components/hooks/usePinLastRow.ts +0 -43
- /package/dist/types/src/{components/hooks → hooks}/useColumnSorting.d.ts +0 -0
- /package/src/{components/hooks → hooks}/useColumnSorting.ts +0 -0
- /package/src/{schema.ts → schema/types.ts} +0 -0
|
@@ -1,35 +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
|
-
import { usePinLastRow } from '../hooks/usePinLastRow';
|
|
31
29
|
|
|
32
|
-
|
|
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>) => {
|
|
33
58
|
const {
|
|
34
59
|
role,
|
|
35
60
|
onColumnResize,
|
|
@@ -38,62 +63,26 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
38
63
|
rowsSelectable,
|
|
39
64
|
debug,
|
|
40
65
|
onDataSelectionChange,
|
|
41
|
-
getScrollElement,
|
|
42
66
|
pinLastRow,
|
|
43
67
|
} = props;
|
|
44
68
|
|
|
45
69
|
const columns = useDefaultValue(props.columns, []);
|
|
46
|
-
const
|
|
70
|
+
const incomingData = useDefaultValue(props.data, []);
|
|
47
71
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
const [columnsInitialised, setColumnsInitialised] = useState(false);
|
|
51
|
-
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({});
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (columnsInitialised) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
setColumnSizing(
|
|
59
|
-
columns
|
|
60
|
-
.filter((column) => !!column.size && (column as any).prop !== undefined)
|
|
61
|
-
.reduce<ColumnSizingState>((state, column) => {
|
|
62
|
-
state[(column as any).prop] = column.size!;
|
|
63
|
-
return state;
|
|
64
|
-
}, {}),
|
|
65
|
-
);
|
|
72
|
+
const [data, setData] = useState([...incomingData]);
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
// Reactivity workaround: https://github.com/dxos/dxos/issues/6376.
|
|
75
|
+
const forceUpdate = useCallback(() => setData([...incomingData]), [incomingData]);
|
|
76
|
+
useEffect(() => forceUpdate(), [JSON.stringify(incomingData), forceUpdate]);
|
|
69
77
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
// Notify on column resize.
|
|
73
|
-
const notifyColumnResize = useCallback(() => onColumnResize?.(columnSizing), [onColumnResize, columnSizing]);
|
|
74
|
-
useOnTransition(columnSizingInfo.isResizingColumn, (v) => typeof v === 'string', false, notifyColumnResize);
|
|
78
|
+
const TableProvider = UntypedTableProvider as TypedTableProvider<TData>;
|
|
75
79
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
defaultProp: props.defaultRowSelection,
|
|
80
|
+
const { columnSizing, setColumnSizing, columnSizingInfo, setColumnSizingInfo } = useColumnResizing({
|
|
81
|
+
columns,
|
|
82
|
+
onColumnResize,
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
const handleRowSelectionChange = useCallback<OnChangeFn<RowSelectionState>>(
|
|
84
|
-
(updaterOrValue) => {
|
|
85
|
-
const nextRowSelection = typeof updaterOrValue === 'function' ? updaterOrValue(rowSelection) : updaterOrValue;
|
|
86
|
-
if (rowsSelectable === 'multi') {
|
|
87
|
-
setRowSelection(nextRowSelection);
|
|
88
|
-
} else if (rowsSelectable) {
|
|
89
|
-
const nextRowSelectionKey = Object.keys(nextRowSelection).filter((id) => !rowSelection[id])[0];
|
|
90
|
-
setRowSelection(nextRowSelectionKey ? { [nextRowSelectionKey]: true } : {});
|
|
91
|
-
} else {
|
|
92
|
-
setRowSelection({});
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
[rowsSelectable, setRowSelection, rowSelection],
|
|
96
|
-
);
|
|
85
|
+
const { rowSelection, handleRowSelectionChange } = useRowSelection(props);
|
|
97
86
|
|
|
98
87
|
const [grouping, handleGroupingChange] = useState<GroupingState>(props.grouping ?? []);
|
|
99
88
|
useEffect(() => handleGroupingChange(props.grouping ?? []), [handleGroupingChange, props.grouping]);
|
|
@@ -150,15 +139,11 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
150
139
|
onDataSelectionChange?.(Object.keys(rowSelection).map((id) => table.getRowModel().rowsById[id].original));
|
|
151
140
|
}, [onDataSelectionChange, rowSelection, table]);
|
|
152
141
|
|
|
153
|
-
usePinLastRow(pinLastRow, table, data
|
|
142
|
+
usePinLastRow(pinLastRow, table, data);
|
|
154
143
|
|
|
155
144
|
// Create additional expansion column if all columns have fixed width.
|
|
156
145
|
const expand = false; // columns.map((column) => column.size).filter(Boolean).length === columns?.length;
|
|
157
146
|
|
|
158
|
-
if (!getScrollElement) {
|
|
159
|
-
log.warn('Table: getScrollElement is not set. This is required for virtualized tables.');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
147
|
return (
|
|
163
148
|
<TableProvider
|
|
164
149
|
{...props}
|
|
@@ -167,22 +152,18 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
167
152
|
expand={expand}
|
|
168
153
|
isGrid={role === 'grid' || role === 'treegrid'}
|
|
169
154
|
>
|
|
170
|
-
<TableImpl<TData> debug={false}
|
|
155
|
+
<TableImpl<TData> debug={false} {...props} />
|
|
171
156
|
</TableProvider>
|
|
172
157
|
);
|
|
173
158
|
};
|
|
174
159
|
|
|
160
|
+
// TODO(Zan): Smush this into the Table component.
|
|
175
161
|
/**
|
|
176
162
|
* Pure implementation of table outside of context set-up.
|
|
177
163
|
*/
|
|
178
164
|
const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
179
|
-
const { debug, classNames,
|
|
180
|
-
const { table } = useTableContext();
|
|
181
|
-
|
|
182
|
-
const centerRows = table.getCenterRows();
|
|
183
|
-
const bottomRows = table.getBottomRows();
|
|
184
|
-
|
|
185
|
-
const rows = [...centerRows, ...bottomRows];
|
|
165
|
+
const { debug, classNames, role, footer, grouping, fullWidth } = props;
|
|
166
|
+
const { table } = useTableContext<TData>();
|
|
186
167
|
|
|
187
168
|
if (debug) {
|
|
188
169
|
return (
|
|
@@ -192,9 +173,10 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
192
173
|
);
|
|
193
174
|
}
|
|
194
175
|
|
|
195
|
-
const virtualisable = getScrollElement !== undefined;
|
|
196
176
|
const isResizingColumn = table.getState().columnSizingInfo.isResizingColumn;
|
|
197
177
|
|
|
178
|
+
const TableComponent = isResizingColumn ? MemoizedVirtualisedTableContent : VirtualizedTableContent;
|
|
179
|
+
|
|
198
180
|
return (
|
|
199
181
|
<table
|
|
200
182
|
role={role}
|
|
@@ -203,39 +185,33 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
|
|
|
203
185
|
>
|
|
204
186
|
<TableHead />
|
|
205
187
|
|
|
206
|
-
{grouping?.length !== 0 ?
|
|
207
|
-
virtualisable ? (
|
|
208
|
-
isResizingColumn ? (
|
|
209
|
-
<MemoizedVirtualisedTableContent getScrollElement={getScrollElement} />
|
|
210
|
-
) : (
|
|
211
|
-
<VirtualizedTableContent getScrollElement={getScrollElement} />
|
|
212
|
-
)
|
|
213
|
-
) : (
|
|
214
|
-
<TableBody rows={rows} />
|
|
215
|
-
)
|
|
216
|
-
) : (
|
|
217
|
-
<GroupedTableContent />
|
|
218
|
-
)}
|
|
188
|
+
{grouping?.length !== 0 ? <TableComponent /> : <GroupedTableContent />}
|
|
219
189
|
|
|
220
190
|
{footer && <TableFooter />}
|
|
221
191
|
</table>
|
|
222
192
|
);
|
|
223
193
|
};
|
|
224
194
|
|
|
225
|
-
const VirtualizedTableContent = ({
|
|
226
|
-
getScrollElement,
|
|
227
|
-
}: Pick<VirtualizerOptions<Element, Element>, 'getScrollElement'>) => {
|
|
195
|
+
const VirtualizedTableContent = () => {
|
|
228
196
|
const { table } = useTableContext();
|
|
197
|
+
const { virtualizer, dispatch } = useContext(TableRootContext);
|
|
229
198
|
|
|
230
199
|
const centerRows = table.getCenterRows();
|
|
231
|
-
|
|
200
|
+
let pinnedRows = [] as Row<unknown>[];
|
|
232
201
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
dispatch({ type: 'updateTableCount', count: centerRows.length });
|
|
204
|
+
}, [centerRows.length]);
|
|
205
|
+
|
|
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;
|
|
239
215
|
|
|
240
216
|
const virtualRows = getVirtualItems();
|
|
241
217
|
const totalSize = getTotalSize();
|
|
@@ -243,10 +219,12 @@ const VirtualizedTableContent = ({
|
|
|
243
219
|
const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0;
|
|
244
220
|
const paddingBottom = virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0;
|
|
245
221
|
|
|
246
|
-
const rowsToRender = [...virtualRows.map((virtualRow) => centerRows[virtualRow.index]), ...pinnedRows]
|
|
222
|
+
const rowsToRender = [...virtualRows.map((virtualRow) => centerRows[virtualRow.index]), ...pinnedRows].filter(
|
|
223
|
+
(r) => !!r,
|
|
224
|
+
);
|
|
247
225
|
|
|
248
226
|
return (
|
|
249
|
-
|
|
227
|
+
<Fragment>
|
|
250
228
|
{paddingTop > 0 && (
|
|
251
229
|
<tbody role='none'>
|
|
252
230
|
<tr role='none'>
|
|
@@ -262,7 +240,7 @@ const VirtualizedTableContent = ({
|
|
|
262
240
|
</tr>
|
|
263
241
|
</tbody>
|
|
264
242
|
)}
|
|
265
|
-
|
|
243
|
+
</Fragment>
|
|
266
244
|
);
|
|
267
245
|
};
|
|
268
246
|
|
|
@@ -299,3 +277,9 @@ const GroupedTableContent = () => {
|
|
|
299
277
|
</>
|
|
300
278
|
);
|
|
301
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
|
|
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();
|
|
@@ -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,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Row, type Table } from '@tanstack/react-table';
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { useTableRootContext } from '../components';
|
|
9
|
+
|
|
10
|
+
export const usePinLastRow = (pinLastRow: boolean | undefined, table: Table<any>, data: any[]) => {
|
|
11
|
+
const { scrollContextRef } = useTableRootContext();
|
|
12
|
+
const [pinnedRowKey, setPinnedRowKey] = useState<string>();
|
|
13
|
+
|
|
14
|
+
// Scrolls to the bottom of a scrollable element.
|
|
15
|
+
const scrollToBottom = useCallback(() => {
|
|
16
|
+
const element = scrollContextRef.current;
|
|
17
|
+
const scrollHeight = element?.scrollHeight;
|
|
18
|
+
|
|
19
|
+
if (element && scrollHeight) {
|
|
20
|
+
requestAnimationFrame(() => element.scrollTo({ top: scrollHeight }));
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const pinRow = useCallback(
|
|
25
|
+
(rowToPin: Row<any>) => {
|
|
26
|
+
table.resetRowPinning();
|
|
27
|
+
rowToPin.pin('bottom');
|
|
28
|
+
setPinnedRowKey(rowToPin.id);
|
|
29
|
+
},
|
|
30
|
+
[table],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const clearRowPinning = useCallback(() => {
|
|
34
|
+
table.resetRowPinning();
|
|
35
|
+
setPinnedRowKey(undefined);
|
|
36
|
+
}, [table]);
|
|
37
|
+
|
|
38
|
+
// Define the logic for pinning the last row in a callback.
|
|
39
|
+
const pinLastRowCallback = useCallback(() => {
|
|
40
|
+
const rows = table.getRowModel().rows;
|
|
41
|
+
|
|
42
|
+
if (rows.length === 0) {
|
|
43
|
+
return clearRowPinning();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lastRow = rows[rows.length - 1];
|
|
47
|
+
if (lastRow.id === pinnedRowKey) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pinRow(lastRow);
|
|
52
|
+
|
|
53
|
+
if (pinnedRowKey !== undefined) {
|
|
54
|
+
scrollToBottom();
|
|
55
|
+
}
|
|
56
|
+
}, [table, scrollToBottom, pinnedRowKey]);
|
|
57
|
+
|
|
58
|
+
// Effect to handle pinning logic when data changes.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (pinLastRow) {
|
|
61
|
+
pinLastRowCallback();
|
|
62
|
+
}
|
|
63
|
+
}, [data, pinLastRow, pinLastRowCallback]);
|
|
64
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
5
|
+
import { type OnChangeFn, type RowSelectionState } from '@tanstack/react-table';
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type TableProps } from '../components/Table';
|
|
9
|
+
|
|
10
|
+
export const useRowSelection = (
|
|
11
|
+
props: Pick<
|
|
12
|
+
TableProps<any>,
|
|
13
|
+
'rowsSelectable' | 'defaultRowSelection' | 'rowSelection' | 'onRowSelectionChange' | 'rowsSelectable'
|
|
14
|
+
>,
|
|
15
|
+
) => {
|
|
16
|
+
const [rowSelection = {}, setRowSelection] = useControllableState({
|
|
17
|
+
prop: props.rowSelection,
|
|
18
|
+
onChange: props.onRowSelectionChange,
|
|
19
|
+
defaultProp: props.defaultRowSelection,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// TODO(thure): Does @tanstack/react-table really need this intervention? It did seem necessary to enforce single-selection...
|
|
23
|
+
const handleRowSelectionChange = useCallback<OnChangeFn<RowSelectionState>>(
|
|
24
|
+
(updaterOrValue) => {
|
|
25
|
+
const nextRowSelection = typeof updaterOrValue === 'function' ? updaterOrValue(rowSelection) : updaterOrValue;
|
|
26
|
+
if (props.rowsSelectable === 'multi') {
|
|
27
|
+
setRowSelection(nextRowSelection);
|
|
28
|
+
} else if (props.rowsSelectable) {
|
|
29
|
+
const nextRowSelectionKey = Object.keys(nextRowSelection).filter((id) => !rowSelection[id])[0];
|
|
30
|
+
setRowSelection(nextRowSelectionKey ? { [nextRowSelectionKey]: true } : {});
|
|
31
|
+
} else {
|
|
32
|
+
setRowSelection({});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[props.rowsSelectable, setRowSelection, rowSelection],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return { rowSelection, handleRowSelectionChange };
|
|
39
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
export { default as translations } from './translations';
|
|
6
|
+
|
|
5
7
|
export * from './components';
|
|
6
8
|
export * from './helpers';
|
|
9
|
+
export * from './hooks';
|
|
7
10
|
export * from './schema';
|
|
8
11
|
export * from './theme';
|
|
9
|
-
export { default as translations } from './translations';
|
|
10
12
|
export * from './types';
|