@cdx-ui/data-grid 0.3.10 → 0.3.12

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 (82) hide show
  1. package/README.md +3 -1
  2. package/coverage.svg +1 -1
  3. package/lib/commonjs/DataGrid.js +232 -62
  4. package/lib/commonjs/DataGrid.js.map +1 -1
  5. package/lib/commonjs/DataGridFilterPanel.js +420 -0
  6. package/lib/commonjs/DataGridFilterPanel.js.map +1 -0
  7. package/lib/commonjs/adapter/columnTypes.js +256 -0
  8. package/lib/commonjs/adapter/columnTypes.js.map +1 -0
  9. package/lib/commonjs/adapter/columns.js +132 -4
  10. package/lib/commonjs/adapter/columns.js.map +1 -1
  11. package/lib/commonjs/adapter/filterOperators.js +422 -0
  12. package/lib/commonjs/adapter/filterOperators.js.map +1 -0
  13. package/lib/commonjs/adapter/pinning.js +101 -0
  14. package/lib/commonjs/adapter/pinning.js.map +1 -0
  15. package/lib/commonjs/index.js +26 -0
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/numberCells.js +280 -0
  18. package/lib/commonjs/numberCells.js.map +1 -0
  19. package/lib/commonjs/styles.js +61 -1
  20. package/lib/commonjs/styles.js.map +1 -1
  21. package/lib/commonjs/types.js +29 -1
  22. package/lib/commonjs/types.js.map +1 -1
  23. package/lib/commonjs/useDataGridTable.js +98 -13
  24. package/lib/commonjs/useDataGridTable.js.map +1 -1
  25. package/lib/module/DataGrid.js +237 -67
  26. package/lib/module/DataGrid.js.map +1 -1
  27. package/lib/module/DataGridFilterPanel.js +414 -0
  28. package/lib/module/DataGridFilterPanel.js.map +1 -0
  29. package/lib/module/adapter/columnTypes.js +240 -0
  30. package/lib/module/adapter/columnTypes.js.map +1 -0
  31. package/lib/module/adapter/columns.js +129 -4
  32. package/lib/module/adapter/columns.js.map +1 -1
  33. package/lib/module/adapter/filterOperators.js +406 -0
  34. package/lib/module/adapter/filterOperators.js.map +1 -0
  35. package/lib/module/adapter/pinning.js +90 -0
  36. package/lib/module/adapter/pinning.js.map +1 -0
  37. package/lib/module/index.js +2 -0
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/numberCells.js +275 -0
  40. package/lib/module/numberCells.js.map +1 -0
  41. package/lib/module/styles.js +60 -0
  42. package/lib/module/styles.js.map +1 -1
  43. package/lib/module/types.js +28 -0
  44. package/lib/module/types.js.map +1 -1
  45. package/lib/module/useDataGridTable.js +99 -14
  46. package/lib/module/useDataGridTable.js.map +1 -1
  47. package/lib/typescript/DataGrid.d.ts.map +1 -1
  48. package/lib/typescript/DataGridFilterPanel.d.ts +17 -0
  49. package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -0
  50. package/lib/typescript/adapter/columnTypes.d.ts +32 -0
  51. package/lib/typescript/adapter/columnTypes.d.ts.map +1 -0
  52. package/lib/typescript/adapter/columns.d.ts +14 -5
  53. package/lib/typescript/adapter/columns.d.ts.map +1 -1
  54. package/lib/typescript/adapter/filterOperators.d.ts +43 -0
  55. package/lib/typescript/adapter/filterOperators.d.ts.map +1 -0
  56. package/lib/typescript/adapter/pinning.d.ts +25 -0
  57. package/lib/typescript/adapter/pinning.d.ts.map +1 -0
  58. package/lib/typescript/index.d.ts +4 -1
  59. package/lib/typescript/index.d.ts.map +1 -1
  60. package/lib/typescript/numberCells.d.ts +46 -0
  61. package/lib/typescript/numberCells.d.ts.map +1 -0
  62. package/lib/typescript/styles.d.ts +21 -0
  63. package/lib/typescript/styles.d.ts.map +1 -1
  64. package/lib/typescript/types.d.ts +86 -7
  65. package/lib/typescript/types.d.ts.map +1 -1
  66. package/lib/typescript/useDataGridTable.d.ts +18 -4
  67. package/lib/typescript/useDataGridTable.d.ts.map +1 -1
  68. package/package.json +5 -5
  69. package/src/DataGrid.tsx +377 -67
  70. package/src/DataGridFilterPanel.tsx +477 -0
  71. package/src/adapter/columnTypes.ts +327 -0
  72. package/src/adapter/columns.test.ts +75 -1
  73. package/src/adapter/columns.ts +178 -3
  74. package/src/adapter/filterOperators.test.ts +292 -0
  75. package/src/adapter/filterOperators.ts +524 -0
  76. package/src/adapter/pinning.ts +125 -0
  77. package/src/index.ts +15 -0
  78. package/src/numberCells.tsx +379 -0
  79. package/src/styles.ts +75 -0
  80. package/src/types.ts +114 -7
  81. package/src/useDataGridTable.test.ts +69 -0
  82. package/src/useDataGridTable.ts +127 -8
