@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.
Files changed (83) hide show
  1. package/dist/lib/browser/index.mjs +676 -397
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/{ColumnMenu.d.ts → ColumnMenu/ColumnMenu.d.ts} +1 -1
  5. package/dist/types/src/components/ColumnMenu/ColumnMenu.d.ts.map +1 -0
  6. package/dist/types/src/components/{ColumnMenu.stories.d.ts → ColumnMenu/ColumnMenu.stories.d.ts} +6 -2
  7. package/dist/types/src/components/ColumnMenu/ColumnMenu.stories.d.ts.map +1 -0
  8. package/dist/types/src/components/{ColumnSettingsForm.d.ts → ColumnMenu/ColumnSettingsForm.d.ts} +5 -4
  9. package/dist/types/src/components/ColumnMenu/ColumnSettingsForm.d.ts.map +1 -0
  10. package/dist/types/src/components/ColumnMenu/index.d.ts +3 -0
  11. package/dist/types/src/components/ColumnMenu/index.d.ts.map +1 -0
  12. package/dist/types/src/components/Table/Table.d.ts +17 -4
  13. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  14. package/dist/types/src/components/Table/Table.stories.d.ts +32 -1
  15. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Table/TableBody.d.ts.map +1 -1
  17. package/dist/types/src/components/Table/TableRootContext.d.ts +17 -0
  18. package/dist/types/src/components/Table/TableRootContext.d.ts.map +1 -0
  19. package/dist/types/src/components/Table/index.d.ts +1 -0
  20. package/dist/types/src/components/Table/index.d.ts.map +1 -1
  21. package/dist/types/src/components/Table/props.d.ts +2 -3
  22. package/dist/types/src/components/Table/props.d.ts.map +1 -1
  23. package/dist/types/src/components/index.d.ts +1 -1
  24. package/dist/types/src/components/index.d.ts.map +1 -1
  25. package/dist/types/src/helpers.d.ts +4 -0
  26. package/dist/types/src/helpers.d.ts.map +1 -1
  27. package/dist/types/src/hooks/index.d.ts +5 -0
  28. package/dist/types/src/hooks/index.d.ts.map +1 -0
  29. package/dist/types/src/hooks/useColumnSizing.d.ts +12 -0
  30. package/dist/types/src/hooks/useColumnSizing.d.ts.map +1 -0
  31. package/dist/types/src/hooks/useColumnSorting.d.ts.map +1 -0
  32. package/dist/types/src/hooks/usePinLastRow.d.ts +3 -0
  33. package/dist/types/src/hooks/usePinLastRow.d.ts.map +1 -0
  34. package/dist/types/src/hooks/useRowSelection.d.ts +7 -0
  35. package/dist/types/src/hooks/useRowSelection.d.ts.map +1 -0
  36. package/dist/types/src/index.d.ts +2 -1
  37. package/dist/types/src/index.d.ts.map +1 -1
  38. package/dist/types/src/schema/SchemaToTable.stories.d.ts +11 -0
  39. package/dist/types/src/schema/SchemaToTable.stories.d.ts.map +1 -0
  40. package/dist/types/src/schema/index.d.ts +3 -0
  41. package/dist/types/src/schema/index.d.ts.map +1 -0
  42. package/dist/types/src/schema/schema.d.ts +5 -0
  43. package/dist/types/src/schema/schema.d.ts.map +1 -0
  44. package/dist/types/src/schema/schema.test.d.ts +2 -0
  45. package/dist/types/src/schema/schema.test.d.ts.map +1 -0
  46. package/dist/types/src/schema/schemaToColumns.d.ts +4 -0
  47. package/dist/types/src/schema/schemaToColumns.d.ts.map +1 -0
  48. package/dist/types/src/{schema.d.ts → schema/types.d.ts} +1 -1
  49. package/dist/types/src/schema/types.d.ts.map +1 -0
  50. package/package.json +23 -17
  51. package/src/components/{ColumnMenu.stories.tsx → ColumnMenu/ColumnMenu.stories.tsx} +31 -1
  52. package/src/components/{ColumnMenu.tsx → ColumnMenu/ColumnMenu.tsx} +36 -34
  53. package/src/components/{ColumnSettingsForm.tsx → ColumnMenu/ColumnSettingsForm.tsx} +83 -75
  54. package/src/components/ColumnMenu/index.ts +6 -0
  55. package/src/components/Table/Table.stories.tsx +244 -74
  56. package/src/components/Table/Table.tsx +81 -97
  57. package/src/components/Table/TableBody.tsx +6 -1
  58. package/src/components/Table/TableRootContext.ts +39 -0
  59. package/src/components/Table/index.ts +1 -0
  60. package/src/components/Table/props.ts +17 -24
  61. package/src/components/index.ts +1 -1
  62. package/src/helpers.tsx +31 -7
  63. package/src/hooks/index.ts +8 -0
  64. package/src/hooks/useColumnSizing.ts +45 -0
  65. package/src/hooks/usePinLastRow.ts +64 -0
  66. package/src/hooks/useRowSelection.ts +39 -0
  67. package/src/index.ts +3 -1
  68. package/src/schema/SchemaToTable.stories.tsx +71 -0
  69. package/src/schema/index.ts +6 -0
  70. package/src/schema/schema.test.ts +148 -0
  71. package/src/schema/schema.ts +92 -0
  72. package/src/schema/schemaToColumns.ts +48 -0
  73. package/dist/types/src/components/ColumnMenu.d.ts.map +0 -1
  74. package/dist/types/src/components/ColumnMenu.stories.d.ts.map +0 -1
  75. package/dist/types/src/components/ColumnSettingsForm.d.ts.map +0 -1
  76. package/dist/types/src/components/hooks/useColumnSorting.d.ts.map +0 -1
  77. package/dist/types/src/components/hooks/usePinLastRow.d.ts +0 -3
  78. package/dist/types/src/components/hooks/usePinLastRow.d.ts.map +0 -1
  79. package/dist/types/src/schema.d.ts.map +0 -1
  80. package/src/components/hooks/usePinLastRow.ts +0 -43
  81. /package/dist/types/src/{components/hooks → hooks}/useColumnSorting.d.ts +0 -0
  82. /package/src/{components/hooks → hooks}/useColumnSorting.ts +0 -0
  83. /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 { useControllableState } from '@radix-ui/react-use-controllable-state';
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 { useVirtualizer, type VirtualizerOptions } from '@tanstack/react-virtual';
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 { log } from '@dxos/log';
22
- import { useDefaultValue, useOnTransition } from '@dxos/react-ui';
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
- export const Table = <TData extends RowData>(props: TableProps<TData>) => {
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 data = useDefaultValue(props.data, []);
70
+ const incomingData = useDefaultValue(props.data, []);
47
71
 
48
- const TableProvider = UntypedTableProvider as TypedTableProvider<TData>;
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
- setColumnsInitialised(true);
68
- }, [columns, setColumnSizing]);
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 [columnSizingInfo, setColumnSizingInfo] = useState<ColumnSizingInfoState>({} as ColumnSizingInfoState);
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 [rowSelection = {}, setRowSelection] = useControllableState({
77
- prop: props.rowSelection,
78
- onChange: props.onRowSelectionChange,
79
- defaultProp: props.defaultRowSelection,
80
+ const { columnSizing, setColumnSizing, columnSizingInfo, setColumnSizingInfo } = useColumnResizing({
81
+ columns,
82
+ onColumnResize,
80
83
  });
81
84
 
82
- // TODO(thure): Does @tanstack/react-table really need this intervention? It did seem necessary to enforce single-selection...
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, getScrollElement);
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} getScrollElement={getScrollElement} {...props} />
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, getScrollElement, role, footer, grouping, fullWidth } = props;
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
- const pinnedRows = table.getBottomRows();
200
+ let pinnedRows = [] as Row<unknown>[];
232
201
 
