@dxos/react-ui-table 0.4.8-main.3a03fb7 → 0.4.8-main.4a229ac

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 (31) hide show
  1. package/dist/lib/browser/index.mjs +276 -191
  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/Cell/Cell.d.ts.map +1 -1
  5. package/dist/types/src/components/ColumnMenu.d.ts.map +1 -1
  6. package/dist/types/src/components/ColumnMenu.stories.d.ts +10 -1
  7. package/dist/types/src/components/ColumnMenu.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/SortButton.d.ts +8 -0
  9. package/dist/types/src/components/SortButton.d.ts.map +1 -0
  10. package/dist/types/src/components/Table/Table.d.ts +2 -0
  11. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  12. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/Table/TableBody.d.ts.map +1 -1
  14. package/dist/types/src/components/Table/TableHead.d.ts.map +1 -1
  15. package/dist/types/src/components/Table/props.d.ts +1 -0
  16. package/dist/types/src/components/Table/props.d.ts.map +1 -1
  17. package/dist/types/src/helpers.d.ts.map +1 -1
  18. package/dist/types/src/theme.d.ts +1 -1
  19. package/dist/types/src/theme.d.ts.map +1 -1
  20. package/package.json +15 -15
  21. package/src/components/Cell/Cell.tsx +4 -1
  22. package/src/components/ColumnMenu.stories.tsx +13 -6
  23. package/src/components/ColumnMenu.tsx +13 -4
  24. package/src/components/SortButton.tsx +34 -0
  25. package/src/components/Table/Table.stories.tsx +51 -62
  26. package/src/components/Table/Table.tsx +64 -41
  27. package/src/components/Table/TableBody.tsx +1 -0
  28. package/src/components/Table/TableHead.tsx +4 -2
  29. package/src/components/Table/props.ts +1 -0
  30. package/src/helpers.tsx +61 -45
  31. package/src/theme.ts +3 -1
@@ -5,12 +5,12 @@
5
5
  import '@dxosTheme';
6
6
 
7
7
  import { Plugs, PlugsConnected } from '@phosphor-icons/react';
8
- import React, { useEffect, useRef, useState } from 'react';
8
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
9
9
 
10
10
  import * as E from '@dxos/echo-schema/schema';
11
11
  import { PublicKey } from '@dxos/keys';
12
12
  import { faker } from '@dxos/random';
13
- import { DensityProvider, AnchoredOverflow } from '@dxos/react-ui';
13
+ import { AnchoredOverflow, DensityProvider } from '@dxos/react-ui';
14
14
  import { withTheme } from '@dxos/storybook-utils';
15
15
  import { range } from '@dxos/util';
16
16
 
