@dxos/react-ui-table 0.3.8-next.f4e0086 → 0.3.9-main.03b62b6
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 +605 -532
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/Cell/Cell.d.ts +12 -0
- package/dist/types/src/components/Cell/Cell.d.ts.map +1 -0
- package/dist/types/src/components/{ComboboxCell.d.ts → Cell/CellCombobox.d.ts} +4 -4
- package/dist/types/src/components/Cell/CellCombobox.d.ts.map +1 -0
- package/dist/types/src/components/Cell/index.d.ts +2 -0
- package/dist/types/src/components/Cell/index.d.ts.map +1 -0
- package/dist/types/src/components/ColumnMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +3 -0
- package/dist/types/src/components/Table/Table.d.ts.map +1 -0
- package/dist/types/src/components/{Table.stories.d.ts → Table/Table.stories.d.ts} +4 -7
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableBody.d.ts +11 -0
- package/dist/types/src/components/Table/TableBody.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableContext.d.ts +14 -0
- package/dist/types/src/components/Table/TableContext.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableFooter.d.ts +7 -0
- package/dist/types/src/components/Table/TableFooter.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableHead.d.ts +8 -0
- package/dist/types/src/components/Table/TableHead.d.ts.map +1 -0
- package/dist/types/src/components/Table/index.d.ts +3 -0
- package/dist/types/src/components/Table/index.d.ts.map +1 -0
- package/dist/types/src/components/Table/props.d.ts +40 -0
- package/dist/types/src/components/Table/props.d.ts.map +1 -0
- 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 +21 -15
- package/dist/types/src/helpers.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/schema.d.ts +0 -1
- package/dist/types/src/schema.d.ts.map +1 -1
- package/dist/types/src/theme.d.ts +30 -33
- package/dist/types/src/theme.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +11 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +13 -10
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +12 -10
- package/src/components/Cell/Cell.tsx +43 -0
- package/src/components/{ComboboxCell.tsx → Cell/CellCombobox.tsx} +6 -6
- package/src/components/Cell/index.ts +5 -0
- package/src/components/ColumnMenu.tsx +16 -16
- package/src/components/{Table.stories.tsx → Table/Table.stories.tsx} +51 -51
- package/src/components/Table/Table.tsx +184 -0
- package/src/components/Table/TableBody.tsx +70 -0
- package/src/components/Table/TableContext.tsx +28 -0
- package/src/components/Table/TableFooter.tsx +42 -0
- package/src/components/Table/TableHead.tsx +79 -0
- package/src/components/Table/index.ts +6 -0
- package/src/components/Table/props.ts +58 -0
- package/src/components/index.ts +1 -1
- package/src/helpers.tsx +289 -219
- package/src/index.ts +1 -0
- package/src/schema.ts +0 -8
- package/src/theme.ts +102 -63
- package/src/translations.ts +11 -0
- package/src/types.ts +15 -10
- package/dist/types/src/components/ComboboxCell.d.ts.map +0 -1
- package/dist/types/src/components/Table.d.ts +0 -29
- package/dist/types/src/components/Table.d.ts.map +0 -1
- package/dist/types/src/components/Table.stories.d.ts.map +0 -1
- package/src/components/Table.tsx +0 -551
|
@@ -6,11 +6,12 @@ import { Check, CaretDown, Trash, X } from '@phosphor-icons/react';
|
|
|
6
6
|
import { type HeaderContext, type RowData } from '@tanstack/react-table';
|
|
7
7
|
import React, { type FC, type PropsWithChildren, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { Button, DensityProvider, Input, Popover, Select, Separator } from '@dxos/react-ui';
|
|
9
|
+
import { Button, DensityProvider, Input, Popover, Select, Separator, useTranslation } from '@dxos/react-ui';
|
|
10
10
|
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
11
11
|
import { safeParseInt } from '@dxos/util';
|
|
12
12
|
|
|
13
|
-
import { type TableDef, type ColumnProps,
|
|
13
|
+
import { type TableDef, type ColumnProps, type ColumnType } from '../schema';
|
|
14
|
+
import { translationKey } from '../translations';
|
|
14
15
|
|
|
15
16
|
export type ColumnMenuProps<TData extends RowData, TValue> = {
|
|
16
17
|
context: HeaderContext<TData, TValue>;
|
|
@@ -23,13 +24,11 @@ export type ColumnMenuProps<TData extends RowData, TValue> = {
|
|
|
23
24
|
|
|
24
25
|
export const ColumnMenu = <TData extends RowData, TValue>({ column, ...props }: ColumnMenuProps<TData, TValue>) => {
|
|
25
26
|
const title = column.label?.length ? column.label : column.id;
|
|
26
|
-
|
|
27
27
|
return (
|
|
28
|
-
<div className='flex
|
|
29
|
-
<div className='truncate' title={title}>
|
|
28
|
+
<div className='flex items-center gap-2'>
|
|
29
|
+
<div className='flex-1 min-is-0 truncate' title={title}>
|
|
30
30
|
{title}
|
|
31
31
|
</div>
|
|
32
|
-
<div className='grow' />
|
|
33
32
|
<div className='flex shrink-0'>
|
|
34
33
|
<ColumnPanel {...props} column={column} />
|
|
35
34
|
</div>
|
|
@@ -59,6 +58,7 @@ export const ColumnPanel = <TData extends RowData, TValue>({
|
|
|
59
58
|
const [label, setLabel] = useState(column.label);
|
|
60
59
|
const [digits, setDigits] = useState(String(column.digits ?? '0'));
|
|
61
60
|
const propRef = useRef<HTMLInputElement>(null);
|
|
61
|
+
const { t } = useTranslation(translationKey);
|
|
62
62
|
|
|
63
63
|
const handleCancel = () => {
|
|
64
64
|
setProp(column.id);
|
|
@@ -99,7 +99,7 @@ export const ColumnPanel = <TData extends RowData, TValue>({
|
|
|
99
99
|
return (
|
|
100
100
|
<Popover.Root open={open} onOpenChange={(nextOpen) => setOpen(nextOpen)}>
|
|
101
101
|
<Popover.Trigger asChild>
|
|
102
|
-
<Button variant='ghost' classNames='p-
|
|
102
|
+
<Button variant='ghost' classNames='p-1'>
|
|
103
103
|
<CaretDown className={getSize(4)} />
|
|
104
104
|
</Button>
|
|
105
105
|
</Popover.Trigger>
|
|
@@ -110,7 +110,7 @@ export const ColumnPanel = <TData extends RowData, TValue>({
|
|
|
110
110
|
<div className='flex flex-col gap-1'>
|
|
111
111
|
<Section>
|
|
112
112
|
<Input.Root>
|
|
113
|
-
<Input.Label classNames='mbe-1'>
|
|
113
|
+
<Input.Label classNames='mbe-1'>{t('column label label')}</Input.Label>
|
|
114
114
|
<Input.TextInput
|
|
115
115
|
placeholder='Column label'
|
|
116
116
|
value={label ?? ''}
|
|
@@ -119,7 +119,7 @@ export const ColumnPanel = <TData extends RowData, TValue>({
|
|
|
119
119
|
/>
|
|
120
120
|
</Input.Root>
|
|
121
121
|
<Input.Root>
|
|
122
|
-
<Input.Label classNames='mbe-1 mbs-3'>
|
|
122
|
+
<Input.Label classNames='mbe-1 mbs-3'>{t('property key label')}</Input.Label>
|
|
123
123
|
<Input.TextInput
|
|
124
124
|
ref={propRef}
|
|
125
125
|
placeholder='Property key'
|
|
@@ -129,15 +129,15 @@ export const ColumnPanel = <TData extends RowData, TValue>({
|
|
|
129
129
|
/>
|
|
130
130
|
</Input.Root>
|
|
131
131
|
<Input.Root>
|
|
132
|
-
<Input.Label classNames='mbe-1 mbs-3'>
|
|
132
|
+
<Input.Label classNames='mbe-1 mbs-3'>{t('column type label')}</Input.Label>
|
|
133
133
|
<Select.Root value={type} onValueChange={setType}>
|
|
134
134
|
<Select.TriggerButton placeholder='Type' classNames='is-full' />
|
|
135
135
|
<Select.Portal>
|
|
136
136
|
<Select.Content>
|
|
137
137
|
<Select.Viewport>
|
|
138
|
-
{
|
|
138
|
+
{(['number', 'boolean', 'string', 'ref'] as ColumnType[]).map((type) => (
|
|
139
139
|
<Select.Option key={type} value={type}>
|
|
140
|
-
{label}
|
|
140
|
+
{t(`${type} column type label`)}
|
|
141
141
|
</Select.Option>
|
|
142
142
|
))}
|
|
143
143
|
</Select.Viewport>
|
|
@@ -151,7 +151,7 @@ export const ColumnPanel = <TData extends RowData, TValue>({
|
|
|
151
151
|
<>
|
|
152
152
|
<Section>
|
|
153
153
|
<Input.Root>
|
|
154
|
-
<Input.Label classNames='mbe-1 mbs-3'>
|
|
154
|
+
<Input.Label classNames='mbe-1 mbs-3'>{t('digits label')}</Input.Label>
|
|
155
155
|
{/* TODO(burdon): Constrain input to numbers. */}
|
|
156
156
|
<Input.TextInput value={digits ?? ''} onChange={(event) => setDigits(event.target.value)} />
|
|
157
157
|
</Input.Root>
|
|
@@ -213,17 +213,17 @@ export const ColumnPanel = <TData extends RowData, TValue>({
|
|
|
213
213
|
<Section className='space-b-1.5'>
|
|
214
214
|
{/* TODO(burdon): Style as DropdownMenuItem. */}
|
|
215
215
|
<Button variant='primary' classNames='is-full flex gap-2' onClick={handleSave}>
|
|
216
|
-
<span>
|
|
216
|
+
<span>{t('save label')}</span>
|
|
217
217
|
<div className='grow' />
|
|
218
218
|
<Check className={getSize(5)} />
|
|
219
219
|
</Button>
|
|
220
220
|
<Button classNames='is-full flex gap-2' onClick={handleCancel}>
|
|
221
|
-
<span>
|
|
221
|
+
<span>{t('cancel label', { ns: 'os' })}</span>
|
|
222
222
|
<div className='grow' />
|
|
223
223
|
<X className={getSize(5)} />
|
|
224
224
|
</Button>
|
|
225
225
|
<Button classNames='is-full flex gap-2' onClick={() => onDelete?.(column.id)}>
|
|
226
|
-
<span>
|
|
226
|
+
<span>{t('delete label')}</span>
|
|
227
227
|
<div className='grow' />
|
|
228
228
|
<Trash className={getSize(5)} />
|
|
229
229
|
</Button>
|
|
@@ -10,12 +10,12 @@ import { deepSignal } from 'deepsignal';
|
|
|
10
10
|
import React, { useEffect, useState } from 'react';
|
|
11
11
|
|
|
12
12
|
import { PublicKey } from '@dxos/keys';
|
|
13
|
-
import { DensityProvider } from '@dxos/react-ui';
|
|
13
|
+
import { DensityProvider, AnchoredOverflow } from '@dxos/react-ui';
|
|
14
14
|
import { range } from '@dxos/util';
|
|
15
15
|
|
|
16
16
|
import { Table } from './Table';
|
|
17
|
-
import { createColumnBuilder, type
|
|
18
|
-
import { type TableColumnDef } from '
|
|
17
|
+
import { createColumnBuilder, type SearchListQueryModel, type ValueUpdater } from '../../helpers';
|
|
18
|
+
import { type TableColumnDef } from '../../types';
|
|
19
19
|
|
|
20
20
|
// TODO(burdon): Header menu builder.
|
|
21
21
|
// TODO(burdon): Expand width if not all columns have explicit size.
|
|
@@ -29,7 +29,6 @@ type Item = {
|
|
|
29
29
|
complete?: boolean;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
// TODO(thure): …y’all okay?
|
|
33
32
|
faker.seed(911);
|
|
34
33
|
|
|
35
34
|
const createItems = (count: number) =>
|
|
@@ -59,7 +58,7 @@ const tableStorySelectItems: Record<string, Item> = range(128).reduce((acc: Reco
|
|
|
59
58
|
|
|
60
59
|
const timeout = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
60
|
|
|
62
|
-
const tableStorySelectModel:
|
|
61
|
+
const tableStorySelectModel: SearchListQueryModel<Item> = {
|
|
63
62
|
getId: (object) => object?.publicKey?.toHex() ?? 'never',
|
|
64
63
|
getText: (object) => object?.name ?? 'never',
|
|
65
64
|
query: async (search: string) => {
|
|
@@ -71,36 +70,44 @@ const tableStorySelectModel: SelectQueryModel<Item> = {
|
|
|
71
70
|
};
|
|
72
71
|
|
|
73
72
|
const { helper, builder } = createColumnBuilder<Item>();
|
|
73
|
+
|
|
74
74
|
const columns = (onUpdate?: ValueUpdater<Item, any>): TableColumnDef<Item, any>[] => [
|
|
75
|
+
helper.display(builder.selectRow()),
|
|
76
|
+
helper.accessor((item) => item.publicKey, { id: 'key', ...builder.key({ tooltip: true }) }),
|
|
75
77
|
helper.accessor(
|
|
76
|
-
'
|
|
77
|
-
builder.
|
|
78
|
-
|
|
79
|
-
getGroupingValue: (row) => row.complete === true,
|
|
80
|
-
label: '',
|
|
78
|
+
'name',
|
|
79
|
+
builder.string({
|
|
80
|
+
label: 'Name',
|
|
81
81
|
onUpdate,
|
|
82
|
+
meta: { expand: true },
|
|
83
|
+
footer: (props) => props.table.getRowModel().rows.length,
|
|
82
84
|
}),
|
|
83
85
|
),
|
|
84
|
-
helper.accessor(
|
|
86
|
+
helper.accessor('started', builder.date({ label: 'Started', relative: true, meta: { resizable: true } })),
|
|
85
87
|
helper.accessor(
|
|
86
|
-
'
|
|
87
|
-
builder.
|
|
88
|
+
'count',
|
|
89
|
+
builder.number({
|
|
90
|
+
label: 'Count',
|
|
91
|
+
meta: { resizable: true },
|
|
92
|
+
// TODO(burdon): Sorting.
|
|
93
|
+
getGroupingValue: (row) => (row.count ? (row.count < 2_000 ? 'A' : row.count < 5_000 ? 'B' : 'C') : 'D'),
|
|
94
|
+
}),
|
|
88
95
|
),
|
|
89
96
|
helper.accessor(
|
|
90
97
|
'company',
|
|
91
|
-
builder.
|
|
98
|
+
builder.combobox({
|
|
92
99
|
label: 'Company',
|
|
93
100
|
model: tableStorySelectModel,
|
|
94
101
|
onUpdate,
|
|
95
102
|
}),
|
|
96
103
|
),
|
|
97
|
-
helper.accessor('started', builder.date({ relative: true, meta: { resizable: true } })),
|
|
98
104
|
helper.accessor(
|
|
99
|
-
'
|
|
100
|
-
builder.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
'complete',
|
|
106
|
+
builder.switch({
|
|
107
|
+
// enableGrouping: true,
|
|
108
|
+
getGroupingValue: (row) => row.complete === true,
|
|
109
|
+
label: '',
|
|
110
|
+
onUpdate,
|
|
104
111
|
}),
|
|
105
112
|
),
|
|
106
113
|
helper.accessor('complete', builder.icon({ id: 'done', label: '' })),
|
|
@@ -138,9 +145,6 @@ export default {
|
|
|
138
145
|
fullWidth: {
|
|
139
146
|
control: 'boolean',
|
|
140
147
|
},
|
|
141
|
-
pinToBottom: {
|
|
142
|
-
control: 'boolean',
|
|
143
|
-
},
|
|
144
148
|
grouping: {
|
|
145
149
|
control: 'select',
|
|
146
150
|
options: ['none', 'complete', 'count'],
|
|
@@ -158,9 +162,9 @@ export default {
|
|
|
158
162
|
limited: { key: false, started: false },
|
|
159
163
|
},
|
|
160
164
|
},
|
|
161
|
-
|
|
165
|
+
rowsSelectable: {
|
|
162
166
|
control: 'select',
|
|
163
|
-
options: [
|
|
167
|
+
options: [false, true, 'multi'],
|
|
164
168
|
},
|
|
165
169
|
},
|
|
166
170
|
decorators: [
|
|
@@ -209,25 +213,23 @@ export const Dynamic = {
|
|
|
209
213
|
if (items.length >= 200) {
|
|
210
214
|
clearInterval(interval);
|
|
211
215
|
}
|
|
212
|
-
|
|
213
216
|
return [...items, ...createItems(1)];
|
|
214
217
|
});
|
|
215
218
|
}, 500);
|
|
216
219
|
return () => clearInterval(interval);
|
|
217
220
|
}, []);
|
|
218
|
-
|
|
219
221
|
return (
|
|
220
|
-
<
|
|
221
|
-
{/* prettier-ignore */}
|
|
222
|
+
<AnchoredOverflow.Root classNames='max-bs-[80dvh]'>
|
|
222
223
|
<Table<Item>
|
|
223
|
-
|
|
224
|
+
rowsSelectable='multi'
|
|
225
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
224
226
|
columns={columns()}
|
|
225
227
|
data={items}
|
|
226
228
|
fullWidth
|
|
227
|
-
pinToBottom
|
|
228
229
|
footer
|
|
229
230
|
/>
|
|
230
|
-
|
|
231
|
+
<AnchoredOverflow.Anchor />
|
|
232
|
+
</AnchoredOverflow.Root>
|
|
231
233
|
);
|
|
232
234
|
},
|
|
233
235
|
};
|
|
@@ -240,15 +242,15 @@ export const Editable = {
|
|
|
240
242
|
};
|
|
241
243
|
|
|
242
244
|
return (
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
245
|
+
<Table<Item>
|
|
246
|
+
role='grid'
|
|
247
|
+
rowsSelectable='multi'
|
|
248
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
249
|
+
columns={columns(onUpdate)}
|
|
250
|
+
data={items}
|
|
251
|
+
fullWidth
|
|
252
|
+
border
|
|
253
|
+
/>
|
|
252
254
|
);
|
|
253
255
|
},
|
|
254
256
|
};
|
|
@@ -265,16 +267,14 @@ export const Resizable = {
|
|
|
265
267
|
// };
|
|
266
268
|
|
|
267
269
|
return (
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
/>
|
|
277
|
-
</div>
|
|
270
|
+
<Table<Item>
|
|
271
|
+
rowsSelectable='multi'
|
|
272
|
+
keyAccessor={(row) => row.publicKey.toHex()}
|
|
273
|
+
columns={columns(onUpdate)}
|
|
274
|
+
data={items}
|
|
275
|
+
fullWidth
|
|
276
|
+
// onColumnResize={handleColumnResize}
|
|
277
|
+
/>
|
|
278
278
|
);
|
|
279
279
|
},
|
|
280
280
|
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
6
|
+
import {
|
|
7
|
+
getCoreRowModel,
|
|
8
|
+
getGroupedRowModel,
|
|
9
|
+
useReactTable,
|
|
10
|
+
type ColumnSizingInfoState,
|
|
11
|
+
type ColumnSizingState,
|
|
12
|
+
type GroupingState,
|
|
13
|
+
type RowData,
|
|
14
|
+
type RowSelectionState,
|
|
15
|
+
type OnChangeFn,
|
|
16
|
+
} from '@tanstack/react-table';
|
|
17
|
+
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
|
18
|
+
|
|
19
|
+
import { debounce } from '@dxos/async';
|
|
20
|
+
|
|
21
|
+
import { TableBody } from './TableBody';
|
|
22
|
+
import { TableProvider as UntypedTableProvider, type TypedTableProvider } from './TableContext';
|
|
23
|
+
import { TableFooter } from './TableFooter';
|
|
24
|
+
import { TableHead } from './TableHead';
|
|
25
|
+
import { type TableProps } from './props';
|
|
26
|
+
import { groupTh, tableRoot } from '../../theme';
|
|
27
|
+
|
|
28
|
+
export const Table = <TData extends RowData>(props: TableProps<TData>) => {
|
|
29
|
+
const {
|
|
30
|
+
role,
|
|
31
|
+
data = [],
|
|
32
|
+
columns = [],
|
|
33
|
+
onColumnResize,
|
|
34
|
+
columnVisibility,
|
|
35
|
+
header = true,
|
|
36
|
+
footer,
|
|
37
|
+
rowsSelectable,
|
|
38
|
+
fullWidth,
|
|
39
|
+
debug,
|
|
40
|
+
onDataSelectionChange,
|
|
41
|
+
classNames,
|
|
42
|
+
} = props;
|
|
43
|
+
|
|
44
|
+
const TableProvider = UntypedTableProvider as TypedTableProvider<TData>;
|
|
45
|
+
|
|
46
|
+
// Row selection
|
|
47
|
+
const [rowSelection = {}, directlySetRowSelection] = useControllableState({
|
|
48
|
+
prop: props.rowSelection,
|
|
49
|
+
onChange: props.onRowSelectionChange,
|
|
50
|
+
defaultProp: props.defaultRowSelection,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// TODO(thure): Does @tanstack/react-table really need this intervention? It did seem necessary to enforce single-selection...
|
|
54
|
+
const handleRowSelectionChange = useCallback<OnChangeFn<RowSelectionState>>(
|
|
55
|
+
(updaterOrValue) => {
|
|
56
|
+
const nextRowSelection = typeof updaterOrValue === 'function' ? updaterOrValue(rowSelection) : updaterOrValue;
|
|
57
|
+
if (rowsSelectable === 'multi') {
|
|
58
|
+
directlySetRowSelection(nextRowSelection);
|
|
59
|
+
} else if (rowsSelectable) {
|
|
60
|
+
const nextRowSelectionKey = Object.keys(nextRowSelection).filter((id) => !rowSelection[id])[0];
|
|
61
|
+
directlySetRowSelection(nextRowSelectionKey ? { [nextRowSelectionKey]: true } : {});
|
|
62
|
+
} else {
|
|
63
|
+
directlySetRowSelection({});
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
[rowSelection, directlySetRowSelection],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Resizing
|
|
70
|
+
const [columnSizingInfo, setColumnSizingInfo] = useState<ColumnSizingInfoState>({} as ColumnSizingInfoState);
|
|
71
|
+
const onColumnResizeDebounced = debounce<ColumnSizingState>((info) => onColumnResize?.(info), 500);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (columnSizingInfo.columnSizingStart?.length === 0) {
|
|
74
|
+
onColumnResizeDebounced(table.getState().columnSizing);
|
|
75
|
+
}
|
|
76
|
+
}, [columnSizingInfo]);
|
|
77
|
+
|
|
78
|
+
const [grouping, setGrouping] = useState<GroupingState>(props.grouping ?? []);
|
|
79
|
+
useEffect(() => setGrouping(props.grouping ?? []), [props.grouping]);
|
|
80
|
+
|
|
81
|
+
const table = useReactTable({
|
|
82
|
+
// Data
|
|
83
|
+
meta: {},
|
|
84
|
+
data,
|
|
85
|
+
|
|
86
|
+
// Columns
|
|
87
|
+
columns,
|
|
88
|
+
defaultColumn: {
|
|
89
|
+
size: 256, // Required in order remove default width.
|
|
90
|
+
maxSize: 1024,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Rows
|
|
94
|
+
getCoreRowModel: getCoreRowModel(),
|
|
95
|
+
|
|
96
|
+
// State
|
|
97
|
+
state: {
|
|
98
|
+
columnVisibility,
|
|
99
|
+
columnSizingInfo,
|
|
100
|
+
rowSelection,
|
|
101
|
+
grouping,
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// Grouping
|
|
105
|
+
getGroupedRowModel: getGroupedRowModel(),
|
|
106
|
+
onGroupingChange: setGrouping,
|
|
107
|
+
|
|
108
|
+
// Selection
|
|
109
|
+
...(rowsSelectable === 'multi'
|
|
110
|
+
? { enableMultiRowSelection: true }
|
|
111
|
+
: rowsSelectable
|
|
112
|
+
? { enableRowSelection: true }
|
|
113
|
+
: {}),
|
|
114
|
+
onRowSelectionChange: handleRowSelectionChange,
|
|
115
|
+
|
|
116
|
+
// Resize columns
|
|
117
|
+
columnResizeMode: 'onChange',
|
|
118
|
+
enableColumnResizing: true,
|
|
119
|
+
onColumnSizingInfoChange: setColumnSizingInfo,
|
|
120
|
+
|
|
121
|
+
// Debug
|
|
122
|
+
debugTable: debug,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (onDataSelectionChange) {
|
|
127
|
+
onDataSelectionChange(Object.keys(rowSelection).map((id) => table.getRowModel().rowsById[id].original));
|
|
128
|
+
}
|
|
129
|
+
}, [onDataSelectionChange, rowSelection, table]);
|
|
130
|
+
|
|
131
|
+
// Create additional expansion column if all columns have fixed width.
|
|
132
|
+
const expand = false; // columns.map((column) => column.size).filter(Boolean).length === columns?.length;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<TableProvider
|
|
136
|
+
{...props}
|
|
137
|
+
header={header}
|
|
138
|
+
expand={expand}
|
|
139
|
+
table={table}
|
|
140
|
+
isGrid={role === 'grid' || role === 'treegrid'}
|
|
141
|
+
>
|
|
142
|
+
<table
|
|
143
|
+
role={role}
|
|
144
|
+
className={tableRoot(props, classNames)}
|
|
145
|
+
{...(!fullWidth && { style: { width: table.getTotalSize() } })}
|
|
146
|
+
>
|
|
147
|
+
<TableHead />
|
|
148
|
+
|
|
149
|
+
{grouping.length === 0 && <TableBody rows={table.getRowModel().rows} />}
|
|
150
|
+
|
|
151
|
+
{grouping.length !== 0 &&
|
|
152
|
+
table.getGroupedRowModel().rows.map((row, i) => {
|
|
153
|
+
return (
|
|
154
|
+
<Fragment key={i}>
|
|
155
|
+
{/* TODO(burdon): Customize group header renderer. */}
|
|
156
|
+
<thead>
|
|
157
|
+
<tr>
|
|
158
|
+
{debug && <th />}
|
|
159
|
+
<th
|
|
160
|
+
// TODO(burdon): Calculate row span.
|
|
161
|
+
colSpan={table.getHeaderGroups()[0].headers.length}
|
|
162
|
+
className={groupTh(props)}
|
|
163
|
+
>
|
|
164
|
+
{table.getState().grouping[0]}[{String(row.getGroupingValue(table.getState().grouping[0]))}]
|
|
165
|
+
</th>
|
|
166
|
+
</tr>
|
|
167
|
+
</thead>
|
|
168
|
+
|
|
169
|
+
<TableBody rows={row.subRows} />
|
|
170
|
+
</Fragment>
|
|
171
|
+
);
|
|
172
|
+
})}
|
|
173
|
+
|
|
174
|
+
{footer && <TableFooter />}
|
|
175
|
+
</table>
|
|
176
|
+
|
|
177
|
+
{debug && (
|
|
178
|
+
<pre className='font-mono text-xs text-neutral-500 my-4 p-2 ring'>
|
|
179
|
+
<code>{JSON.stringify(table.getState(), undefined, 2)}</code>
|
|
180
|
+
</pre>
|
|
181
|
+
)}
|
|
182
|
+
</TableProvider>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
|
|
6
|
+
import { flexRender, type Row, type RowData } from '@tanstack/react-table';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import { useTableContext } from './TableContext';
|
|
10
|
+
import { tbodyTr } from '../../theme';
|
|
11
|
+
import { Cell } from '../Cell/Cell';
|
|
12
|
+
|
|
13
|
+
type TableBodyProps<TData extends RowData> = {
|
|
14
|
+
rows: Row<TData>[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const TABLE_BODY_NAME = 'TableBody';
|
|
18
|
+
|
|
19
|
+
const TableBody = <TData extends RowData>({ rows }: TableBodyProps<TData>) => {
|
|
20
|
+
const tableContext = useTableContext<TData>(TABLE_BODY_NAME);
|
|
21
|
+
const { table, keyAccessor, debug, expand, isGrid, onDatumClick, currentDatum } = tableContext;
|
|
22
|
+
const rowSelection = table.getState().rowSelection;
|
|
23
|
+
const domAttributes = useArrowNavigationGroup({ axis: 'grid' });
|
|
24
|
+
const canBeCurrent = !isGrid && !!onDatumClick;
|
|
25
|
+
return (
|
|
26
|
+
<tbody {...(isGrid && domAttributes)}>
|
|
27
|
+
{rows.map((row) => {
|
|
28
|
+
const isCurrent = currentDatum === row.original;
|
|
29
|
+
const isSelected = rowSelection?.[row.id];
|
|
30
|
+
return (
|
|
31
|
+
<tr
|
|
32
|
+
key={keyAccessor ? keyAccessor(row.original) : row.id}
|
|
33
|
+
className={tbodyTr({ canBeCurrent })}
|
|
34
|
+
{...(isCurrent && { 'aria-current': 'location' })}
|
|
35
|
+
{...(isSelected && { 'aria-selected': 'true' })}
|
|
36
|
+
{...(canBeCurrent && {
|
|
37
|
+
tabIndex: 0,
|
|
38
|
+
onClick: () => {
|
|
39
|
+
onDatumClick(row.original);
|
|
40
|
+
},
|
|
41
|
+
onKeyUp: ({ key }) => {
|
|
42
|
+
if (key === 'Enter' || key === ' ') {
|
|
43
|
+
onDatumClick(row.original);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
})}
|
|
47
|
+
>
|
|
48
|
+
{debug && <td>{row.id}</td>}
|
|
49
|
+
|
|
50
|
+
{row.getVisibleCells().map((cell) => {
|
|
51
|
+
return (
|
|
52
|
+
<Cell key={cell.id} cell={cell}>
|
|
53
|
+
{flexRender(cell.column.columnDef.cell, { className: 'pli-2', ...cell.getContext() })}
|
|
54
|
+
</Cell>
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
57
|
+
|
|
58
|
+
{expand && <td />}
|
|
59
|
+
</tr>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</tbody>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
TableBody.displayName = TABLE_BODY_NAME;
|
|
67
|
+
|
|
68
|
+
export { TableBody };
|
|
69
|
+
|
|
70
|
+
export type { TableBodyProps };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { createContext } from '@radix-ui/react-context';
|
|
6
|
+
import { type RowData } from '@tanstack/react-table';
|
|
7
|
+
import { type ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
import { type TableContextValue } from './props';
|
|
10
|
+
|
|
11
|
+
export const TABLE_NAME = 'Table';
|
|
12
|
+
|
|
13
|
+
type CreateContextResult<TData extends RowData> = Readonly<
|
|
14
|
+
[TypedTableProvider<unknown>, (consumerName: string) => TableContextValue<TData>]
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
export type TypedTableProvider<TData extends RowData> = {
|
|
18
|
+
(props: TableContextValue<TData> & { children: ReactNode }): JSX.Element;
|
|
19
|
+
displayName: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const [TableProvider, useUntypedTableContext]: CreateContextResult<unknown> =
|
|
23
|
+
createContext<TableContextValue<unknown>>(TABLE_NAME);
|
|
24
|
+
|
|
25
|
+
export const useTableContext = <TData extends RowData>(consumerName: string) =>
|
|
26
|
+
useUntypedTableContext(consumerName) as TableContextValue<TData>;
|
|
27
|
+
|
|
28
|
+
export { TableProvider };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { flexRender, type RowData } from '@tanstack/react-table';
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
import { useTableContext } from './TableContext';
|
|
9
|
+
import { tfootRoot, tfootTr, tfootTh } from '../../theme';
|
|
10
|
+
|
|
11
|
+
export type TableFooterProps = {};
|
|
12
|
+
|
|
13
|
+
const TABLE_FOOTER_NAME = 'TableFooter';
|
|
14
|
+
|
|
15
|
+
const TableFooter = <TData extends RowData>(props: TableFooterProps) => {
|
|
16
|
+
const tableContext = useTableContext<TData>(TABLE_FOOTER_NAME);
|
|
17
|
+
const { table, expand, debug } = tableContext;
|
|
18
|
+
const footers = table.getFooterGroups();
|
|
19
|
+
return (
|
|
20
|
+
<tfoot className={tfootRoot(props)}>
|
|
21
|
+
{footers.map((footerGroup) => (
|
|
22
|
+
<tr key={footerGroup.id} className={tfootTr(tableContext)}>
|
|
23
|
+
{debug && <th />}
|
|
24
|
+
|
|
25
|
+
{footerGroup.headers.map((footer) => {
|
|
26
|
+
return (
|
|
27
|
+
<th key={footer.id} className={tfootTh(tableContext, footer.column.columnDef.meta?.footer?.classNames)}>
|
|
28
|
+
{footer.isPlaceholder ? null : flexRender(footer.column.columnDef.footer, footer.getContext())}
|
|
29
|
+
</th>
|
|
30
|
+
);
|
|
31
|
+
})}
|
|
32
|
+
|
|
33
|
+
{expand && <th />}
|
|
34
|
+
</tr>
|
|
35
|
+
))}
|
|
36
|
+
</tfoot>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
TableFooter.displayName = TABLE_FOOTER_NAME;
|
|
41
|
+
|
|
42
|
+
export { TableFooter };
|