233
- const { getTotalSize, getVirtualItems } = useVirtualizer({
234
- getScrollElement,
235
- count: centerRows.length,
236
- overscan: 8,
237
- estimateSize: () => 40,
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);
@@ -4,3 +4,4 @@
4
4
 
5
5
  export * from './Table';
6
6
  export * from './props';
7
+ export * from './TableRootContext';
@@ -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
- keyAccessor: KeyValue<TData>;
43
- data: TData[];
44
- columns: TableColumnDef<TData>[];
45
- onColumnResize: (state: Record<string, number>) => void;
46
- columnVisibility: VisibilityState;
47
- // Controllable row selection
48
- rowSelection: RowSelectionState;
49
- defaultRowSelection: RowSelectionState;
50
- onRowSelectionChange: (rowSelection: RowSelectionState) => void;
51
- // Derived from row selection
52
- onDataSelectionChange: (dataSelection: TData[]) => void;
53
- // `table` element props
54
- classNames: ClassNameValue;
55
- pinLastRow: boolean;
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
 
@@ -2,6 +2,6 @@
2
2
  // Copyright 2022 DXOS.org
3
3
  //
4
4
 
5
+ export * from './Cell';
5
6
  export * from './ColumnMenu';
6
7
  export * from './Table';
7
- export * from './Cell';
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
- const [value, setValue] = useState(() => {
105
+
106
+ const getInitialValue = useCallback(() => {
106
107
  // TODO(burdon): Temporary shim for automerge.
107
- if ((initialValue as any)?.content) {
108
- return (initialValue as any)?.content;
109
- }
108
+ return (initialValue as any)?.content ?? initialValue;
109
+ }, [initialValue]);
110
+
111
+ const [value, setValue] = useState(getInitialValue);
110
112
 
111
- return initialValue;
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,8 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './useColumnSizing';
6
+ export * from './useColumnSorting';
7
+ export * from './usePinLastRow';
8
+ export * from './useRowSelection';
@@ -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';