@@ -72,7 +72,7 @@ const tableStorySelectModel: SearchListQueryModel<Item> = {
72
72
 
73
73
  const { helper, builder } = createColumnBuilder<Item>();
74
74
 
75
- const columns = (onUpdate?: ValueUpdater<Item, any>): TableColumnDef<Item, any>[] => [
75
+ const makeColumns = (onUpdate?: ValueUpdater<Item, any>): TableColumnDef<Item, any>[] => [
76
76
  helper.display(builder.selectRow()),
77
77
  helper.accessor((item) => item.publicKey, { id: 'key', ...builder.key({ tooltip: true }) }),
78
78
  helper.accessor(
@@ -90,26 +90,14 @@ const columns = (onUpdate?: ValueUpdater<Item, any>): TableColumnDef<Item, any>[
90
90
  builder.number({
91
91
  label: 'Count',
92
92
  meta: { resizable: true },
93
- // TODO(burdon): Sorting.
94
- getGroupingValue: (row) => (row.count ? (row.count < 2_000 ? 'A' : row.count < 5_000 ? 'B' : 'C') : 'D'),
95
- }),
96
- ),
97
- helper.accessor(
98
- 'company',
99
- builder.combobox({
100
- label: 'Company',
101
- model: tableStorySelectModel,
102
93
  onUpdate,
94
+ getGroupingValue: (row) => (row.count ? (row.count < 2000 ? 'A' : row.count < 5000 ? 'B' : 'C') : 'D'),
103
95
  }),
104
96
  ),
97
+ helper.accessor('company', builder.combobox({ label: 'Company', model: tableStorySelectModel, onUpdate })),
105
98
  helper.accessor(
106
99
  'complete',
107
- builder.switch({
108
- // enableGrouping: true,
109
- getGroupingValue: (row) => row.complete === true,
110
- label: '',
111
- onUpdate,
112
- }),
100
+ builder.switch({ getGroupingValue: (row) => row.complete === true, label: '', onUpdate }),
113
101
  ),
114
102
  helper.accessor('complete', builder.icon({ id: 'done', label: '' })),
115
103
  helper.accessor(
@@ -135,18 +123,10 @@ export default {
135
123
  keyAccessor: (item: Item) => item.publicKey.toHex(),
136
124
  },
137
125
  argTypes: {
138
- header: {
139
- control: 'boolean',
140
- },
141
- footer: {
142
- control: 'boolean',
143
- },
144
- border: {
145
- control: 'boolean',
146
- },
147
- fullWidth: {
148
- control: 'boolean',
149
- },
126
+ header: { control: 'boolean' },
127
+ footer: { control: 'boolean' },
128
+ border: { control: 'boolean' },
129
+ fullWidth: { control: 'boolean' },
150
130
  grouping: {
151
131
  control: 'select',
152
132
  options: ['none', 'complete', 'count'],
@@ -164,10 +144,7 @@ export default {
164
144
  limited: { key: false, started: false },
165
145
  },
166
146
  },
167
- rowsSelectable: {
168
- control: 'select',
169
- options: [false, true, 'multi'],
170
- },
147
+ rowsSelectable: { control: 'select', options: [false, true, 'multi'] },
171
148
  },
172
149
  decorators: [
173
150
  withTheme,
@@ -181,21 +158,21 @@ export default {
181
158
 
182
159
  export const Default = {
183
160
  args: {
184
- columns: columns(),
161
+ columns: makeColumns(),
185
162
  data: createItems(20),
186
163
  },
187
164
  };
188
165
 
189
166
  export const Scrolling = {
190
167
  args: {
191
- columns: columns(),
168
+ columns: makeColumns(),
192
169
  data: createItems(200),
193
170
  },
194
171
  };
195
172
 
196
173
  export const Visibility = {
197
174
  args: {
198
- columns: columns(),
175
+ columns: makeColumns(),
199
176
  data: createItems(10),
200
177
  columnVisibility: { key: false, started: false },
201
178
  },
@@ -203,7 +180,7 @@ export const Visibility = {
203
180
 
204
181
  export const Empty = {
205
182
  args: {
206
- columns: columns(),
183
+ columns: makeColumns(),
207
184
  },
208
185
  };
209
186
 
@@ -211,6 +188,7 @@ export const Dynamic = {
211
188
  render: () => {
212
189
  const containerRef = useRef<HTMLDivElement | null>(null);
213
190
  const [items, setItems] = useState<Item[]>(createItems(50));
191
+
214
192
  useEffect(() => {
215
193
  const interval = setInterval(() => {
216
194
  setItems((items) => {
@@ -223,12 +201,14 @@ export const Dynamic = {
223
201
  return () => clearInterval(interval);
224
202
  }, []);
225
203
 
204
+ const columns = useMemo(() => makeColumns(), []);
205
+
226
206
  return (
227
207
  <AnchoredOverflow.Root classNames='max-bs-[80dvh]' ref={containerRef}>
228
208
  <Table<Item>
229
209
  rowsSelectable='multi'
230
210
  keyAccessor={(row) => row.publicKey.toHex()}
231
- columns={columns()}
211
+ columns={columns}
232
212
  data={items}
233
213
  fullWidth
234
214
  footer
@@ -245,9 +225,13 @@ export const Editable = {
245
225
  render: () => {
246
226
  const containerRef = useRef<HTMLDivElement | null>(null);
247
227
  const [items, setItems] = useState<Item[]>(createItems(200));
248
- const onUpdate: ValueUpdater<Item, any> = (item, prop, value) => {
249
- setItems((items) => updateItems(items, item.publicKey, prop, value));
250
- };
228
+
229
+ const onUpdate: ValueUpdater<Item, any> = useCallback(
230
+ (item, prop, value) => setItems((items) => updateItems(items, item.publicKey, prop, value)),
231
+ [setItems],
232
+ );
233
+
234
+ const columns = useMemo(() => makeColumns(onUpdate), [onUpdate]);
251
235
 
252
236
  return (
253
237
  <div ref={containerRef} className='fixed inset-0 overflow-auto'>
@@ -255,7 +239,7 @@ export const Editable = {
255
239
  role='grid'
256
240
  rowsSelectable='multi'
257
241
  keyAccessor={(row) => row.publicKey.toHex()}
258
- columns={columns(onUpdate)}
242
+ columns={columns}
259
243
  data={items}
260
244
  fullWidth
261
245
  stickyHeader
@@ -269,25 +253,27 @@ export const Editable = {
269
253
 
270
254
  export const Resizable = {
271
255
  render: () => {
256
+ const containerRef = useRef<HTMLDivElement | null>(null);
272
257
  const [items, setItems] = useState<Item[]>(createItems(10));
273
- const onUpdate: ValueUpdater<Item, any> = (item, prop, value) => {
274
- setItems((items) => updateItems(items, item.publicKey, prop, value));
275
- };
276
258
 
277
- // const handleColumnResize = (state) => {
278
- // console.log('resize', state);
279
- // };
259
+ const onUpdate: ValueUpdater<Item, any> = useCallback(
260
+ (item, prop, value) => setItems((items) => updateItems(items, item.publicKey, prop, value)),
261
+ [setItems],
262
+ );
280
263
 
281
264
  return (
282
- <Table<Item>
283
- rowsSelectable='multi'
284
- keyAccessor={(row) => row.publicKey.toHex()}
285
- columns={columns(onUpdate)}
286
- data={items}
287
- fullWidth
288
- stickyHeader
289
- // onColumnResize={handleColumnResize}
290
- />
265
+ <div ref={containerRef} className='fixed inset-0 overflow-auto'>
266
+ <Table<Item>
267
+ rowsSelectable='multi'
268
+ keyAccessor={(row) => row.publicKey.toHex()}
269
+ columns={makeColumns(onUpdate)}
270
+ data={items}
271
+ fullWidth
272
+ stickyHeader
273
+ getScrollElement={() => containerRef.current}
274
+ // onColumnResize={handleColumnResize}
275
+ />
276
+ </div>
291
277
  );
292
278
  },
293
279
  };
@@ -297,9 +283,12 @@ export const TenThousandRows = {
297
283
  const containerRef = useRef<HTMLDivElement | null>(null);
298
284
  const [items, setItems] = useState<Item[]>(createItems(10000));
299
285
 
300
- const onUpdate: ValueUpdater<Item, any> = (item, prop, value) => {
301
- setItems((items) => updateItems(items, item.publicKey, prop, value));
302
- };
286
+ const onUpdate: ValueUpdater<Item, any> = useCallback(
287
+ (item, prop, value) => setItems((items) => updateItems(items, item.publicKey, prop, value)),
288
+ [setItems],
289
+ );
290
+
291
+ const columns = useMemo(() => makeColumns(onUpdate), [onUpdate]);
303
292
 
304
293
  return (
305
294
  <div ref={containerRef} className='fixed inset-0 overflow-auto'>
@@ -307,7 +296,7 @@ export const TenThousandRows = {
307
296
  role='grid'
308
297
  rowsSelectable='multi'
309
298
  keyAccessor={(row) => row.publicKey.toHex()}
310
- columns={columns(onUpdate)}
299
+ columns={columns}
311
300
  data={items}
312
301
  fullWidth
313
302
  stickyHeader
@@ -4,6 +4,7 @@
4
4
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
5
5
  import {
6
6
  getCoreRowModel,
7
+ getSortedRowModel,
7
8
  useReactTable,
8
9
  type ColumnSizingInfoState,
9
10
  type GroupingState,
@@ -12,12 +13,14 @@ import {
12
13
  type OnChangeFn,
13
14
  type RowSelectionState,
14
15
  getGroupedRowModel,
16
+ getExpandedRowModel,
15
17
  } from '@tanstack/react-table';
16
18
  import { useVirtualizer, type VirtualizerOptions } from '@tanstack/react-virtual';
17
19
  import React, { Fragment, useCallback, useEffect, useState } from 'react';
18
20
 
19
21
  import { debounce } from '@dxos/async';
20
22
  import { log } from '@dxos/log';
23
+ import { useDefaultValue } from '@dxos/react-ui';
21
24
 
22
25
  import { TableBody } from './TableBody';
23
26
  import { TableProvider as UntypedTableProvider, type TypedTableProvider, useTableContext } from './TableContext';
@@ -29,8 +32,6 @@ import { groupTh, tableRoot } from '../../theme';
29
32
  export const Table = <TData extends RowData>(props: TableProps<TData>) => {
30
33
  const {
31
34
  role,
32
- data = [],
33
- columns = [],
34
35
  onColumnResize,
35
36
  columnVisibility,
36
37
  header = true,
@@ -38,18 +39,17 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
38
39
  debug,
39
40
  onDataSelectionChange,
40
41
  getScrollElement,
42
+ pinLastRow,
41
43
  } = props;
42
44
 
43
- const TableProvider = UntypedTableProvider as TypedTableProvider<TData>;
45
+ const columns = useDefaultValue(props.columns, []);
46
+ const data = useDefaultValue(props.data, []);
44
47
 
45
- //
46
- // Column resizing
47
- //
48
+ const TableProvider = UntypedTableProvider as TypedTableProvider<TData>;
48
49
 
49
50
  const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({});
50
51
 
51
52
  useEffect(() => {
52
- // Set initial state.
53
53
  setColumnSizing(
54
54
  columns
55
55
  .filter((column) => !!column.size && (column as any).prop !== undefined)
@@ -58,31 +58,16 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
58
58
  return state;
59
59
  }, {}),
60
60
  );
61
- }, [columns]);
61
+ }, [columns, setColumnSizing]);
62
62
 
63
63
  const [columnSizingInfo, setColumnSizingInfo] = useState<ColumnSizingInfoState>({} as ColumnSizingInfoState);
64
64
 
65
- const onColumnResizeDebounced = onColumnResize && debounce(onColumnResize, 1_000);
66
- useEffect(() => {
67
- if (columnSizingInfo.columnSizingStart?.length === 0) {
68
- onColumnResizeDebounced?.(table.getState().columnSizing);
69
- }
70
- }, [columnSizingInfo]);
71
-
72
- //
73
- // Row selection
74
- //
75
-
76
65
  const [rowSelection = {}, setRowSelection] = useControllableState({
77
66
  prop: props.rowSelection,
78
67
  onChange: props.onRowSelectionChange,
79
68
  defaultProp: props.defaultRowSelection,
80
69
  });
81
70
 
82
- useEffect(() => {
83
- onDataSelectionChange?.(Object.keys(rowSelection).map((id) => table.getRowModel().rowsById[id].original));
84
- }, [onDataSelectionChange, rowSelection]);
85
-
86
71
  // TODO(thure): Does @tanstack/react-table really need this intervention? It did seem necessary to enforce single-selection...
87
72
  const handleRowSelectionChange = useCallback<OnChangeFn<RowSelectionState>>(
88
73
  (updaterOrValue) => {
@@ -96,15 +81,11 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
96
81
  setRowSelection({});
97
82
  }
98
83
  },
99
- [rowSelection, setRowSelection],
84
+ [rowsSelectable, setRowSelection, rowSelection],
100
85
  );
101
86
 
102
- //
103
- // Row grouping
104
- //
105
-
106
87
  const [grouping, handleGroupingChange] = useState<GroupingState>(props.grouping ?? []);
107
- useEffect(() => handleGroupingChange(props.grouping ?? []), [props.grouping]);
88
+ useEffect(() => handleGroupingChange(props.grouping ?? []), [handleGroupingChange, props.grouping]);
108
89
 
109
90
  const table = useReactTable({
110
91
  // Data
@@ -135,23 +116,50 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
135
116
 
136
117
  // Rows
137
118
  getCoreRowModel: getCoreRowModel(),
119
+ getSortedRowModel: getSortedRowModel(),
120
+ getExpandedRowModel: getExpandedRowModel(),
138
121
 
139
122
  // Grouping
140
123
  getGroupedRowModel: grouping.length > 1 ? getGroupedRowModel() : undefined,
141
124
  onGroupingChange: handleGroupingChange,
142
125
 
143
- // Selection
144
- ...(rowsSelectable === 'multi'
145
- ? { enableMultiRowSelection: true }
146
- : rowsSelectable
147
- ? { enableRowSelection: true }
148
- : {}),
126
+ enableMultiRowSelection: rowsSelectable === 'multi' ? true : undefined,
127
+ enableRowSelection: rowsSelectable === true ? true : undefined,
128
+
149
129
  onRowSelectionChange: handleRowSelectionChange,
150
130
 
131
+ enableSorting: true,
132
+ enableColumnPinning: true,
133
+
151
134
  // Debug
152
135
  debugTable: debug,
153
136
  });
154
137
 
138
+ const onColumnResizeDebounced = onColumnResize && debounce(onColumnResize, 1_000);
139
+
140
+ useEffect(() => {
141
+ onDataSelectionChange?.(Object.keys(rowSelection).map((id) => table.getRowModel().rowsById[id].original));
142
+ }, [onDataSelectionChange, rowSelection, table]);
143
+
144
+ useEffect(() => {
145
+ // TODO(zan): This is super jank, let's think of a nicer way to do this ... later.
146
+ if (pinLastRow) {
147
+ // Clear row pinning
148
+ table.resetRowPinning();
149
+
150
+ const rows = table.getRowModel().rows;
151
+ rows[rows.length - 1].pin('bottom');
152
+ }
153
+ }, [pinLastRow, table, data]);
154
+
155
+ useEffect(() => {
156
+ const shouldTriggerResize = columnSizingInfo.columnSizingStart?.length === 0;
157
+
158
+ if (shouldTriggerResize) {
159
+ onColumnResizeDebounced?.(table.getState().columnSizing);
160
+ }
161
+ }, [columnSizingInfo, onColumnResizeDebounced, table]);
162
+
155
163
  // Create additional expansion column if all columns have fixed width.
156
164
  const expand = false; // columns.map((column) => column.size).filter(Boolean).length === columns?.length;
157
165
 
@@ -178,7 +186,11 @@ export const Table = <TData extends RowData>(props: TableProps<TData>) => {
178
186
  const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
179
187
  const { debug, classNames, getScrollElement, role, footer, grouping, fullWidth } = props;
180
188
  const { table } = useTableContext<TData>('TableImpl');
181
- const { rows } = table.getRowModel();
189
+
190
+ const centerRows = table.getCenterRows();
191
+ const bottomRows = table.getBottomRows();
192
+
193
+ const rows = [...centerRows, ...bottomRows];
182
194
 
183
195
  if (debug) {
184
196
  return (
@@ -188,6 +200,8 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
188
200
  );
189
201
  }
190
202
 
203
+ const isResizingColumn = table.getState().columnSizingInfo.isResizingColumn;
204
+
191
205
  return (
192
206
  <table
193
207
  role={role}
@@ -198,7 +212,11 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
198
212
 
199
213
  {grouping?.length !== 0 ? (
200
214
  getScrollElement ? (
201
- <VirtualizedTableContent getScrollElement={getScrollElement} />
215
+ isResizingColumn ? (
216
+ <MemoizedVirtualisedTableContent getScrollElement={getScrollElement} />
217
+ ) : (
218
+ <VirtualizedTableContent getScrollElement={getScrollElement} />
219
+ )
202
220
  ) : (
203
221
  <TableBody rows={rows} />
204
222
  )
@@ -214,10 +232,12 @@ const TableImpl = <TData extends RowData>(props: TableProps<TData>) => {
214
232
  const VirtualizedTableContent = ({
215
233
  getScrollElement,
216
234
  }: Pick<VirtualizerOptions<Element, Element>, 'getScrollElement'>) => {
217
- const {
218
- table: { getRowModel },
219
- } = useTableContext('VirtualizedTableContent');
220
- const rows = getRowModel().rows;
235
+ const { table } = useTableContext('VirtualizedTableContent');
236
+
237
+ const centerRows = table.getCenterRows();
238
+ const pinnedRows = table.getBottomRows();
239
+
240
+ const rows = [...centerRows, ...pinnedRows];
221
241
 
222
242
  const { getTotalSize, getVirtualItems } = useVirtualizer({
223
243
  getScrollElement,
@@ -225,6 +245,7 @@ const VirtualizedTableContent = ({
225
245
  overscan: 4,
226
246
  estimateSize: () => 33,
227
247
  });
248
+
228
249
  const virtualRows = getVirtualItems();
229
250
  const totalSize = getTotalSize();
230
251
 
@@ -252,6 +273,8 @@ const VirtualizedTableContent = ({
252
273
  );
253
274
  };
254
275
 
276
+ export const MemoizedVirtualisedTableContent = React.memo(VirtualizedTableContent) as typeof VirtualizedTableContent;
277
+
255
278
  const GroupedTableContent = () => {
256
279
  const {
257
280
  table: { getGroupedRowModel, getHeaderGroups, getState },
@@ -19,6 +19,7 @@ type TableBodyProps<TData extends RowData> = {
19
19
  const TableBody = <TData extends RowData>({ rows }: TableBodyProps<TData>) => {
20
20
  const { table, keyAccessor, currentDatum, debug, expand, isGrid, onDatumClick } =
21
21
  useTableContext<TData>(TABLE_BODY_NAME);
22
+
22
23
  const rowSelection = table.getState().rowSelection;
23
24
  const domAttributes = useArrowNavigationGroup({ axis: 'grid' });
24
25
  const canBeCurrent = !isGrid && !!onDatumClick;
@@ -34,6 +34,8 @@ const TableHead = <TData extends RowData>(_props: TableHeadProps) => {
34
34
 
35
35
  {headerGroup.headers.map((header) => {
36
36
  const isResizing = header.column.getIsResizing();
37
+ const resizeHandler = header.getResizeHandler();
38
+
37
39
  return (
38
40
  <th
39
41
  key={header.id}
@@ -56,8 +58,8 @@ const TableHead = <TData extends RowData>(_props: TableHeadProps) => {
56
58
  style={{
57
59
  transform: `translateX(${isResizing ? state.columnSizingInfo.deltaOffset : 0}px)`,
58
60
  }}
59
- onMouseDown={header.getResizeHandler()}
60
- onTouchStart={header.getResizeHandler()}
61
+ onMouseDown={resizeHandler}
62
+ onTouchStart={resizeHandler}
61
63
  >
62
64
  <div className={mx(theadResizeThumb(tableContext))} />
63
65
  </div>
@@ -52,6 +52,7 @@ export type TableProps<
52
52
  onDataSelectionChange: (dataSelection: TData[]) => void;
53
53
  // `table` element props
54
54
  classNames: ClassNameValue;
55
+ pinLastRow: boolean;
55
56
  } & Pick<VirtualizerOptions<ScrollElement, ItemElement>, 'getScrollElement'>
56
57
  >;
57
58
 
package/src/helpers.tsx CHANGED
@@ -95,8 +95,11 @@ const ComboboxBuilderCell = <TData extends RowData>(cellContext: CellContext<TDa
95
95
  };
96
96
 
97
97
  const StringBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, string>) => {
98
- const inputRef = useRef<HTMLInputElement>(null);
99
- const { onUpdate } = cellContext.column.columnDef.meta as ColumnMeta<TData, string>;
98
+ const ref = useRef<HTMLInputElement>(null);
99
+ const { findPrevFocusable } = useFocusFinders();
100
+
101
+ const { onUpdate, cell } = cellContext.column.columnDef.meta as ColumnMeta<TData, string>;
102
+
100
103
  // https://tanstack.com/table/v8/docs/examples/react/editable-data
101
104
  const initialValue = cellContext.getValue();
102
105
  const [value, setValue] = useState(() => {
@@ -109,32 +112,34 @@ const StringBuilderCell = <TData extends RowData>(cellContext: CellContext<TData
109
112
  });
110
113
 
111
114
  const handleSave = () => {
112
- if (value === initialValue) {
113
- return;
114
- }
115
+ ref.current && document.activeElement === ref.current && findPrevFocusable(ref.current)?.focus();
115
116
 
116
- onUpdate?.(cellContext.row.original, cellContext.column.id, value);
117
+ if (value !== initialValue) {
118
+ onUpdate?.(cellContext.row.original, cellContext.column.id, value);
119
+ }
117
120
  };
118
121
 
119
- const handleCancel = () => {
120
- setValue(initialValue);
121
- };
122
+ const handleCancel = () => setValue(initialValue);
122
123
 
123
- // Check if first input column of last row.
124
- const rows = cellContext.table.getRowModel().flatRows;
124
+ let placeholder = false;
125
125
  const columns = cellContext.table.getVisibleFlatColumns();
126
- const placeholder = cellContext.row.index === rows.length - 1 && columns[0].id === cellContext.column.id;
126
+
127
+ // Calling getRowModel is expensive, so only do it when we're a cell in the first position
128
+ if (columns[0].id === cellContext.column.id) {
129
+ const rowCount = cellContext.table.getRowModel().rows.length;
130
+ placeholder = cellContext.row.index === rowCount - 1 && columns[0].id === cellContext.column.id;
131
+ }
127
132
 
128
133
  // TODO(burdon): Don't render inputs unless mouse over (Show ellipsis when div)?
129
134
  return (
130
135
  <Input.Root>
131
136
  <Input.TextInput
132
- ref={inputRef}
137
+ ref={ref}
133
138
  variant='subdued'
134
139
  placeholder={placeholder ? 'Add row...' : undefined}
135
- classNames={['is-full', textPadding]}
140
+ classNames={['is-full', textPadding, cell?.classNames]}
136
141
  value={value ?? ''}
137
- onBlur={() => handleSave()}
142
+ onBlur={handleSave}
138
143
  onChange={(event) => setValue(event.target.value)}
139
144
  onKeyDown={(event) => (event.key === 'Enter' && handleSave()) || (event.key === 'Escape' && handleCancel())}
140
145
  />
@@ -164,17 +169,20 @@ const NumberBuilderCell = <TData extends RowData>(cellContext: CellContext<TData
164
169
  (!document.activeElement || document.activeElement !== ref.current) && setText(displayNumber(value, digits));
165
170
  }, [digits, value]);
166
171
 
167
- const handleEdit = () => {
168
- if (onUpdate) {
169
- setText(value !== undefined ? String(value) : '');
170
- }
171
- };
172
+ const handleEdit = () => setText(value !== undefined ? String(value) : '');
172
173
 
173
174
  // TODO(burdon): Property is encoded as float (e.g., 6.1 => 6.099)
174
175
  const handleSave = () => {
175
176
  const nextValue = parseNumber(text);
176
- onUpdate?.(cellContext.row.original, cellContext.column.id, isNaN(nextValue) ? undefined : nextValue);
177
- setText(displayNumber(nextValue, digits));
177
+
178
+ if (!isNaN(nextValue)) {
179
+ onUpdate?.(cellContext.row.original, cellContext.column.id, nextValue);
180
+ setText(displayNumber(nextValue, digits));
181
+ } else {
182
+ // Reset the text to display the original value without updating the model.
183
+ setText(displayNumber(value, digits));
184
+ }
185
+
178
186
  // Shift focus only if input is focused on save (e.g. on `Enter`).
179
187
  ref.current && document.activeElement === ref.current && findPrevFocusable(ref.current)?.focus();
180
188
  };
@@ -210,20 +218,23 @@ const RowSelectorBuilderCell = <TData extends RowData>(cellContext: CellContext<
210
218
  const checked = row.getCanSelect()
211
219
  ? row.getIsSelected()
212
220
  : row.getCanSelectSubRows() && (row.getIsSomeSelected() ? 'indeterminate' : row.getIsAllSubRowsSelected());
221
+
213
222
  return (
214
- <Input.Root>
215
- <Input.Checkbox
216
- size={4}
217
- classNames={['mli-auto', cell?.classNames]}
218
- checked={checked}
219
- onCheckedChange={(event) => {
220
- if (row.getCanSelect()) {
221
- row.getToggleSelectedHandler()(event);
222
- }
223
- }}
224
- disabled={!(row.getCanSelect() || row.getCanSelectSubRows())}
225
- />
226
- </Input.Root>
223
+ <div className='flex justify-center w-full h-full' role='presentation'>
224
+ <Input.Root>
225
+ <Input.Checkbox
226
+ size={4}
227
+ classNames={[cell?.classNames]}
228
+ checked={checked}
229
+ onCheckedChange={(event) => {
230
+ if (row.getCanSelect()) {
231
+ row.getToggleSelectedHandler()(event);
232
+ }
233
+ }}
234
+ disabled={!(row.getCanSelect() || row.getCanSelectSubRows())}
235
+ />
236
+ </Input.Root>
237
+ </div>
227
238
  );
228
239
  };
229
240
 
@@ -422,21 +433,22 @@ export class ColumnBuilder<TData extends RowData> {
422
433
  return {
423
434
  id,
424
435
  size: 32,
425
- minSize: 32,
426
- maxSize: 32,
436
+ enableResizing: false,
427
437
  meta: { onUpdate, cell: { classNames } },
428
438
  header: ({ table }) => {
429
439
  const { rowsSelectable } = useTableContext('HELPER_SELECT_ROW_HEADER_CELL');
430
440
  const checked = table.getIsSomeRowsSelected() ? 'indeterminate' : table.getIsAllRowsSelected();
431
441
  return rowsSelectable === 'multi' ? (
432
- <Input.Root>
433
- <Input.Checkbox
434
- size={4}
435
- classNames={['mli-auto', classNames]}
436
- checked={checked}
437
- onCheckedChange={() => table.toggleAllRowsSelected()}
438
- />
439
- </Input.Root>
442
+ <div className='flex justify-center w-full h-full' role='presentation'>
443
+ <Input.Root>
444
+ <Input.Checkbox
445
+ size={4}
446
+ classNames={['mli-auto', classNames]}
447
+ checked={checked}
448
+ onCheckedChange={() => table.toggleAllRowsSelected()}
449
+ />
450
+ </Input.Root>
451
+ </div>
440
452
  ) : null;
441
453
  },
442
454
  cell: RowSelectorBuilderCell,
@@ -453,6 +465,8 @@ export class ColumnBuilder<TData extends RowData> {
453
465
  size: 50,
454
466
  minSize: 50,
455
467
  header: (column) => label ?? column.header.id,
468
+ // TODO(Zan): Implement sort algorithm
469
+ enableSorting: true,
456
470
  cell: SwitchBuilderCell,
457
471
  ...props,
458
472
  meta: { ...props.meta, onUpdate, cell: { ...props.meta?.cell, classNames: [textPadding, classNames] } },
@@ -467,6 +481,8 @@ export class ColumnBuilder<TData extends RowData> {
467
481
  const IconOff = off?.Icon ?? X;
468
482
  return {
469
483
  size: size ?? 32,
484
+ // TODO(Zan): Implement sort algorithm
485
+ enableSorting: false,
470
486
  header: (column) => <div className={'justify-center'}>{label ?? column.header.id}</div>,
471
487
  cell: (cell) => {
472
488
  const value = cell.getValue();