package/src/DataGrid.tsx CHANGED
@@ -1,7 +1,8 @@
1
- import { useMemo, type CSSProperties, type ReactNode } from 'react';
2
- import { Platform, View, type ViewStyle } from 'react-native';
1
+ import { useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
2
+ import { I18nManager, Platform, View, type ViewStyle } from 'react-native';
3
3
  import {
4
4
  Checkbox,
5
+ Icon,
5
6
  IconButton,
6
7
  Input,
7
8
  Skeleton,
@@ -9,10 +10,35 @@ import {
9
10
  TablePagination,
10
11
  Text,
11
12
  } from '@cdx-ui/components';
12
- import { KeyboardArrowDown, KeyboardArrowUp, Search } from '@cdx-ui/icons';
13
+ import {
14
+ Check,
15
+ Close,
16
+ FilterList,
17
+ KeyboardArrowDown,
18
+ KeyboardArrowUp,
19
+ Search,
20
+ } from '@cdx-ui/icons';
13
21
  import { dataAttributes, mergeDataAttributes } from '@cdx-ui/primitives';
14
22
  import { cn, useControllableState } from '@cdx-ui/utils';
15
- import { normalizeGridAlignment } from './adapter/columns';
23
+ import {
24
+ normalizePinnedColumns,
25
+ resolveColumnAlign,
26
+ resolveColumnHeaderAlign,
27
+ } from './adapter/columns';
28
+ import {
29
+ DEFAULT_PINNED_COLUMN_WIDTH,
30
+ SELECT_COLUMN_WIDTH,
31
+ buildPinnedOffsets,
32
+ isColumnPinningActive,
33
+ pinnedCellStyle,
34
+ pinnedEdgeGuidesImage,
35
+ pinnedFieldSet,
36
+ warnPinnedFlexColumns,
37
+ type ColumnPinEdge,
38
+ type PinnedOffsetMaps,
39
+ } from './adapter/pinning';
40
+ import { getTypedCellText } from './adapter/columnTypes';
41
+ import { DataGridFilterPanel } from './DataGridFilterPanel';
16
42
  import {
17
43
  dataGridBodyVariants,
18
44
  dataGridCellVariants,
@@ -27,6 +53,8 @@ import {
27
53
  dataGridOverlayVariants,
28
54
  dataGridPaginationRangeVariants,
29
55
  dataGridPaginationVariants,
56
+ dataGridPinnedEdgeVariants,
57
+ dataGridPinnedSurfaceVariants,
30
58
  dataGridRootVariants,
31
59
  dataGridSelectCellVariants,
32
60
  dataGridSelectHeadVariants,
@@ -49,8 +77,14 @@ import { resolveRowId, useDataGridTable } from './useDataGridTable';
49
77
 
50
78
  const MAX_SKELETON_ROW_COUNT = 10;
51
79
  const DEFAULT_WEB_COLUMN_WIDTH = 100;
52
- const SELECT_COLUMN_WIDTH = 56;
53
80
  const WEB_TABLE_CONTAINER_STYLE: ViewStyle = { overflow: 'visible' };
81
+ // Same token as the pinned cells' `border-stroke-secondary` divider.
82
+ const PINNED_EDGE_COLOR = 'var(--color-stroke-secondary)';
83
+
84
+ interface PinRenderMeta {
85
+ edge: ColumnPinEdge | false;
86
+ isEdgeDivider: boolean;
87
+ }
54
88
 
55
89
  function cellStyle<R extends GridValidRowModel>(
56
90
  col: GridColDef<R> | undefined,
@@ -78,16 +112,32 @@ function cellStyle<R extends GridValidRowModel>(
78
112
  return Object.keys(style).length > 0 ? style : undefined;
79
113
  }
80
114
 
115
+ function effectiveFlex<R extends GridValidRowModel>(
116
+ column: GridColDef<R>,
117
+ pinned: ReadonlySet<string>,
118
+ ): number {
119
+ if (pinned.has(column.field)) {
120
+ return 0;
121
+ }
122
+ return Math.max(column.flex ?? 0, 0);
123
+ }
124
+
81
125
  function webColumnWidths<R extends GridValidRowModel>(
82
126
  columns: readonly GridColDef<R>[],
83
127
  checkboxSelection: boolean,
128
+ pinned: ReadonlySet<string> = new Set(),
84
129
  ): Map<string, CSSProperties['width']> {
85
- const totalFlex = columns.reduce((total, column) => total + Math.max(column.flex ?? 0, 0), 0);
130
+ const totalFlex = columns.reduce((total, column) => total + effectiveFlex(column, pinned), 0);
86
131
  const widths = new Map<string, CSSProperties['width']>();
87
132
 
88
133
  if (totalFlex === 0) {
89
134
  for (const column of columns) {
90
- if (column.width !== undefined) {
135
+ if (pinned.has(column.field)) {
136
+ widths.set(
137
+ column.field,
138
+ Math.max(column.width ?? DEFAULT_PINNED_COLUMN_WIDTH, column.minWidth ?? 0),
139
+ );
140
+ } else if (column.width !== undefined) {
91
141
  widths.set(column.field, column.width);
92
142
  }
93
143
  }
@@ -97,15 +147,19 @@ function webColumnWidths<R extends GridValidRowModel>(
97
147
  const fixedWidth =
98
148
  (checkboxSelection ? SELECT_COLUMN_WIDTH : 0) +
99
149
  columns.reduce((total, column) => {
100
- if ((column.flex ?? 0) > 0) {
150
+ if (effectiveFlex(column, pinned) > 0) {
101
151
  return total;
102
152
  }
103
- return total + Math.max(column.width ?? DEFAULT_WEB_COLUMN_WIDTH, column.minWidth ?? 0);
153
+ const fallback = pinned.has(column.field)
154
+ ? DEFAULT_PINNED_COLUMN_WIDTH
155
+ : DEFAULT_WEB_COLUMN_WIDTH;
156
+ return total + Math.max(column.width ?? fallback, column.minWidth ?? 0);
104
157
  }, 0);
105
158
 
106
159
  for (const column of columns) {
107
- if ((column.flex ?? 0) > 0) {
108
- const share = (column.flex ?? 0) / totalFlex;
160
+ const flex = effectiveFlex(column, pinned);
161
+ if (flex > 0) {
162
+ const share = flex / totalFlex;
109
163
  const percentage = share * 100;
110
164
  const fixedShare = fixedWidth * share;
111
165
  const width = `calc(${percentage.toString()}% - ${fixedShare.toString()}px)`;
@@ -114,16 +168,65 @@ function webColumnWidths<R extends GridValidRowModel>(
114
168
  column.minWidth === undefined ? width : `max(${column.minWidth.toString()}px, ${width})`,
115
169
  );
116
170
  } else {
117
- widths.set(
118
- column.field,
119
- Math.max(column.width ?? DEFAULT_WEB_COLUMN_WIDTH, column.minWidth ?? 0),
120
- );
171
+ const fallback = pinned.has(column.field)
172
+ ? DEFAULT_PINNED_COLUMN_WIDTH
173
+ : DEFAULT_WEB_COLUMN_WIDTH;
174
+ widths.set(column.field, Math.max(column.width ?? fallback, column.minWidth ?? 0));
121
175
  }
122
176
  }
123
177
 
124
178
  return widths;
125
179
  }
126
180
 
181
+ function pinMetaFor(columnId: string, pinning: { start: string[]; end: string[] }): PinRenderMeta {
182
+ const startIndex = pinning.start.indexOf(columnId);
183
+ if (startIndex >= 0) {
184
+ return {
185
+ edge: 'start',
186
+ isEdgeDivider: startIndex === pinning.start.length - 1,
187
+ };
188
+ }
189
+ const endIndex = pinning.end.indexOf(columnId);
190
+ if (endIndex >= 0) {
191
+ return {
192
+ edge: 'end',
193
+ isEdgeDivider: endIndex === 0,
194
+ };
195
+ }
196
+ return { edge: false, isEdgeDivider: false };
197
+ }
198
+
199
+ function mergePinnedStyle(
200
+ base: ViewStyle | undefined,
201
+ pin: PinRenderMeta,
202
+ offsets: PinnedOffsetMaps | null,
203
+ columnId: string,
204
+ isHeader: boolean,
205
+ ): ViewStyle | undefined {
206
+ if (!pin.edge || offsets == null) {
207
+ return base;
208
+ }
209
+ const offset =
210
+ pin.edge === 'start' ? (offsets.start.get(columnId) ?? 0) : (offsets.end.get(columnId) ?? 0);
211
+ return { ...base, ...pinnedCellStyle(pin.edge, offset, isHeader) };
212
+ }
213
+
214
+ function pinnedClassName(pin: PinRenderMeta, isHeader: boolean): string | undefined {
215
+ if (!pin.edge) {
216
+ return undefined;
217
+ }
218
+ return cn(
219
+ dataGridPinnedSurfaceVariants({ header: isHeader }),
220
+ dataGridPinnedEdgeVariants({ edge: pin.isEdgeDivider ? pin.edge : 'none' }),
221
+ );
222
+ }
223
+
224
+ function BooleanCell({ value }: { value: boolean }) {
225
+ return (
226
+ <Icon as={value ? Check : Close} accessibilityLabel={value ? 'Yes' : 'No'} className="size-5" />
227
+ );
228
+ }
229
+
127
230
  function defaultCellContent(value: unknown): ReactNode {
128
231
  if (value == null) {
129
232
  return '';
@@ -148,12 +251,25 @@ function DefaultLoadingRows<R extends GridValidRowModel>({
148
251
  checkboxSelection,
149
252
  rowCount,
150
253
  columnWidths,
254
+ orderedColumnIds,
255
+ pinning,
256
+ offsets,
151
257
  }: {
152
258
  columns: readonly GridColDef<R>[];
153
259
  checkboxSelection: boolean;
154
260
  rowCount: number;
155
261
  columnWidths: ReadonlyMap<string, CSSProperties['width']>;
262
+ orderedColumnIds?: readonly string[];
263
+ pinning: { start: string[]; end: string[] };
264
+ offsets: PinnedOffsetMaps | null;
156
265
  }) {
266
+ const colByField = new Map(columns.map((column) => [column.field, column]));
267
+ const ids =
268
+ orderedColumnIds ??
269
+ (checkboxSelection
270
+ ? [SELECT_COLUMN_ID, ...columns.map((column) => column.field)]
271
+ : columns.map((column) => column.field));
272
+
157
273
  return (
158
274
  <>
159
275
  {Array.from({ length: rowCount }, (_, rowIndex) => (
@@ -162,21 +278,41 @@ function DefaultLoadingRows<R extends GridValidRowModel>({
162
278
  className="pointer-events-none web:hover:bg-surface-primary"
163
279
  testID={rowIndex === 0 ? 'data-grid-default-loading-rows' : undefined}
164
280
  >
165
- {checkboxSelection ? (
166
- <Table.Cell align="center" className={dataGridSelectCellVariants()}>
167
- <Skeleton className="size-5" />
168
- </Table.Cell>
169
- ) : null}
170
- {columns.map((column) => (
171
- <Table.Cell
172
- key={column.field}
173
- align={normalizeGridAlignment(column.align)}
174
- className={dataGridCellVariants()}
175
- style={cellStyle(column, columnWidths.get(column.field))}
176
- >
177
- <Skeleton className="h-4 w-3/5" />
178
- </Table.Cell>
179
- ))}
281
+ {ids.map((columnId) => {
282
+ const pin = pinMetaFor(columnId, pinning);
283
+ if (columnId === SELECT_COLUMN_ID) {
284
+ return (
285
+ <Table.Cell
286
+ key={columnId}
287
+ align="center"
288
+ className={cn(dataGridSelectCellVariants(), pinnedClassName(pin, false))}
289
+ style={mergePinnedStyle(undefined, pin, offsets, columnId, false)}
290
+ >
291
+ <Skeleton className="size-5" />
292
+ </Table.Cell>
293
+ );
294
+ }
295
+ const column = colByField.get(columnId);
296
+ if (!column) {
297
+ return null;
298
+ }
299
+ return (
300
+ <Table.Cell
301
+ key={column.field}
302
+ align={resolveColumnAlign(column)}
303
+ className={cn(dataGridCellVariants(), pinnedClassName(pin, false))}
304
+ style={mergePinnedStyle(
305
+ cellStyle(column, columnWidths.get(column.field)),
306
+ pin,
307
+ offsets,
308
+ column.field,
309
+ false,
310
+ )}
311
+ >
312
+ <Skeleton className="h-4 w-3/5" />
313
+ </Table.Cell>
314
+ );
315
+ })}
180
316
  </Table.Row>
181
317
  ))}
182
318
  </>
@@ -219,6 +355,7 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
219
355
  rowCount,
220
356
  showToolbar = false,
221
357
  headerFilters = false,
358
+ pinnedColumns,
222
359
  pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
223
360
  noRowsOverlay,
224
361
  loadingOverlay,
@@ -245,6 +382,11 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
245
382
  defaultProp: props.defaultFilterModel ?? [],
246
383
  onChange: props.onFilterModelChange,
247
384
  });
385
+ const [filterLogicOperator, setFilterLogicOperator] = useControllableState({
386
+ prop: props.filterLogicOperator,
387
+ defaultProp: props.defaultFilterLogicOperator ?? 'and',
388
+ onChange: props.onFilterLogicOperatorChange,
389
+ });
248
390
  const [quickFilter, setQuickFilter] = useControllableState({
249
391
  prop: props.quickFilter,
250
392
  defaultProp: props.defaultQuickFilter ?? '',
@@ -259,13 +401,23 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
259
401
  const resolvedSortModel = sortModel ?? [];
260
402
  const resolvedPagination = paginationModel ?? DEFAULT_PAGINATION_MODEL;
261
403
  const resolvedFilterModel = filterModel ?? [];
404
+ const resolvedFilterLogicOperator = filterLogicOperator ?? 'and';
262
405
  const resolvedQuickFilter = quickFilter ?? '';
263
406
  const resolvedSelection = rowSelectionModel ?? EMPTY_SELECTION;
264
407
 
408
+ const knownFields = useMemo(() => new Set(columns.map((column) => column.field)), [columns]);
409
+ const columnPinning = useMemo(
410
+ () => normalizePinnedColumns(pinnedColumns, knownFields, checkboxSelection),
411
+ [pinnedColumns, knownFields, checkboxSelection],
412
+ );
413
+ const pinningActive = isColumnPinningActive(columnPinning);
414
+ const pinnedFields = useMemo(() => pinnedFieldSet(columnPinning), [columnPinning]);
415
+
265
416
  const {
266
417
  table,
267
418
  setQuickFilter: setQuickFilterWithPageReset,
268
419
  setColumnFilter,
420
+ setFilterModel: setFilterModelWithPageReset,
269
421
  } = useDataGridTable({
270
422
  rows,
271
423
  columns,
@@ -282,10 +434,12 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
282
434
  onPaginationModelChange: (model) => setPaginationModel(model),
283
435
  filterModel: resolvedFilterModel,
284
436
  onFilterModelChange: (model) => setFilterModel(model),
437
+ filterLogicOperator: resolvedFilterLogicOperator,
285
438
  quickFilter: resolvedQuickFilter,
286
439
  onQuickFilterChange: (value) => setQuickFilter(value),
287
440
  rowSelectionModel: resolvedSelection,
288
441
  onRowSelectionModelChange: (model) => setRowSelectionModel(model),
442
+ columnPinning,
289
443
  });
290
444
 
291
445
  const colByField = useMemo(() => {
@@ -295,11 +449,47 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
295
449
  }
296
450
  return map;
297
451
  }, [columns]);
452
+
453
+ warnPinnedFlexColumns(columns, pinnedFields);
454
+
298
455
  const columnWidths = useMemo(
299
- () => webColumnWidths(columns, checkboxSelection),
300
- [checkboxSelection, columns],
456
+ () => webColumnWidths(columns, checkboxSelection, pinnedFields),
457
+ [checkboxSelection, columns, pinnedFields],
458
+ );
459
+ const hasFlexColumns = useMemo(
460
+ () => columns.some((column) => effectiveFlex(column, pinnedFields) > 0),
461
+ [columns, pinnedFields],
301
462
  );
302
- const hasFlexColumns = useMemo(() => columns.some((column) => (column.flex ?? 0) > 0), [columns]);
463
+ const pinnedOffsets = useMemo(
464
+ () => (pinningActive ? buildPinnedOffsets(columnPinning, colByField) : null),
465
+ [pinningActive, columnPinning, colByField],
466
+ );
467
+ const orderedLeafColumnIds = useMemo(() => {
468
+ if (!pinningActive) {
469
+ return undefined;
470
+ }
471
+ return [
472
+ ...table.getStartVisibleLeafColumns().map((column) => column.id),
473
+ ...table.getCenterVisibleLeafColumns().map((column) => column.id),
474
+ ...table.getEndVisibleLeafColumns().map((column) => column.id),
475
+ ];
476
+ }, [pinningActive, table]);
477
+ const isEmpty = !loading && rows.length === 0;
478
+ const mainScrollStyle = useMemo((): ViewStyle | undefined => {
479
+ if (!pinningActive || pinnedOffsets == null) {
480
+ return undefined;
481
+ }
482
+ const guides =
483
+ Platform.OS === 'web' && !isEmpty
484
+ ? pinnedEdgeGuidesImage(pinnedOffsets, PINNED_EDGE_COLOR, I18nManager.isRTL)
485
+ : undefined;
486
+ // Logical scroll-padding keeps focused cells out from under pinned edges (web).
487
+ return {
488
+ scrollPaddingInlineStart: pinnedOffsets.startTotal,
489
+ scrollPaddingInlineEnd: pinnedOffsets.endTotal,
490
+ ...(guides ? { backgroundImage: guides, backgroundRepeat: 'no-repeat' } : {}),
491
+ } as ViewStyle;
492
+ }, [pinningActive, pinnedOffsets, isEmpty]);
303
493
 
304
494
  const visibleRows = table.getRowModel().rows;
305
495
  const pageCount = Math.max(table.getPageCount(), 1);
@@ -313,17 +503,29 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
313
503
  : { checked: false, indeterminate: true };
314
504
 
315
505
  const accessibleName = ariaLabel ?? label;
316
- const isEmpty = !loading && rows.length === 0;
317
506
  const totalCount = paginationMode === 'server' ? (rowCount ?? rows.length) : filterableCount;
318
507
 
319
- const filterValueFor = (field: string) =>
320
- resolvedFilterModel.find((item) => item.field === field)?.value ?? '';
508
+ const filterValueFor = (field: string) => {
509
+ const value = resolvedFilterModel.find((item) => item.field === field)?.value;
510
+ return typeof value === 'string' ? value : '';
511
+ };
321
512
 
322
513
  const showToolbarBar = showToolbar || toolbarActions != null;
514
+ const filterPanelColumns = useMemo(
515
+ () => columns.filter((column) => column.filterable !== false),
516
+ [columns],
517
+ );
518
+ const [filterPanelOpen, setFilterPanelOpen] = useState(false);
519
+ const filterPanelButtonRef = useRef<View>(null);
520
+ const showFilterPanelButton = showToolbar && filterPanelColumns.length > 0;
323
521
 
324
522
  return (
325
523
  <View
326
- className={cn(dataGridRootVariants({ autoHeight }), className)}
524
+ className={cn(
525
+ dataGridRootVariants({ autoHeight }),
526
+ className,
527
+ filterPanelOpen && 'overflow-visible',
528
+ )}
327
529
  style={style}
328
530
  {...mergeDataAttributes({}, dataAttributes({ slot: 'data-grid' }))}
329
531
  >
@@ -340,17 +542,46 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
340
542
  />
341
543
  </Input>
342
544
  ) : null}
545
+ {showFilterPanelButton ? (
546
+ <IconButton
547
+ ref={filterPanelButtonRef}
548
+ as={FilterList}
549
+ variant="ghost"
550
+ color="neutral"
551
+ size="small"
552
+ accessibilityLabel="Filters"
553
+ accessibilityState={{ expanded: filterPanelOpen }}
554
+ onPress={() => setFilterPanelOpen((open) => !open)}
555
+ />
556
+ ) : null}
343
557
  {toolbarActions ? (
344
558
  <View className={dataGridToolbarActionsVariants()}>{toolbarActions}</View>
345
559
  ) : null}
346
560
  </View>
347
561
  ) : null}
562
+ {showFilterPanelButton && filterPanelOpen ? (
563
+ <DataGridFilterPanel
564
+ columns={filterPanelColumns}
565
+ filterModel={resolvedFilterModel}
566
+ onFilterModelChange={setFilterModelWithPageReset}
567
+ logicOperator={resolvedFilterLogicOperator}
568
+ onLogicOperatorChange={(operator) => {
569
+ setFilterLogicOperator(operator);
570
+ if (resolvedPagination.page !== 0) {
571
+ setPaginationModel({ ...resolvedPagination, page: 0 });
572
+ }
573
+ }}
574
+ onClose={() => setFilterPanelOpen(false)}
575
+ triggerRef={filterPanelButtonRef}
576
+ />
577
+ ) : null}
348
578
 
349
579
  <View
350
580
  className={dataGridMainVariants({
351
581
  autoHeight,
352
582
  overlay: isEmpty || (loading && loadingOverlay != null),
353
583
  })}
584
+ style={mainScrollStyle}
354
585
  >
355
586
  <Table
356
587
  className={cn(
@@ -370,28 +601,50 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
370
601
  {label ? <Table.Caption>{label}</Table.Caption> : null}
371
602
  {Platform.OS === 'web' && hasFlexColumns ? (
372
603
  <colgroup>
373
- {checkboxSelection ? <col style={{ width: SELECT_COLUMN_WIDTH }} /> : null}
374
- {columns.map((column) => (
375
- <col
376
- key={column.field}
377
- style={{
378
- width: columnWidths.get(column.field),
379
- minWidth: column.minWidth,
380
- }}
381
- />
382
- ))}
604
+ {(
605
+ orderedLeafColumnIds ??
606
+ (checkboxSelection
607
+ ? [SELECT_COLUMN_ID, ...columns.map((column) => column.field)]
608
+ : columns.map((column) => column.field))
609
+ ).map((columnId) =>
610
+ columnId === SELECT_COLUMN_ID ? (
611
+ <col key={columnId} style={{ width: SELECT_COLUMN_WIDTH }} />
612
+ ) : (
613
+ <col
614
+ key={columnId}
615
+ style={{
616
+ width: columnWidths.get(columnId),
617
+ minWidth: colByField.get(columnId)?.minWidth,
618
+ }}
619
+ />
620
+ ),
621
+ )}
383
622
  </colgroup>
384
623
  ) : null}
385
624
  <Table.Header className={dataGridHeaderVariants()}>
386
- {table.getHeaderGroups().map((headerGroup) => (
625
+ {(pinningActive
626
+ ? [
627
+ {
628
+ id: 'pinned',
629
+ headers: [
630
+ ...table.getStartLeafHeaders(),
631
+ ...table.getCenterLeafHeaders(),
632
+ ...table.getEndLeafHeaders(),
633
+ ],
634
+ },
635
+ ]
636
+ : table.getHeaderGroups()
637
+ ).map((headerGroup) => (
387
638
  <Table.Row key={headerGroup.id}>
388
639
  {headerGroup.headers.map((header) => {
640
+ const pin = pinMetaFor(header.column.id, columnPinning);
389
641
  if (header.id === SELECT_COLUMN_ID) {
390
642
  return (
391
643
  <Table.Head
392
644
  key={header.id}
393
645
  align="center"
394
- className={dataGridSelectHeadVariants()}
646
+ className={cn(dataGridSelectHeadVariants(), pinnedClassName(pin, true))}
647
+ style={mergePinnedStyle(undefined, pin, pinnedOffsets, header.id, true)}
395
648
  >
396
649
  <Checkbox
397
650
  value="select-all"
@@ -408,7 +661,7 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
408
661
  const col = colByField.get(header.column.id);
409
662
  const sorted = header.column.getIsSorted();
410
663
  const canSort = !disableColumnSorting && header.column.getCanSort();
411
- const headerAlign = normalizeGridAlignment(col?.headerAlign ?? col?.align);
664
+ const headerAlign = resolveColumnHeaderAlign(col);
412
665
  const headerName = col?.headerName ?? header.column.id;
413
666
  const SortIcon = sorted === 'asc' ? KeyboardArrowUp : KeyboardArrowDown;
414
667
  const sortButton = canSort ? (
@@ -433,8 +686,14 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
433
686
  <Table.Head
434
687
  key={header.id}
435
688
  align={headerAlign}
436
- className={dataGridHeadVariants()}
437
- style={cellStyle(col, columnWidths.get(header.column.id))}
689
+ className={cn(dataGridHeadVariants(), pinnedClassName(pin, true))}
690
+ style={mergePinnedStyle(
691
+ cellStyle(col, columnWidths.get(header.column.id)),
692
+ pin,
693
+ pinnedOffsets,
694
+ header.column.id,
695
+ true,
696
+ )}
438
697
  {...(Platform.OS === 'web'
439
698
  ? {
440
699
  title: col?.renderHeader ? undefined : headerName,
@@ -470,34 +729,54 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
470
729
  ))}
471
730
  {headerFilters ? (
472
731
  <Table.Row>
473
- {table.getAllLeafColumns().map((column) => {
732
+ {(pinningActive
733
+ ? [
734
+ ...table.getStartVisibleLeafColumns(),
735
+ ...table.getCenterVisibleLeafColumns(),
736
+ ...table.getEndVisibleLeafColumns(),
737
+ ]
738
+ : table.getAllLeafColumns()
739
+ ).map((column) => {
740
+ const pin = pinMetaFor(column.id, columnPinning);
474
741
  if (column.id === SELECT_COLUMN_ID) {
475
742
  return (
476
743
  <Table.Cell
477
744
  key={column.id}
478
745
  align="center"
479
- className={dataGridSelectHeadVariants()}
746
+ className={cn(dataGridSelectHeadVariants(), pinnedClassName(pin, true))}
747
+ style={mergePinnedStyle(undefined, pin, pinnedOffsets, column.id, true)}
480
748
  />
481
749
  );
482
750
  }
483
751
  const col = colByField.get(column.id);
484
- const headerAlign = col?.headerAlign ?? col?.align;
485
752
  if (col?.filterable === false) {
486
753
  return (
487
754
  <Table.Head
488
755
  key={column.id}
489
- align={normalizeGridAlignment(headerAlign)}
490
- className={dataGridHeadVariants()}
491
- style={cellStyle(col, columnWidths.get(column.id))}
756
+ align={resolveColumnHeaderAlign(col)}
757
+ className={cn(dataGridHeadVariants(), pinnedClassName(pin, true))}
758
+ style={mergePinnedStyle(
759
+ cellStyle(col, columnWidths.get(column.id)),
760
+ pin,
761
+ pinnedOffsets,
762
+ column.id,
763
+ true,
764
+ )}
492
765
  />
493
766
  );
494
767
  }
495
768
  return (
496
769
  <Table.Head
497
770
  key={column.id}
498
- align={normalizeGridAlignment(headerAlign)}
499
- className={dataGridHeadVariants()}
500
- style={cellStyle(col, columnWidths.get(column.id))}
771
+ align={resolveColumnHeaderAlign(col)}
772
+ className={cn(dataGridHeadVariants(), pinnedClassName(pin, true))}
773
+ style={mergePinnedStyle(
774
+ cellStyle(col, columnWidths.get(column.id)),
775
+ pin,
776
+ pinnedOffsets,
777
+ column.id,
778
+ true,
779
+ )}
501
780
  >
502
781
  <Input size="small" className={dataGridFilterInputVariants()}>
503
782
  <Input.Field
@@ -520,19 +799,40 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
520
799
  checkboxSelection={checkboxSelection}
521
800
  rowCount={Math.min(resolvedPagination.pageSize, MAX_SKELETON_ROW_COUNT)}
522
801
  columnWidths={columnWidths}
802
+ orderedColumnIds={orderedLeafColumnIds}
803
+ pinning={columnPinning}
804
+ offsets={pinnedOffsets}
523
805
  />
524
806
  ) : isEmpty ? null : (
525
807
  visibleRows.map((row) => {
526
808
  const rowId = resolveRowId(row.original, getRowId);
809
+ const cells = pinningActive
810
+ ? [
811
+ ...row.getStartVisibleCells(),
812
+ ...row.getCenterVisibleCells(),
813
+ ...row.getEndVisibleCells(),
814
+ ]
815
+ : row.getAllCells();
527
816
  return (
528
817
  <Table.Row key={row.id}>
529
- {row.getAllCells().map((cell) => {
818
+ {cells.map((cell) => {
819
+ const pin = pinMetaFor(cell.column.id, columnPinning);
530
820
  if (cell.column.id === SELECT_COLUMN_ID) {
531
821
  return (
532
822
  <Table.Cell
533
823
  key={cell.id}
534
824
  align="center"
535
- className={dataGridSelectCellVariants()}
825
+ className={cn(
826
+ dataGridSelectCellVariants(),
827
+ pinnedClassName(pin, false),
828
+ )}
829
+ style={mergePinnedStyle(
830
+ undefined,
831
+ pin,
832
+ pinnedOffsets,
833
+ cell.column.id,
834
+ false,
835
+ )}
536
836
  >
537
837
  <Checkbox
538
838
  value={String(rowId)}
@@ -554,19 +854,29 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
554
854
  row: row.original,
555
855
  field: col.field,
556
856
  id: rowId,
857
+ col,
557
858
  });
558
859
  } else if (col?.valueFormatter) {
559
860
  content = col.valueFormatter(value, row.original, col);
861
+ } else if (col?.type === 'boolean' && typeof value === 'boolean') {
862
+ content = <BooleanCell value={value} />;
560
863
  } else {
561
- content = defaultCellContent(value);
864
+ const typedText = getTypedCellText(col, value, row.original);
865
+ content = typedText !== undefined ? typedText : defaultCellContent(value);
562
866
  }
563
867
 
564
868
  return (
565
869
  <Table.Cell
566
870
  key={cell.id}
567
- align={normalizeGridAlignment(col?.align)}
568
- className={dataGridCellVariants()}
569
- style={cellStyle(col, columnWidths.get(cell.column.id))}
871
+ align={resolveColumnAlign(col)}
872
+ className={cn(dataGridCellVariants(), pinnedClassName(pin, false))}
873
+ style={mergePinnedStyle(
874
+ cellStyle(col, columnWidths.get(cell.column.id)),
875
+ pin,
876
+ pinnedOffsets,
877
+ cell.column.id,
878
+ false,
879
+ )}
570
880
  >
571
881
  {content}
572
882
  </Table.Cell>