@cdx-ui/data-grid 0.0.1-beta.185
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/README.md +11 -0
- package/lib/commonjs/DataGrid.js +478 -0
- package/lib/commonjs/DataGrid.js.map +1 -0
- package/lib/commonjs/adapter/columns.js +38 -0
- package/lib/commonjs/adapter/columns.js.map +1 -0
- package/lib/commonjs/adapter/selectColumn.js +18 -0
- package/lib/commonjs/adapter/selectColumn.js.map +1 -0
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/styles.js +61 -0
- package/lib/commonjs/styles.js.map +1 -0
- package/lib/commonjs/types.js +17 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useDataGridTable.js +253 -0
- package/lib/commonjs/useDataGridTable.js.map +1 -0
- package/lib/module/DataGrid.js +474 -0
- package/lib/module/DataGrid.js.map +1 -0
- package/lib/module/adapter/columns.js +32 -0
- package/lib/module/adapter/columns.js.map +1 -0
- package/lib/module/adapter/selectColumn.js +14 -0
- package/lib/module/adapter/selectColumn.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/styles.js +57 -0
- package/lib/module/styles.js.map +1 -0
- package/lib/module/types.js +13 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useDataGridTable.js +240 -0
- package/lib/module/useDataGridTable.js.map +1 -0
- package/lib/typescript/DataGrid.d.ts +7 -0
- package/lib/typescript/DataGrid.d.ts.map +1 -0
- package/lib/typescript/adapter/columns.d.ts +17 -0
- package/lib/typescript/adapter/columns.d.ts.map +1 -0
- package/lib/typescript/adapter/selectColumn.d.ts +9 -0
- package/lib/typescript/adapter/selectColumn.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +3 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/styles.d.ts +22 -0
- package/lib/typescript/styles.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +112 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/useDataGridTable.d.ts +54 -0
- package/lib/typescript/useDataGridTable.d.ts.map +1 -0
- package/package.json +87 -0
- package/src/DataGrid.tsx +622 -0
- package/src/adapter/columns.test.ts +47 -0
- package/src/adapter/columns.ts +32 -0
- package/src/adapter/selectColumn.ts +12 -0
- package/src/index.ts +17 -0
- package/src/styles.ts +104 -0
- package/src/types.ts +133 -0
- package/src/useDataGridTable.test.ts +125 -0
- package/src/useDataGridTable.ts +295 -0
package/src/DataGrid.tsx
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import { useMemo, type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
import { Platform, View, type ViewStyle } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
Checkbox,
|
|
5
|
+
IconButton,
|
|
6
|
+
Input,
|
|
7
|
+
Skeleton,
|
|
8
|
+
Table,
|
|
9
|
+
TablePagination,
|
|
10
|
+
Text,
|
|
11
|
+
} from '@cdx-ui/components';
|
|
12
|
+
import { KeyboardArrowDown, KeyboardArrowUp, Search } from '@cdx-ui/icons';
|
|
13
|
+
import { dataAttributes, mergeDataAttributes } from '@cdx-ui/primitives';
|
|
14
|
+
import { cn, useControllableState } from '@cdx-ui/utils';
|
|
15
|
+
import { normalizeGridAlignment } from './adapter/columns';
|
|
16
|
+
import {
|
|
17
|
+
dataGridBodyVariants,
|
|
18
|
+
dataGridCellVariants,
|
|
19
|
+
dataGridEmptyOverlayVariants,
|
|
20
|
+
dataGridFilterInputVariants,
|
|
21
|
+
dataGridFooterVariants,
|
|
22
|
+
dataGridHeadVariants,
|
|
23
|
+
dataGridHeaderCellInnerVariants,
|
|
24
|
+
dataGridHeaderVariants,
|
|
25
|
+
dataGridMainVariants,
|
|
26
|
+
dataGridOverlayVariants,
|
|
27
|
+
dataGridPaginationRangeVariants,
|
|
28
|
+
dataGridPaginationVariants,
|
|
29
|
+
dataGridRootVariants,
|
|
30
|
+
dataGridSelectCellVariants,
|
|
31
|
+
dataGridSelectHeadVariants,
|
|
32
|
+
dataGridTableVariants,
|
|
33
|
+
dataGridToolbarActionsVariants,
|
|
34
|
+
dataGridToolbarSearchVariants,
|
|
35
|
+
dataGridToolbarVariants,
|
|
36
|
+
} from './styles';
|
|
37
|
+
import {
|
|
38
|
+
DEFAULT_PAGE_SIZE_OPTIONS,
|
|
39
|
+
DEFAULT_PAGINATION_MODEL,
|
|
40
|
+
EMPTY_SELECTION,
|
|
41
|
+
SELECT_COLUMN_ID,
|
|
42
|
+
type DataGridProps,
|
|
43
|
+
type GridColDef,
|
|
44
|
+
type GridValidRowModel,
|
|
45
|
+
} from './types';
|
|
46
|
+
import { resolveRowId, useDataGridTable } from './useDataGridTable';
|
|
47
|
+
|
|
48
|
+
const MAX_SKELETON_ROW_COUNT = 10;
|
|
49
|
+
const DEFAULT_WEB_COLUMN_WIDTH = 100;
|
|
50
|
+
const SELECT_COLUMN_WIDTH = 56;
|
|
51
|
+
const WEB_TABLE_CONTAINER_STYLE: ViewStyle = { overflow: 'visible' };
|
|
52
|
+
|
|
53
|
+
function cellStyle<R extends GridValidRowModel>(
|
|
54
|
+
col: GridColDef<R> | undefined,
|
|
55
|
+
webWidth?: CSSProperties['width'],
|
|
56
|
+
): ViewStyle | undefined {
|
|
57
|
+
if (!col) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const style: ViewStyle = {};
|
|
61
|
+
if (Platform.OS === 'web' && webWidth !== undefined) {
|
|
62
|
+
style.width = webWidth as ViewStyle['width'];
|
|
63
|
+
} else if (col.width !== undefined) {
|
|
64
|
+
style.width = col.width;
|
|
65
|
+
}
|
|
66
|
+
if (col.minWidth !== undefined) {
|
|
67
|
+
style.minWidth = col.minWidth;
|
|
68
|
+
}
|
|
69
|
+
if (Platform.OS !== 'web' && col.flex !== undefined) {
|
|
70
|
+
style.flex = col.flex;
|
|
71
|
+
}
|
|
72
|
+
return Object.keys(style).length > 0 ? style : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function webColumnWidths<R extends GridValidRowModel>(
|
|
76
|
+
columns: readonly GridColDef<R>[],
|
|
77
|
+
checkboxSelection: boolean,
|
|
78
|
+
): Map<string, CSSProperties['width']> {
|
|
79
|
+
const totalFlex = columns.reduce((total, column) => total + Math.max(column.flex ?? 0, 0), 0);
|
|
80
|
+
const widths = new Map<string, CSSProperties['width']>();
|
|
81
|
+
|
|
82
|
+
if (totalFlex === 0) {
|
|
83
|
+
for (const column of columns) {
|
|
84
|
+
if (column.width !== undefined) {
|
|
85
|
+
widths.set(column.field, column.width);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return widths;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fixedWidth =
|
|
92
|
+
(checkboxSelection ? SELECT_COLUMN_WIDTH : 0) +
|
|
93
|
+
columns.reduce((total, column) => {
|
|
94
|
+
if ((column.flex ?? 0) > 0) {
|
|
95
|
+
return total;
|
|
96
|
+
}
|
|
97
|
+
return total + Math.max(column.width ?? DEFAULT_WEB_COLUMN_WIDTH, column.minWidth ?? 0);
|
|
98
|
+
}, 0);
|
|
99
|
+
|
|
100
|
+
for (const column of columns) {
|
|
101
|
+
if ((column.flex ?? 0) > 0) {
|
|
102
|
+
const share = (column.flex ?? 0) / totalFlex;
|
|
103
|
+
const percentage = share * 100;
|
|
104
|
+
const fixedShare = fixedWidth * share;
|
|
105
|
+
const width = `calc(${percentage.toString()}% - ${fixedShare.toString()}px)`;
|
|
106
|
+
widths.set(
|
|
107
|
+
column.field,
|
|
108
|
+
column.minWidth === undefined ? width : `max(${column.minWidth.toString()}px, ${width})`,
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
widths.set(
|
|
112
|
+
column.field,
|
|
113
|
+
Math.max(column.width ?? DEFAULT_WEB_COLUMN_WIDTH, column.minWidth ?? 0),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return widths;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function defaultCellContent(value: unknown): ReactNode {
|
|
122
|
+
if (value == null) {
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
if (typeof value === 'bigint' || typeof value === 'boolean' || typeof value === 'symbol') {
|
|
131
|
+
return String(value);
|
|
132
|
+
}
|
|
133
|
+
const serialized: unknown = JSON.stringify(value, null, 2);
|
|
134
|
+
return typeof serialized === 'string' ? serialized : '[Unserializable value]';
|
|
135
|
+
} catch {
|
|
136
|
+
return '[Unserializable value]';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function DefaultLoadingRows<R extends GridValidRowModel>({
|
|
141
|
+
columns,
|
|
142
|
+
checkboxSelection,
|
|
143
|
+
rowCount,
|
|
144
|
+
columnWidths,
|
|
145
|
+
}: {
|
|
146
|
+
columns: readonly GridColDef<R>[];
|
|
147
|
+
checkboxSelection: boolean;
|
|
148
|
+
rowCount: number;
|
|
149
|
+
columnWidths: ReadonlyMap<string, CSSProperties['width']>;
|
|
150
|
+
}) {
|
|
151
|
+
return (
|
|
152
|
+
<>
|
|
153
|
+
{Array.from({ length: rowCount }, (_, rowIndex) => (
|
|
154
|
+
<Table.Row
|
|
155
|
+
key={rowIndex}
|
|
156
|
+
className="pointer-events-none web:hover:bg-surface-primary"
|
|
157
|
+
testID={rowIndex === 0 ? 'data-grid-default-loading-rows' : undefined}
|
|
158
|
+
>
|
|
159
|
+
{checkboxSelection ? (
|
|
160
|
+
<Table.Cell align="center" className={dataGridSelectCellVariants()}>
|
|
161
|
+
<Skeleton className="size-5" />
|
|
162
|
+
</Table.Cell>
|
|
163
|
+
) : null}
|
|
164
|
+
{columns.map((column) => (
|
|
165
|
+
<Table.Cell
|
|
166
|
+
key={column.field}
|
|
167
|
+
align={normalizeGridAlignment(column.align)}
|
|
168
|
+
className={dataGridCellVariants()}
|
|
169
|
+
style={cellStyle(column, columnWidths.get(column.field))}
|
|
170
|
+
>
|
|
171
|
+
<Skeleton className="h-4 w-3/5" />
|
|
172
|
+
</Table.Cell>
|
|
173
|
+
))}
|
|
174
|
+
</Table.Row>
|
|
175
|
+
))}
|
|
176
|
+
</>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function paginationRangeLabel(page: number, pageSize: number, total: number): string {
|
|
181
|
+
if (total <= 0) {
|
|
182
|
+
return `0\u20130 of 0`;
|
|
183
|
+
}
|
|
184
|
+
const start = page * pageSize + 1;
|
|
185
|
+
const end = Math.min((page + 1) * pageSize, total);
|
|
186
|
+
return `${start.toString()}\u2013${end.toString()} of ${total.toString()}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function withCurrentPageSize(
|
|
190
|
+
options: readonly (number | { value: number; label: string })[],
|
|
191
|
+
pageSize: number,
|
|
192
|
+
): (number | { value: number; label: string })[] {
|
|
193
|
+
const exists = options.some((option) =>
|
|
194
|
+
typeof option === 'number' ? option === pageSize : option.value === pageSize,
|
|
195
|
+
);
|
|
196
|
+
return exists ? [...options] : [pageSize, ...options];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
|
|
200
|
+
const {
|
|
201
|
+
rows,
|
|
202
|
+
columns,
|
|
203
|
+
getRowId,
|
|
204
|
+
loading = false,
|
|
205
|
+
checkboxSelection = false,
|
|
206
|
+
hideFooter = false,
|
|
207
|
+
hideFooterPagination = false,
|
|
208
|
+
disableColumnSorting = false,
|
|
209
|
+
paginationMode = 'client',
|
|
210
|
+
sortingMode = 'client',
|
|
211
|
+
rowCount,
|
|
212
|
+
showToolbar = false,
|
|
213
|
+
headerFilters = false,
|
|
214
|
+
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
|
215
|
+
noRowsOverlay,
|
|
216
|
+
loadingOverlay,
|
|
217
|
+
toolbarActions,
|
|
218
|
+
className,
|
|
219
|
+
style,
|
|
220
|
+
label,
|
|
221
|
+
'aria-label': ariaLabel,
|
|
222
|
+
'aria-labelledby': ariaLabelledBy,
|
|
223
|
+
} = props;
|
|
224
|
+
|
|
225
|
+
const [sortModel, setSortModel] = useControllableState({
|
|
226
|
+
prop: props.sortModel,
|
|
227
|
+
defaultProp: props.defaultSortModel ?? [],
|
|
228
|
+
onChange: props.onSortModelChange,
|
|
229
|
+
});
|
|
230
|
+
const [paginationModel, setPaginationModel] = useControllableState({
|
|
231
|
+
prop: props.paginationModel,
|
|
232
|
+
defaultProp: props.defaultPaginationModel ?? DEFAULT_PAGINATION_MODEL,
|
|
233
|
+
onChange: props.onPaginationModelChange,
|
|
234
|
+
});
|
|
235
|
+
const [filterModel, setFilterModel] = useControllableState({
|
|
236
|
+
prop: props.filterModel,
|
|
237
|
+
defaultProp: props.defaultFilterModel ?? [],
|
|
238
|
+
onChange: props.onFilterModelChange,
|
|
239
|
+
});
|
|
240
|
+
const [quickFilter, setQuickFilter] = useControllableState({
|
|
241
|
+
prop: props.quickFilter,
|
|
242
|
+
defaultProp: props.defaultQuickFilter ?? '',
|
|
243
|
+
onChange: props.onQuickFilterChange,
|
|
244
|
+
});
|
|
245
|
+
const [rowSelectionModel, setRowSelectionModel] = useControllableState({
|
|
246
|
+
prop: props.rowSelectionModel,
|
|
247
|
+
defaultProp: props.defaultRowSelectionModel ?? EMPTY_SELECTION,
|
|
248
|
+
onChange: props.onRowSelectionModelChange,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const resolvedSortModel = sortModel ?? [];
|
|
252
|
+
const resolvedPagination = paginationModel ?? DEFAULT_PAGINATION_MODEL;
|
|
253
|
+
const resolvedFilterModel = filterModel ?? [];
|
|
254
|
+
const resolvedQuickFilter = quickFilter ?? '';
|
|
255
|
+
const resolvedSelection = rowSelectionModel ?? EMPTY_SELECTION;
|
|
256
|
+
|
|
257
|
+
const {
|
|
258
|
+
table,
|
|
259
|
+
setQuickFilter: setQuickFilterWithPageReset,
|
|
260
|
+
setColumnFilter,
|
|
261
|
+
} = useDataGridTable({
|
|
262
|
+
rows,
|
|
263
|
+
columns,
|
|
264
|
+
getRowId,
|
|
265
|
+
checkboxSelection,
|
|
266
|
+
disableColumnSorting,
|
|
267
|
+
paginationMode,
|
|
268
|
+
sortingMode,
|
|
269
|
+
rowCount,
|
|
270
|
+
sortModel: resolvedSortModel,
|
|
271
|
+
onSortModelChange: (model) => setSortModel(model),
|
|
272
|
+
paginationModel: resolvedPagination,
|
|
273
|
+
onPaginationModelChange: (model) => setPaginationModel(model),
|
|
274
|
+
filterModel: resolvedFilterModel,
|
|
275
|
+
onFilterModelChange: (model) => setFilterModel(model),
|
|
276
|
+
quickFilter: resolvedQuickFilter,
|
|
277
|
+
onQuickFilterChange: (value) => setQuickFilter(value),
|
|
278
|
+
rowSelectionModel: resolvedSelection,
|
|
279
|
+
onRowSelectionModelChange: (model) => setRowSelectionModel(model),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const colByField = useMemo(() => {
|
|
283
|
+
const map = new Map<string, GridColDef<R>>();
|
|
284
|
+
for (const col of columns) {
|
|
285
|
+
map.set(col.field, col);
|
|
286
|
+
}
|
|
287
|
+
return map;
|
|
288
|
+
}, [columns]);
|
|
289
|
+
const columnWidths = useMemo(
|
|
290
|
+
() => webColumnWidths(columns, checkboxSelection),
|
|
291
|
+
[checkboxSelection, columns],
|
|
292
|
+
);
|
|
293
|
+
const hasFlexColumns = useMemo(() => columns.some((column) => (column.flex ?? 0) > 0), [columns]);
|
|
294
|
+
|
|
295
|
+
const visibleRows = table.getRowModel().rows;
|
|
296
|
+
const pageCount = Math.max(table.getPageCount(), 1);
|
|
297
|
+
const selectedCount = table.getFilteredSelectedRowModel().rows.length;
|
|
298
|
+
const filterableCount = table.getFilteredRowModel().rows.length;
|
|
299
|
+
const headerSelectState =
|
|
300
|
+
selectedCount === 0
|
|
301
|
+
? { checked: false, indeterminate: false }
|
|
302
|
+
: selectedCount === filterableCount
|
|
303
|
+
? { checked: true, indeterminate: false }
|
|
304
|
+
: { checked: false, indeterminate: true };
|
|
305
|
+
|
|
306
|
+
const accessibleName = ariaLabel ?? label;
|
|
307
|
+
const isEmpty = !loading && rows.length === 0;
|
|
308
|
+
const totalCount = paginationMode === 'server' ? (rowCount ?? rows.length) : filterableCount;
|
|
309
|
+
|
|
310
|
+
const filterValueFor = (field: string) =>
|
|
311
|
+
resolvedFilterModel.find((item) => item.field === field)?.value ?? '';
|
|
312
|
+
|
|
313
|
+
const showToolbarBar = showToolbar || toolbarActions != null;
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<View
|
|
317
|
+
className={cn(dataGridRootVariants(), className)}
|
|
318
|
+
style={style}
|
|
319
|
+
{...mergeDataAttributes({}, dataAttributes({ slot: 'data-grid' }))}
|
|
320
|
+
>
|
|
321
|
+
{showToolbarBar ? (
|
|
322
|
+
<View className={dataGridToolbarVariants()}>
|
|
323
|
+
{showToolbar ? (
|
|
324
|
+
<Input size="small" className={dataGridToolbarSearchVariants()}>
|
|
325
|
+
<Input.Icon as={Search} />
|
|
326
|
+
<Input.Field
|
|
327
|
+
value={resolvedQuickFilter}
|
|
328
|
+
onChangeText={setQuickFilterWithPageReset}
|
|
329
|
+
placeholder="Search"
|
|
330
|
+
accessibilityLabel="Search"
|
|
331
|
+
/>
|
|
332
|
+
</Input>
|
|
333
|
+
) : null}
|
|
334
|
+
{toolbarActions ? (
|
|
335
|
+
<View className={dataGridToolbarActionsVariants()}>{toolbarActions}</View>
|
|
336
|
+
) : null}
|
|
337
|
+
</View>
|
|
338
|
+
) : null}
|
|
339
|
+
|
|
340
|
+
<View className={dataGridMainVariants()}>
|
|
341
|
+
<Table
|
|
342
|
+
className={cn(
|
|
343
|
+
dataGridTableVariants(),
|
|
344
|
+
Platform.OS === 'web' && hasFlexColumns ? 'table-fixed' : undefined,
|
|
345
|
+
)}
|
|
346
|
+
containerStyle={Platform.OS === 'web' ? WEB_TABLE_CONTAINER_STYLE : undefined}
|
|
347
|
+
accessibilityLabel={accessibleName}
|
|
348
|
+
{...(Platform.OS === 'web'
|
|
349
|
+
? {
|
|
350
|
+
'aria-label': accessibleName,
|
|
351
|
+
'aria-labelledby': ariaLabelledBy,
|
|
352
|
+
'aria-busy': loading,
|
|
353
|
+
}
|
|
354
|
+
: {})}
|
|
355
|
+
>
|
|
356
|
+
{label ? <Table.Caption>{label}</Table.Caption> : null}
|
|
357
|
+
{Platform.OS === 'web' && hasFlexColumns ? (
|
|
358
|
+
<colgroup>
|
|
359
|
+
{checkboxSelection ? <col style={{ width: SELECT_COLUMN_WIDTH }} /> : null}
|
|
360
|
+
{columns.map((column) => (
|
|
361
|
+
<col
|
|
362
|
+
key={column.field}
|
|
363
|
+
style={{
|
|
364
|
+
width: columnWidths.get(column.field),
|
|
365
|
+
minWidth: column.minWidth,
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
))}
|
|
369
|
+
</colgroup>
|
|
370
|
+
) : null}
|
|
371
|
+
<Table.Header className={dataGridHeaderVariants()}>
|
|
372
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
373
|
+
<Table.Row key={headerGroup.id}>
|
|
374
|
+
{headerGroup.headers.map((header) => {
|
|
375
|
+
if (header.id === SELECT_COLUMN_ID) {
|
|
376
|
+
return (
|
|
377
|
+
<Table.Head
|
|
378
|
+
key={header.id}
|
|
379
|
+
align="center"
|
|
380
|
+
className={dataGridSelectHeadVariants()}
|
|
381
|
+
>
|
|
382
|
+
<Checkbox
|
|
383
|
+
value="select-all"
|
|
384
|
+
isChecked={headerSelectState.checked}
|
|
385
|
+
isIndeterminate={headerSelectState.indeterminate}
|
|
386
|
+
onChange={() => table.toggleAllRowsSelected()}
|
|
387
|
+
accessibilityLabel="Select all"
|
|
388
|
+
aria-label="Select all"
|
|
389
|
+
/>
|
|
390
|
+
</Table.Head>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const col = colByField.get(header.column.id);
|
|
395
|
+
const sorted = header.column.getIsSorted();
|
|
396
|
+
const canSort = !disableColumnSorting && header.column.getCanSort();
|
|
397
|
+
const headerAlign = col?.headerAlign ?? col?.align;
|
|
398
|
+
const SortIcon =
|
|
399
|
+
sorted === 'asc'
|
|
400
|
+
? KeyboardArrowUp
|
|
401
|
+
: sorted === 'desc'
|
|
402
|
+
? KeyboardArrowDown
|
|
403
|
+
: KeyboardArrowDown;
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<Table.Head
|
|
407
|
+
key={header.id}
|
|
408
|
+
align={normalizeGridAlignment(headerAlign)}
|
|
409
|
+
className={dataGridHeadVariants()}
|
|
410
|
+
style={cellStyle(col, columnWidths.get(header.column.id))}
|
|
411
|
+
{...(Platform.OS === 'web' && canSort
|
|
412
|
+
? {
|
|
413
|
+
'aria-sort':
|
|
414
|
+
sorted === 'asc'
|
|
415
|
+
? 'ascending'
|
|
416
|
+
: sorted === 'desc'
|
|
417
|
+
? 'descending'
|
|
418
|
+
: 'none',
|
|
419
|
+
}
|
|
420
|
+
: {})}
|
|
421
|
+
>
|
|
422
|
+
<View className={dataGridHeaderCellInnerVariants()}>
|
|
423
|
+
<Text className="font-medium text-content-primary">
|
|
424
|
+
{col?.renderHeader ? col.renderHeader() : (col?.headerName ?? col?.field)}
|
|
425
|
+
</Text>
|
|
426
|
+
{canSort ? (
|
|
427
|
+
<IconButton
|
|
428
|
+
as={SortIcon}
|
|
429
|
+
variant="ghost"
|
|
430
|
+
color="neutral"
|
|
431
|
+
size="small"
|
|
432
|
+
iconClassName="size-4"
|
|
433
|
+
accessibilityLabel={`Sort by ${col?.headerName ?? header.column.id}`}
|
|
434
|
+
onPress={() => {
|
|
435
|
+
header.column.toggleSorting();
|
|
436
|
+
}}
|
|
437
|
+
className={sorted ? undefined : 'opacity-40'}
|
|
438
|
+
/>
|
|
439
|
+
) : null}
|
|
440
|
+
</View>
|
|
441
|
+
</Table.Head>
|
|
442
|
+
);
|
|
443
|
+
})}
|
|
444
|
+
</Table.Row>
|
|
445
|
+
))}
|
|
446
|
+
{headerFilters ? (
|
|
447
|
+
<Table.Row>
|
|
448
|
+
{table.getAllLeafColumns().map((column) => {
|
|
449
|
+
if (column.id === SELECT_COLUMN_ID) {
|
|
450
|
+
return (
|
|
451
|
+
<Table.Cell
|
|
452
|
+
key={column.id}
|
|
453
|
+
align="center"
|
|
454
|
+
className={dataGridSelectHeadVariants()}
|
|
455
|
+
/>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
const col = colByField.get(column.id);
|
|
459
|
+
const headerAlign = col?.headerAlign ?? col?.align;
|
|
460
|
+
if (col?.filterable === false) {
|
|
461
|
+
return (
|
|
462
|
+
<Table.Head
|
|
463
|
+
key={column.id}
|
|
464
|
+
align={normalizeGridAlignment(headerAlign)}
|
|
465
|
+
className={dataGridHeadVariants()}
|
|
466
|
+
style={cellStyle(col, columnWidths.get(column.id))}
|
|
467
|
+
/>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
return (
|
|
471
|
+
<Table.Head
|
|
472
|
+
key={column.id}
|
|
473
|
+
align={normalizeGridAlignment(headerAlign)}
|
|
474
|
+
className={dataGridHeadVariants()}
|
|
475
|
+
style={cellStyle(col, columnWidths.get(column.id))}
|
|
476
|
+
>
|
|
477
|
+
<Input size="small" className={dataGridFilterInputVariants()}>
|
|
478
|
+
<Input.Field
|
|
479
|
+
value={filterValueFor(column.id)}
|
|
480
|
+
onChangeText={(value) => setColumnFilter(column.id, value)}
|
|
481
|
+
placeholder="Filter"
|
|
482
|
+
accessibilityLabel={`Filter ${col?.headerName ?? column.id}`}
|
|
483
|
+
/>
|
|
484
|
+
</Input>
|
|
485
|
+
</Table.Head>
|
|
486
|
+
);
|
|
487
|
+
})}
|
|
488
|
+
</Table.Row>
|
|
489
|
+
) : null}
|
|
490
|
+
</Table.Header>
|
|
491
|
+
<Table.Body className={dataGridBodyVariants()}>
|
|
492
|
+
{loading && loadingOverlay == null ? (
|
|
493
|
+
<DefaultLoadingRows
|
|
494
|
+
columns={columns}
|
|
495
|
+
checkboxSelection={checkboxSelection}
|
|
496
|
+
rowCount={Math.min(resolvedPagination.pageSize, MAX_SKELETON_ROW_COUNT)}
|
|
497
|
+
columnWidths={columnWidths}
|
|
498
|
+
/>
|
|
499
|
+
) : isEmpty ? null : (
|
|
500
|
+
visibleRows.map((row) => {
|
|
501
|
+
const rowId = resolveRowId(row.original, getRowId);
|
|
502
|
+
return (
|
|
503
|
+
<Table.Row key={row.id}>
|
|
504
|
+
{row.getAllCells().map((cell) => {
|
|
505
|
+
if (cell.column.id === SELECT_COLUMN_ID) {
|
|
506
|
+
return (
|
|
507
|
+
<Table.Cell
|
|
508
|
+
key={cell.id}
|
|
509
|
+
align="center"
|
|
510
|
+
className={dataGridSelectCellVariants()}
|
|
511
|
+
>
|
|
512
|
+
<Checkbox
|
|
513
|
+
value={String(rowId)}
|
|
514
|
+
isChecked={row.getIsSelected()}
|
|
515
|
+
onChange={() => row.toggleSelected()}
|
|
516
|
+
accessibilityLabel={`Select row ${String(rowId)}`}
|
|
517
|
+
aria-label={`Select row ${String(rowId)}`}
|
|
518
|
+
/>
|
|
519
|
+
</Table.Cell>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const col = colByField.get(cell.column.id);
|
|
524
|
+
const value = cell.getValue();
|
|
525
|
+
let content: ReactNode;
|
|
526
|
+
if (col?.renderCell) {
|
|
527
|
+
content = col.renderCell({
|
|
528
|
+
value,
|
|
529
|
+
row: row.original,
|
|
530
|
+
field: col.field,
|
|
531
|
+
id: rowId,
|
|
532
|
+
});
|
|
533
|
+
} else if (col?.valueFormatter) {
|
|
534
|
+
content = col.valueFormatter(value, row.original, col);
|
|
535
|
+
} else {
|
|
536
|
+
content = defaultCellContent(value);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<Table.Cell
|
|
541
|
+
key={cell.id}
|
|
542
|
+
align={normalizeGridAlignment(col?.align)}
|
|
543
|
+
className={dataGridCellVariants()}
|
|
544
|
+
style={cellStyle(col, columnWidths.get(cell.column.id))}
|
|
545
|
+
>
|
|
546
|
+
{content}
|
|
547
|
+
</Table.Cell>
|
|
548
|
+
);
|
|
549
|
+
})}
|
|
550
|
+
</Table.Row>
|
|
551
|
+
);
|
|
552
|
+
})
|
|
553
|
+
)}
|
|
554
|
+
</Table.Body>
|
|
555
|
+
</Table>
|
|
556
|
+
|
|
557
|
+
{isEmpty ? (
|
|
558
|
+
<View className={dataGridEmptyOverlayVariants()} accessibilityLabel="No rows">
|
|
559
|
+
{noRowsOverlay ?? <Text className="text-center text-content-secondary">No rows</Text>}
|
|
560
|
+
</View>
|
|
561
|
+
) : null}
|
|
562
|
+
|
|
563
|
+
{loading ? (
|
|
564
|
+
<Text
|
|
565
|
+
className="pointer-events-none absolute h-px w-px overflow-hidden opacity-0"
|
|
566
|
+
accessibilityLiveRegion="polite"
|
|
567
|
+
>
|
|
568
|
+
Loading
|
|
569
|
+
</Text>
|
|
570
|
+
) : null}
|
|
571
|
+
|
|
572
|
+
{loading && loadingOverlay != null ? (
|
|
573
|
+
<View className={dataGridOverlayVariants({ headerFilters })}>{loadingOverlay}</View>
|
|
574
|
+
) : null}
|
|
575
|
+
</View>
|
|
576
|
+
|
|
577
|
+
{!hideFooter &&
|
|
578
|
+
!hideFooterPagination &&
|
|
579
|
+
(paginationMode === 'server' ? totalCount > 0 : rows.length > 0) ? (
|
|
580
|
+
<View className={dataGridFooterVariants()}>
|
|
581
|
+
<TablePagination
|
|
582
|
+
page={resolvedPagination.page + 1}
|
|
583
|
+
count={pageCount}
|
|
584
|
+
pageSize={resolvedPagination.pageSize}
|
|
585
|
+
pageSizeOptions={withCurrentPageSize(pageSizeOptions, resolvedPagination.pageSize)}
|
|
586
|
+
onPageChange={(page: number) =>
|
|
587
|
+
setPaginationModel({ ...resolvedPagination, page: page - 1 })
|
|
588
|
+
}
|
|
589
|
+
onPageSizeChange={(pageSize: number) => setPaginationModel({ page: 0, pageSize })}
|
|
590
|
+
disabled={loading}
|
|
591
|
+
accessibilityLabel="Grid pagination"
|
|
592
|
+
className={dataGridPaginationVariants()}
|
|
593
|
+
>
|
|
594
|
+
<Text size="md" className="text-content-primary">
|
|
595
|
+
Rows per page:
|
|
596
|
+
</Text>
|
|
597
|
+
<TablePagination.PageSize />
|
|
598
|
+
<TablePagination.Status className={dataGridPaginationRangeVariants()}>
|
|
599
|
+
<Text size="md" className="text-content-primary">
|
|
600
|
+
{paginationRangeLabel(
|
|
601
|
+
resolvedPagination.page,
|
|
602
|
+
resolvedPagination.pageSize,
|
|
603
|
+
totalCount,
|
|
604
|
+
)}
|
|
605
|
+
</Text>
|
|
606
|
+
</TablePagination.Status>
|
|
607
|
+
<TablePagination.Previous className="h-11 w-11 min-w-11" />
|
|
608
|
+
<TablePagination.Next className="h-11 w-11 min-w-11" />
|
|
609
|
+
</TablePagination>
|
|
610
|
+
</View>
|
|
611
|
+
) : null}
|
|
612
|
+
</View>
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export function DataGrid<R extends GridValidRowModel>(props: DataGridProps<R>) {
|
|
617
|
+
return <DataGridInner {...props} />;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
DataGrid.displayName = 'DataGrid';
|
|
621
|
+
|
|
622
|
+
export type { DataGridProps };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { applyUpdater, gridColDefToColumnDef, normalizeGridAlignment } from './columns';
|
|
2
|
+
|
|
3
|
+
describe('gridColDefToColumnDef', () => {
|
|
4
|
+
it('maps field, header, and sort/filter flags', () => {
|
|
5
|
+
const def = gridColDefToColumnDef({
|
|
6
|
+
field: 'name',
|
|
7
|
+
headerName: 'Name',
|
|
8
|
+
sortable: false,
|
|
9
|
+
filterable: false,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(def.id).toBe('name');
|
|
13
|
+
expect(def.header).toBe('Name');
|
|
14
|
+
expect(def.enableSorting).toBe(false);
|
|
15
|
+
expect(def.enableColumnFilter).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('uses valueGetter when provided', () => {
|
|
19
|
+
const def = gridColDefToColumnDef({
|
|
20
|
+
field: 'name',
|
|
21
|
+
valueGetter: (_value, row) => String(row.name).toUpperCase(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(def.accessorFn?.({ name: 'operating' }, 0)).toBe('OPERATING');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('applyUpdater', () => {
|
|
29
|
+
it('returns the next value or applies a function', () => {
|
|
30
|
+
expect(applyUpdater(2, 1)).toBe(2);
|
|
31
|
+
expect(applyUpdater((prev: number) => prev + 1, 1)).toBe(2);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('normalizeGridAlignment', () => {
|
|
36
|
+
it('passes through logical alignment and defaults to start', () => {
|
|
37
|
+
expect(normalizeGridAlignment('start')).toBe('start');
|
|
38
|
+
expect(normalizeGridAlignment('center')).toBe('center');
|
|
39
|
+
expect(normalizeGridAlignment('end')).toBe('end');
|
|
40
|
+
expect(normalizeGridAlignment(undefined)).toBe('start');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('normalizes deprecated physical aliases', () => {
|
|
44
|
+
expect(normalizeGridAlignment('left')).toBe('start');
|
|
45
|
+
expect(normalizeGridAlignment('right')).toBe('end');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TableAlignment } from '@cdx-ui/components';
|
|
2
|
+
import type { GridAlignment, GridColDef, GridValidRowModel } from '../types';
|
|
3
|
+
|
|
4
|
+
export function gridColDefToColumnDef<R extends GridValidRowModel>(col: GridColDef<R>) {
|
|
5
|
+
return {
|
|
6
|
+
id: col.field,
|
|
7
|
+
accessorFn: (row: R) => {
|
|
8
|
+
const raw = row[col.field];
|
|
9
|
+
return col.valueGetter ? col.valueGetter(raw, row, col) : raw;
|
|
10
|
+
},
|
|
11
|
+
header: col.headerName ?? col.field,
|
|
12
|
+
enableSorting: col.sortable !== false,
|
|
13
|
+
enableColumnFilter: col.filterable !== false,
|
|
14
|
+
enableGlobalFilter: col.filterable !== false,
|
|
15
|
+
filterFn: 'includesString' as const,
|
|
16
|
+
meta: { colDef: col },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function applyUpdater<T>(updater: T | ((old: T) => T), previous: T): T {
|
|
21
|
+
return typeof updater === 'function' ? (updater as (old: T) => T)(previous) : updater;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeGridAlignment(align?: GridAlignment): TableAlignment {
|
|
25
|
+
if (align === 'left') {
|
|
26
|
+
return 'start';
|
|
27
|
+
}
|
|
28
|
+
if (align === 'right') {
|
|
29
|
+
return 'end';
|
|
30
|
+
}
|
|
31
|
+
return align ?? 'start';
|
|
32
|
+
}
|