@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.
Files changed (54) hide show
  1. package/README.md +11 -0
  2. package/lib/commonjs/DataGrid.js +478 -0
  3. package/lib/commonjs/DataGrid.js.map +1 -0
  4. package/lib/commonjs/adapter/columns.js +38 -0
  5. package/lib/commonjs/adapter/columns.js.map +1 -0
  6. package/lib/commonjs/adapter/selectColumn.js +18 -0
  7. package/lib/commonjs/adapter/selectColumn.js.map +1 -0
  8. package/lib/commonjs/index.js +13 -0
  9. package/lib/commonjs/index.js.map +1 -0
  10. package/lib/commonjs/package.json +1 -0
  11. package/lib/commonjs/styles.js +61 -0
  12. package/lib/commonjs/styles.js.map +1 -0
  13. package/lib/commonjs/types.js +17 -0
  14. package/lib/commonjs/types.js.map +1 -0
  15. package/lib/commonjs/useDataGridTable.js +253 -0
  16. package/lib/commonjs/useDataGridTable.js.map +1 -0
  17. package/lib/module/DataGrid.js +474 -0
  18. package/lib/module/DataGrid.js.map +1 -0
  19. package/lib/module/adapter/columns.js +32 -0
  20. package/lib/module/adapter/columns.js.map +1 -0
  21. package/lib/module/adapter/selectColumn.js +14 -0
  22. package/lib/module/adapter/selectColumn.js.map +1 -0
  23. package/lib/module/index.js +4 -0
  24. package/lib/module/index.js.map +1 -0
  25. package/lib/module/styles.js +57 -0
  26. package/lib/module/styles.js.map +1 -0
  27. package/lib/module/types.js +13 -0
  28. package/lib/module/types.js.map +1 -0
  29. package/lib/module/useDataGridTable.js +240 -0
  30. package/lib/module/useDataGridTable.js.map +1 -0
  31. package/lib/typescript/DataGrid.d.ts +7 -0
  32. package/lib/typescript/DataGrid.d.ts.map +1 -0
  33. package/lib/typescript/adapter/columns.d.ts +17 -0
  34. package/lib/typescript/adapter/columns.d.ts.map +1 -0
  35. package/lib/typescript/adapter/selectColumn.d.ts +9 -0
  36. package/lib/typescript/adapter/selectColumn.d.ts.map +1 -0
  37. package/lib/typescript/index.d.ts +3 -0
  38. package/lib/typescript/index.d.ts.map +1 -0
  39. package/lib/typescript/styles.d.ts +22 -0
  40. package/lib/typescript/styles.d.ts.map +1 -0
  41. package/lib/typescript/types.d.ts +112 -0
  42. package/lib/typescript/types.d.ts.map +1 -0
  43. package/lib/typescript/useDataGridTable.d.ts +54 -0
  44. package/lib/typescript/useDataGridTable.d.ts.map +1 -0
  45. package/package.json +87 -0
  46. package/src/DataGrid.tsx +622 -0
  47. package/src/adapter/columns.test.ts +47 -0
  48. package/src/adapter/columns.ts +32 -0
  49. package/src/adapter/selectColumn.ts +12 -0
  50. package/src/index.ts +17 -0
  51. package/src/styles.ts +104 -0
  52. package/src/types.ts +133 -0
  53. package/src/useDataGridTable.test.ts +125 -0
  54. package/src/useDataGridTable.ts +295 -0
@@ -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
+ }