@cdx-ui/data-grid 0.3.11 → 0.3.13

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 (96) hide show
  1. package/README.md +3 -1
  2. package/coverage.svg +1 -1
  3. package/lib/commonjs/DataGrid.js +490 -81
  4. package/lib/commonjs/DataGrid.js.map +1 -1
  5. package/lib/commonjs/DataGridFilterPanel.js +547 -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 +140 -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 +105 -0
  14. package/lib/commonjs/adapter/pinning.js.map +1 -0
  15. package/lib/commonjs/adapter/selectColumn.js +11 -0
  16. package/lib/commonjs/adapter/selectColumn.js.map +1 -1
  17. package/lib/commonjs/adapter/treeData.js +195 -0
  18. package/lib/commonjs/adapter/treeData.js.map +1 -0
  19. package/lib/commonjs/index.js +26 -0
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/commonjs/numberCells.js +280 -0
  22. package/lib/commonjs/numberCells.js.map +1 -0
  23. package/lib/commonjs/styles.js +107 -1
  24. package/lib/commonjs/styles.js.map +1 -1
  25. package/lib/commonjs/types.js +39 -1
  26. package/lib/commonjs/types.js.map +1 -1
  27. package/lib/commonjs/useDataGridTable.js +234 -18
  28. package/lib/commonjs/useDataGridTable.js.map +1 -1
  29. package/lib/module/DataGrid.js +497 -88
  30. package/lib/module/DataGrid.js.map +1 -1
  31. package/lib/module/DataGridFilterPanel.js +540 -0
  32. package/lib/module/DataGridFilterPanel.js.map +1 -0
  33. package/lib/module/adapter/columnTypes.js +240 -0
  34. package/lib/module/adapter/columnTypes.js.map +1 -0
  35. package/lib/module/adapter/columns.js +137 -4
  36. package/lib/module/adapter/columns.js.map +1 -1
  37. package/lib/module/adapter/filterOperators.js +406 -0
  38. package/lib/module/adapter/filterOperators.js.map +1 -0
  39. package/lib/module/adapter/pinning.js +94 -0
  40. package/lib/module/adapter/pinning.js.map +1 -0
  41. package/lib/module/adapter/selectColumn.js +11 -1
  42. package/lib/module/adapter/selectColumn.js.map +1 -1
  43. package/lib/module/adapter/treeData.js +187 -0
  44. package/lib/module/adapter/treeData.js.map +1 -0
  45. package/lib/module/index.js +2 -0
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/numberCells.js +275 -0
  48. package/lib/module/numberCells.js.map +1 -0
  49. package/lib/module/styles.js +106 -0
  50. package/lib/module/styles.js.map +1 -1
  51. package/lib/module/types.js +38 -0
  52. package/lib/module/types.js.map +1 -1
  53. package/lib/module/useDataGridTable.js +238 -22
  54. package/lib/module/useDataGridTable.js.map +1 -1
  55. package/lib/typescript/DataGrid.d.ts.map +1 -1
  56. package/lib/typescript/DataGridFilterPanel.d.ts +52 -0
  57. package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -0
  58. package/lib/typescript/adapter/columnTypes.d.ts +32 -0
  59. package/lib/typescript/adapter/columnTypes.d.ts.map +1 -0
  60. package/lib/typescript/adapter/columns.d.ts +14 -5
  61. package/lib/typescript/adapter/columns.d.ts.map +1 -1
  62. package/lib/typescript/adapter/filterOperators.d.ts +43 -0
  63. package/lib/typescript/adapter/filterOperators.d.ts.map +1 -0
  64. package/lib/typescript/adapter/pinning.d.ts +26 -0
  65. package/lib/typescript/adapter/pinning.d.ts.map +1 -0
  66. package/lib/typescript/adapter/selectColumn.d.ts +8 -0
  67. package/lib/typescript/adapter/selectColumn.d.ts.map +1 -1
  68. package/lib/typescript/adapter/treeData.d.ts +36 -0
  69. package/lib/typescript/adapter/treeData.d.ts.map +1 -0
  70. package/lib/typescript/index.d.ts +4 -1
  71. package/lib/typescript/index.d.ts.map +1 -1
  72. package/lib/typescript/numberCells.d.ts +46 -0
  73. package/lib/typescript/numberCells.d.ts.map +1 -0
  74. package/lib/typescript/styles.d.ts +32 -0
  75. package/lib/typescript/styles.d.ts.map +1 -1
  76. package/lib/typescript/types.d.ts +154 -7
  77. package/lib/typescript/types.d.ts.map +1 -1
  78. package/lib/typescript/useDataGridTable.d.ts +40 -4
  79. package/lib/typescript/useDataGridTable.d.ts.map +1 -1
  80. package/package.json +5 -5
  81. package/src/DataGrid.tsx +745 -95
  82. package/src/DataGridFilterPanel.tsx +648 -0
  83. package/src/adapter/columnTypes.ts +327 -0
  84. package/src/adapter/columns.test.ts +75 -1
  85. package/src/adapter/columns.ts +192 -3
  86. package/src/adapter/filterOperators.test.ts +292 -0
  87. package/src/adapter/filterOperators.ts +524 -0
  88. package/src/adapter/pinning.ts +134 -0
  89. package/src/adapter/selectColumn.ts +12 -1
  90. package/src/adapter/treeData.ts +228 -0
  91. package/src/index.ts +19 -0
  92. package/src/numberCells.tsx +379 -0
  93. package/src/styles.ts +131 -0
  94. package/src/types.ts +190 -7
  95. package/src/useDataGridTable.test.ts +69 -0
  96. package/src/useDataGridTable.ts +338 -17
package/src/DataGrid.tsx CHANGED
@@ -1,7 +1,16 @@
1
- import { useMemo, type CSSProperties, type ReactNode } from 'react';
2
- import { Platform, View, type ViewStyle } from 'react-native';
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type CSSProperties,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import { I18nManager, Platform, View, type TextInput, type ViewStyle } from 'react-native';
3
11
  import {
4
12
  Checkbox,
13
+ Icon,
5
14
  IconButton,
6
15
  Input,
7
16
  Skeleton,
@@ -9,13 +18,43 @@ import {
9
18
  TablePagination,
10
19
  Text,
11
20
  } from '@cdx-ui/components';
12
- import { KeyboardArrowDown, KeyboardArrowUp, Search } from '@cdx-ui/icons';
21
+ import {
22
+ Check,
23
+ Close,
24
+ FilterList,
25
+ KeyboardArrowDown,
26
+ KeyboardArrowRight,
27
+ KeyboardArrowUp,
28
+ Search,
29
+ } from '@cdx-ui/icons';
13
30
  import { dataAttributes, mergeDataAttributes } from '@cdx-ui/primitives';
14
31
  import { cn, useControllableState } from '@cdx-ui/utils';
15
- import { normalizeGridAlignment } from './adapter/columns';
32
+ import {
33
+ normalizePinnedColumns,
34
+ resolveColumnAlign,
35
+ resolveColumnHeaderAlign,
36
+ } from './adapter/columns';
37
+ import {
38
+ DEFAULT_PINNED_COLUMN_WIDTH,
39
+ DETAIL_PANEL_TOGGLE_WIDTH,
40
+ SELECT_COLUMN_WIDTH,
41
+ buildPinnedOffsets,
42
+ isColumnPinningActive,
43
+ pinnedCellStyle,
44
+ pinnedEdgeGuidesImage,
45
+ pinnedFieldSet,
46
+ warnPinnedFlexColumns,
47
+ type ColumnPinEdge,
48
+ type PinnedOffsetMaps,
49
+ } from './adapter/pinning';
50
+ import { getTypedCellText } from './adapter/columnTypes';
51
+ import { DataGridFilterPanel, useDataGridFilterPanel } from './DataGridFilterPanel';
16
52
  import {
17
53
  dataGridBodyVariants,
54
+ dataGridCellTextVariants,
18
55
  dataGridCellVariants,
56
+ dataGridDetailPanelCellVariants,
57
+ dataGridDetailToggleCellVariants,
19
58
  dataGridEmptyOverlayVariants,
20
59
  dataGridFilterInputVariants,
21
60
  dataGridFooterVariants,
@@ -27,6 +66,8 @@ import {
27
66
  dataGridOverlayVariants,
28
67
  dataGridPaginationRangeVariants,
29
68
  dataGridPaginationVariants,
69
+ dataGridPinnedEdgeVariants,
70
+ dataGridPinnedSurfaceVariants,
30
71
  dataGridRootVariants,
31
72
  dataGridSelectCellVariants,
32
73
  dataGridSelectHeadVariants,
@@ -35,22 +76,38 @@ import {
35
76
  dataGridToolbarActionsVariants,
36
77
  dataGridToolbarSearchVariants,
37
78
  dataGridToolbarVariants,
79
+ dataGridTreeCellInnerVariants,
80
+ dataGridTreeContentVariants,
81
+ dataGridTreeToggleSlotVariants,
38
82
  } from './styles';
39
83
  import {
40
84
  DEFAULT_PAGE_SIZE_OPTIONS,
41
85
  DEFAULT_PAGINATION_MODEL,
86
+ DETAIL_PANEL_TOGGLE_ID,
87
+ EMPTY_ROW_ID_SET,
42
88
  EMPTY_SELECTION,
43
89
  SELECT_COLUMN_ID,
44
90
  type DataGridProps,
45
91
  type GridColDef,
92
+ type GridRowId,
46
93
  type GridValidRowModel,
47
94
  } from './types';
48
95
  import { resolveRowId, useDataGridTable } from './useDataGridTable';
49
96
 
50
97
  const MAX_SKELETON_ROW_COUNT = 10;
98
+ /** Matches the detail panel cell's `px-4`. */
99
+ const DETAIL_PANEL_INSET = 16;
100
+
101
+ const TREE_INDENT = 24;
51
102
  const DEFAULT_WEB_COLUMN_WIDTH = 100;
52
- const SELECT_COLUMN_WIDTH = 56;
53
103
  const WEB_TABLE_CONTAINER_STYLE: ViewStyle = { overflow: 'visible' };
104
+ // Same token as the pinned cells' `border-stroke-secondary` divider.
105
+ const PINNED_EDGE_COLOR = 'var(--color-stroke-secondary)';
106
+
107
+ interface PinRenderMeta {
108
+ edge: ColumnPinEdge | false;
109
+ isEdgeDivider: boolean;
110
+ }
54
111
 
55
112
  function cellStyle<R extends GridValidRowModel>(
56
113
  col: GridColDef<R> | undefined,
@@ -78,16 +135,32 @@ function cellStyle<R extends GridValidRowModel>(
78
135
  return Object.keys(style).length > 0 ? style : undefined;
79
136
  }
80
137
 
138
+ function effectiveFlex<R extends GridValidRowModel>(
139
+ column: GridColDef<R>,
140
+ pinned: ReadonlySet<string>,
141
+ ): number {
142
+ if (pinned.has(column.field)) {
143
+ return 0;
144
+ }
145
+ return Math.max(column.flex ?? 0, 0);
146
+ }
147
+
81
148
  function webColumnWidths<R extends GridValidRowModel>(
82
149
  columns: readonly GridColDef<R>[],
83
- checkboxSelection: boolean,
150
+ leadingWidth: number,
151
+ pinned: ReadonlySet<string> = new Set(),
84
152
  ): Map<string, CSSProperties['width']> {
85
- const totalFlex = columns.reduce((total, column) => total + Math.max(column.flex ?? 0, 0), 0);
153
+ const totalFlex = columns.reduce((total, column) => total + effectiveFlex(column, pinned), 0);
86
154
  const widths = new Map<string, CSSProperties['width']>();
87
155
 
88
156
  if (totalFlex === 0) {
89
157
  for (const column of columns) {
90
- if (column.width !== undefined) {
158
+ if (pinned.has(column.field)) {
159
+ widths.set(
160
+ column.field,
161
+ Math.max(column.width ?? DEFAULT_PINNED_COLUMN_WIDTH, column.minWidth ?? 0),
162
+ );
163
+ } else if (column.width !== undefined) {
91
164
  widths.set(column.field, column.width);
92
165
  }
93
166
  }
@@ -95,17 +168,21 @@ function webColumnWidths<R extends GridValidRowModel>(
95
168
  }
96
169
 
97
170
  const fixedWidth =
98
- (checkboxSelection ? SELECT_COLUMN_WIDTH : 0) +
171
+ leadingWidth +
99
172
  columns.reduce((total, column) => {
100
- if ((column.flex ?? 0) > 0) {
173
+ if (effectiveFlex(column, pinned) > 0) {
101
174
  return total;
102
175
  }
103
- return total + Math.max(column.width ?? DEFAULT_WEB_COLUMN_WIDTH, column.minWidth ?? 0);
176
+ const fallback = pinned.has(column.field)
177
+ ? DEFAULT_PINNED_COLUMN_WIDTH
178
+ : DEFAULT_WEB_COLUMN_WIDTH;
179
+ return total + Math.max(column.width ?? fallback, column.minWidth ?? 0);
104
180
  }, 0);
105
181
 
106
182
  for (const column of columns) {
107
- if ((column.flex ?? 0) > 0) {
108
- const share = (column.flex ?? 0) / totalFlex;
183
+ const flex = effectiveFlex(column, pinned);
184
+ if (flex > 0) {
185
+ const share = flex / totalFlex;
109
186
  const percentage = share * 100;
110
187
  const fixedShare = fixedWidth * share;
111
188
  const width = `calc(${percentage.toString()}% - ${fixedShare.toString()}px)`;
@@ -114,16 +191,65 @@ function webColumnWidths<R extends GridValidRowModel>(
114
191
  column.minWidth === undefined ? width : `max(${column.minWidth.toString()}px, ${width})`,
115
192
  );
116
193
  } else {
117
- widths.set(
118
- column.field,
119
- Math.max(column.width ?? DEFAULT_WEB_COLUMN_WIDTH, column.minWidth ?? 0),
120
- );
194
+ const fallback = pinned.has(column.field)
195
+ ? DEFAULT_PINNED_COLUMN_WIDTH
196
+ : DEFAULT_WEB_COLUMN_WIDTH;
197
+ widths.set(column.field, Math.max(column.width ?? fallback, column.minWidth ?? 0));
121
198
  }
122
199
  }
123
200
 
124
201
  return widths;
125
202
  }
126
203
 
204
+ function pinMetaFor(columnId: string, pinning: { start: string[]; end: string[] }): PinRenderMeta {
205
+ const startIndex = pinning.start.indexOf(columnId);
206
+ if (startIndex >= 0) {
207
+ return {
208
+ edge: 'start',
209
+ isEdgeDivider: startIndex === pinning.start.length - 1,
210
+ };
211
+ }
212
+ const endIndex = pinning.end.indexOf(columnId);
213
+ if (endIndex >= 0) {
214
+ return {
215
+ edge: 'end',
216
+ isEdgeDivider: endIndex === 0,
217
+ };
218
+ }
219
+ return { edge: false, isEdgeDivider: false };
220
+ }
221
+
222
+ function mergePinnedStyle(
223
+ base: ViewStyle | undefined,
224
+ pin: PinRenderMeta,
225
+ offsets: PinnedOffsetMaps | null,
226
+ columnId: string,
227
+ isHeader: boolean,
228
+ ): ViewStyle | undefined {
229
+ if (!pin.edge || offsets == null) {
230
+ return base;
231
+ }
232
+ const offset =
233
+ pin.edge === 'start' ? (offsets.start.get(columnId) ?? 0) : (offsets.end.get(columnId) ?? 0);
234
+ return { ...base, ...pinnedCellStyle(pin.edge, offset, isHeader) };
235
+ }
236
+
237
+ function pinnedClassName(pin: PinRenderMeta, isHeader: boolean): string | undefined {
238
+ if (!pin.edge) {
239
+ return undefined;
240
+ }
241
+ return cn(
242
+ dataGridPinnedSurfaceVariants({ header: isHeader }),
243
+ dataGridPinnedEdgeVariants({ edge: pin.isEdgeDivider ? pin.edge : 'none' }),
244
+ );
245
+ }
246
+
247
+ function BooleanCell({ value }: { value: boolean }) {
248
+ return (
249
+ <Icon as={value ? Check : Close} accessibilityLabel={value ? 'Yes' : 'No'} className="size-5" />
250
+ );
251
+ }
252
+
127
253
  function defaultCellContent(value: unknown): ReactNode {
128
254
  if (value == null) {
129
255
  return '';
@@ -145,15 +271,24 @@ function defaultCellContent(value: unknown): ReactNode {
145
271
 
146
272
  function DefaultLoadingRows<R extends GridValidRowModel>({
147
273
  columns,
148
- checkboxSelection,
274
+ leadingColumnIds,
149
275
  rowCount,
150
276
  columnWidths,
277
+ orderedColumnIds,
278
+ pinning,
279
+ offsets,
151
280
  }: {
152
281
  columns: readonly GridColDef<R>[];
153
- checkboxSelection: boolean;
282
+ leadingColumnIds: readonly string[];
154
283
  rowCount: number;
155
284
  columnWidths: ReadonlyMap<string, CSSProperties['width']>;
285
+ orderedColumnIds?: readonly string[];
286
+ pinning: { start: string[]; end: string[] };
287
+ offsets: PinnedOffsetMaps | null;
156
288
  }) {
289
+ const colByField = new Map(columns.map((column) => [column.field, column]));
290
+ const ids = orderedColumnIds ?? [...leadingColumnIds, ...columns.map((column) => column.field)];
291
+
157
292
  return (
158
293
  <>
159
294
  {Array.from({ length: rowCount }, (_, rowIndex) => (
@@ -162,21 +297,50 @@ function DefaultLoadingRows<R extends GridValidRowModel>({
162
297
  className="pointer-events-none web:hover:bg-surface-primary"
163
298
  testID={rowIndex === 0 ? 'data-grid-default-loading-rows' : undefined}
164
299
  >
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
- ))}
300
+ {ids.map((columnId) => {
301
+ const pin = pinMetaFor(columnId, pinning);
302
+ if (columnId === SELECT_COLUMN_ID) {
303
+ return (
304
+ <Table.Cell
305
+ key={columnId}
306
+ align="center"
307
+ className={cn(dataGridSelectCellVariants(), pinnedClassName(pin, false))}
308
+ style={mergePinnedStyle(undefined, pin, offsets, columnId, false)}
309
+ >
310
+ <Skeleton className="size-5" />
311
+ </Table.Cell>
312
+ );
313
+ }
314
+ if (columnId === DETAIL_PANEL_TOGGLE_ID) {
315
+ return (
316
+ <Table.Cell
317
+ key={columnId}
318
+ className={cn(dataGridDetailToggleCellVariants(), pinnedClassName(pin, false))}
319
+ style={mergePinnedStyle(undefined, pin, offsets, columnId, false)}
320
+ />
321
+ );
322
+ }
323
+ const column = colByField.get(columnId);
324
+ if (!column) {
325
+ return null;
326
+ }
327
+ return (
328
+ <Table.Cell
329
+ key={column.field}
330
+ align={resolveColumnAlign(column)}
331
+ className={cn(dataGridCellVariants(), pinnedClassName(pin, false))}
332
+ style={mergePinnedStyle(
333
+ cellStyle(column, columnWidths.get(column.field)),
334
+ pin,
335
+ offsets,
336
+ column.field,
337
+ false,
338
+ )}
339
+ >
340
+ <Skeleton className="h-4 w-3/5" />
341
+ </Table.Cell>
342
+ );
343
+ })}
180
344
  </Table.Row>
181
345
  ))}
182
346
  </>
@@ -219,6 +383,8 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
219
383
  rowCount,
220
384
  showToolbar = false,
221
385
  headerFilters = false,
386
+ pinnedColumns,
387
+ columnVisibilityModel,
222
388
  pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
223
389
  noRowsOverlay,
224
390
  loadingOverlay,
@@ -228,8 +394,66 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
228
394
  label,
229
395
  'aria-label': ariaLabel,
230
396
  'aria-labelledby': ariaLabelledBy,
397
+ treeData = false,
398
+ getTreeDataPath,
399
+ groupingColDef,
400
+ defaultGroupingExpansionDepth,
401
+ isGroupExpandedByDefault,
402
+ rowSelectionPropagation,
403
+ getDetailPanelContent,
231
404
  } = props;
232
405
 
406
+ const treeActive = treeData && getTreeDataPath !== undefined;
407
+ const isColumnVisible = useCallback(
408
+ (column: GridColDef<R>) => columnVisibilityModel?.[column.field] !== false,
409
+ [columnVisibilityModel],
410
+ );
411
+ const treeField = treeActive ? columns.find(isColumnVisible)?.field : undefined;
412
+ const gridColumns = useMemo((): readonly GridColDef<R>[] => {
413
+ if (treeField === undefined || groupingColDef === undefined) {
414
+ return columns;
415
+ }
416
+ return columns.map((column) =>
417
+ column.field === treeField
418
+ ? ({ ...column, ...groupingColDef, field: column.field } as GridColDef<R>)
419
+ : column,
420
+ );
421
+ }, [columns, groupingColDef, treeField]);
422
+ const visibleColumns = useMemo(
423
+ () => gridColumns.filter(isColumnVisible),
424
+ [gridColumns, isColumnVisible],
425
+ );
426
+ const selectColumn = checkboxSelection && !treeActive;
427
+ const treeCheckbox = checkboxSelection && treeActive;
428
+ const detailPanel = getDetailPanelContent !== undefined;
429
+ const leadingColumnIds = useMemo(
430
+ () => [
431
+ ...(selectColumn ? [SELECT_COLUMN_ID] : []),
432
+ ...(detailPanel ? [DETAIL_PANEL_TOGGLE_ID] : []),
433
+ ],
434
+ [detailPanel, selectColumn],
435
+ );
436
+ const leadingWidth =
437
+ (selectColumn ? SELECT_COLUMN_WIDTH : 0) + (detailPanel ? DETAIL_PANEL_TOGGLE_WIDTH : 0);
438
+
439
+ const [detailPanelExpandedRowIds, setDetailPanelExpandedRowIds] = useControllableState<
440
+ ReadonlySet<GridRowId>
441
+ >({
442
+ prop: props.detailPanelExpandedRowIds,
443
+ defaultProp: props.defaultDetailPanelExpandedRowIds ?? EMPTY_ROW_ID_SET,
444
+ onChange: (ids) => props.onDetailPanelExpandedRowIdsChange?.(new Set(ids)),
445
+ });
446
+ const resolvedDetailIds = detailPanelExpandedRowIds ?? EMPTY_ROW_ID_SET;
447
+ const toggleDetailPanel = (id: GridRowId) => {
448
+ const next = new Set(resolvedDetailIds);
449
+ if (next.has(id)) {
450
+ next.delete(id);
451
+ } else {
452
+ next.add(id);
453
+ }
454
+ setDetailPanelExpandedRowIds(next);
455
+ };
456
+
233
457
  const [sortModel, setSortModel] = useControllableState({
234
458
  prop: props.sortModel,
235
459
  defaultProp: props.defaultSortModel ?? [],
@@ -245,6 +469,11 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
245
469
  defaultProp: props.defaultFilterModel ?? [],
246
470
  onChange: props.onFilterModelChange,
247
471
  });
472
+ const [filterLogicOperator, setFilterLogicOperator] = useControllableState({
473
+ prop: props.filterLogicOperator,
474
+ defaultProp: props.defaultFilterLogicOperator ?? 'and',
475
+ onChange: props.onFilterLogicOperatorChange,
476
+ });
248
477
  const [quickFilter, setQuickFilter] = useControllableState({
249
478
  prop: props.quickFilter,
250
479
  defaultProp: props.defaultQuickFilter ?? '',
@@ -259,17 +488,34 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
259
488
  const resolvedSortModel = sortModel ?? [];
260
489
  const resolvedPagination = paginationModel ?? DEFAULT_PAGINATION_MODEL;
261
490
  const resolvedFilterModel = filterModel ?? [];
491
+ const resolvedFilterLogicOperator = filterLogicOperator ?? 'and';
262
492
  const resolvedQuickFilter = quickFilter ?? '';
263
493
  const resolvedSelection = rowSelectionModel ?? EMPTY_SELECTION;
264
494
 
495
+ const knownFields = useMemo(() => new Set(columns.map((column) => column.field)), [columns]);
496
+ const columnPinning = useMemo(
497
+ () => normalizePinnedColumns(pinnedColumns, knownFields, selectColumn, detailPanel),
498
+ [pinnedColumns, knownFields, selectColumn, detailPanel],
499
+ );
500
+ const pinningActive = isColumnPinningActive(columnPinning);
501
+ const pinnedFields = useMemo(() => pinnedFieldSet(columnPinning), [columnPinning]);
502
+
265
503
  const {
266
504
  table,
267
505
  setQuickFilter: setQuickFilterWithPageReset,
268
506
  setColumnFilter,
507
+ setFilterModel: setFilterModelWithPageReset,
508
+ tree,
269
509
  } = useDataGridTable({
270
510
  rows,
271
- columns,
511
+ columns: gridColumns,
272
512
  getRowId,
513
+ detailPanel,
514
+ treeData: treeActive,
515
+ getTreeDataPath,
516
+ defaultGroupingExpansionDepth,
517
+ isGroupExpandedByDefault,
518
+ rowSelectionPropagation,
273
519
  checkboxSelection,
274
520
  disableColumnSorting,
275
521
  paginationMode,
@@ -282,48 +528,166 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
282
528
  onPaginationModelChange: (model) => setPaginationModel(model),
283
529
  filterModel: resolvedFilterModel,
284
530
  onFilterModelChange: (model) => setFilterModel(model),
531
+ filterLogicOperator: resolvedFilterLogicOperator,
285
532
  quickFilter: resolvedQuickFilter,
286
533
  onQuickFilterChange: (value) => setQuickFilter(value),
287
534
  rowSelectionModel: resolvedSelection,
288
535
  onRowSelectionModelChange: (model) => setRowSelectionModel(model),
536
+ columnPinning,
537
+ columnVisibility: columnVisibilityModel,
289
538
  });
290
539
 
291
540
  const colByField = useMemo(() => {
292
541
  const map = new Map<string, GridColDef<R>>();
293
- for (const col of columns) {
542
+ for (const col of gridColumns) {
294
543
  map.set(col.field, col);
295
544
  }
296
545
  return map;
297
- }, [columns]);
546
+ }, [gridColumns]);
547
+
548
+ warnPinnedFlexColumns(visibleColumns, pinnedFields);
549
+
298
550
  const columnWidths = useMemo(
299
- () => webColumnWidths(columns, checkboxSelection),
300
- [checkboxSelection, columns],
551
+ () => webColumnWidths(visibleColumns, leadingWidth, pinnedFields),
552
+ [leadingWidth, visibleColumns, pinnedFields],
553
+ );
554
+ const hasFlexColumns = useMemo(
555
+ () => visibleColumns.some((column) => effectiveFlex(column, pinnedFields) > 0),
556
+ [visibleColumns, pinnedFields],
301
557
  );
302
- const hasFlexColumns = useMemo(() => columns.some((column) => (column.flex ?? 0) > 0), [columns]);
558
+ const pinnedOffsets = useMemo(
559
+ () => (pinningActive ? buildPinnedOffsets(columnPinning, colByField) : null),
560
+ [pinningActive, columnPinning, colByField],
561
+ );
562
+ const orderedLeafColumnIds = useMemo(() => {
563
+ if (!pinningActive) {
564
+ return undefined;
565
+ }
566
+ return [
567
+ ...table.getStartVisibleLeafColumns().map((column) => column.id),
568
+ ...table.getCenterVisibleLeafColumns().map((column) => column.id),
569
+ ...table.getEndVisibleLeafColumns().map((column) => column.id),
570
+ ];
571
+ }, [pinningActive, table]);
572
+ const isEmpty = !loading && rows.length === 0;
573
+ const mainScrollStyle = useMemo((): ViewStyle | undefined => {
574
+ if (!pinningActive || pinnedOffsets == null) {
575
+ return undefined;
576
+ }
577
+ const guides =
578
+ Platform.OS === 'web' && !isEmpty
579
+ ? pinnedEdgeGuidesImage(pinnedOffsets, PINNED_EDGE_COLOR, I18nManager.isRTL)
580
+ : undefined;
581
+ // Logical scroll-padding keeps focused cells out from under pinned edges (web).
582
+ return {
583
+ scrollPaddingInlineStart: pinnedOffsets.startTotal,
584
+ scrollPaddingInlineEnd: pinnedOffsets.endTotal,
585
+ ...(guides ? { backgroundImage: guides, backgroundRepeat: 'no-repeat' } : {}),
586
+ } as ViewStyle;
587
+ }, [pinningActive, pinnedOffsets, isEmpty]);
303
588
 
304
589
  const visibleRows = table.getRowModel().rows;
305
590
  const pageCount = Math.max(table.getPageCount(), 1);
306
591
  const selectedCount = table.getFilteredSelectedRowModel().rows.length;
307
592
  const filterableCount = table.getFilteredRowModel().rows.length;
308
593
  const headerSelectState =
309
- selectedCount === 0
594
+ tree?.headerSelection ??
595
+ (selectedCount === 0
310
596
  ? { checked: false, indeterminate: false }
311
597
  : selectedCount === filterableCount
312
598
  ? { checked: true, indeterminate: false }
313
- : { checked: false, indeterminate: true };
599
+ : { checked: false, indeterminate: true });
600
+ const leafColumnCount = table.getVisibleLeafColumns().length;
314
601
 
315
602
  const accessibleName = ariaLabel ?? label;
316
- const isEmpty = !loading && rows.length === 0;
317
603
  const totalCount = paginationMode === 'server' ? (rowCount ?? rows.length) : filterableCount;
318
604
 
319
- const filterValueFor = (field: string) =>
320
- resolvedFilterModel.find((item) => item.field === field)?.value ?? '';
605
+ const filterValueFor = (field: string) => {
606
+ const value = resolvedFilterModel.find((item) => item.field === field)?.value;
607
+ return typeof value === 'string' ? value : '';
608
+ };
321
609
 
322
610
  const showToolbarBar = showToolbar || toolbarActions != null;
611
+ const filterPanelColumns = useMemo(
612
+ () => gridColumns.filter((column) => column.filterable !== false),
613
+ [gridColumns],
614
+ );
615
+ // Header filter cells and the column menu open the panel through `filterPanel.openForField`.
616
+ const filterPanel = useDataGridFilterPanel({
617
+ columns: filterPanelColumns,
618
+ filterModel: resolvedFilterModel,
619
+ onFilterModelChange: setFilterModel,
620
+ });
621
+ const filterPanelOpen = filterPanel.opener !== null && filterPanelColumns.length > 0;
622
+ const filterPanelAnchored = filterPanel.opener?.field !== undefined;
623
+ const filterPanelButtonRef = useRef<View>(null);
624
+ const showFilterPanelButton = showToolbar && filterPanelColumns.length > 0;
625
+
626
+ const mainRef = useRef<View>(null);
627
+ const [viewportWidth, setViewportWidth] = useState<number | undefined>(undefined);
628
+ useEffect(() => {
629
+ const main = mainRef.current as unknown as HTMLElement | null;
630
+ if (
631
+ Platform.OS !== 'web' ||
632
+ !detailPanel ||
633
+ main === null ||
634
+ typeof ResizeObserver === 'undefined'
635
+ ) {
636
+ return undefined;
637
+ }
638
+ const observer = new ResizeObserver(() => setViewportWidth(main.clientWidth));
639
+ observer.observe(main);
640
+ return () => observer.disconnect();
641
+ }, [detailPanel]);
642
+ const detailPanelContentStyle = useMemo(
643
+ (): ViewStyle | undefined =>
644
+ viewportWidth === undefined
645
+ ? undefined
646
+ : ({
647
+ position: 'sticky',
648
+ insetInlineStart: DETAIL_PANEL_INSET,
649
+ width: viewportWidth - DETAIL_PANEL_INSET * 2,
650
+ } as unknown as ViewStyle),
651
+ [viewportWidth],
652
+ );
653
+
654
+ const rootRef = useRef<View>(null);
655
+ const searchRef = useRef<TextInput>(null);
656
+ const bodyFocusRef = useRef<Element | null>(null);
657
+ useEffect(() => {
658
+ if (Platform.OS !== 'web') {
659
+ return undefined;
660
+ }
661
+ const root = rootRef.current as unknown as HTMLElement | null;
662
+ const onFocusIn = (event: FocusEvent) => {
663
+ const target = event.target instanceof Element ? event.target : null;
664
+ bodyFocusRef.current =
665
+ target !== null && root?.contains(target.closest('tbody')) === true ? target : null;
666
+ };
667
+ document.addEventListener('focusin', onFocusIn);
668
+ return () => document.removeEventListener('focusin', onFocusIn);
669
+ }, []);
670
+ // Only when the focused row control was removed: focus that left the grid stays where it went.
671
+ useEffect(() => {
672
+ const focused = bodyFocusRef.current;
673
+ if (Platform.OS !== 'web' || focused === null || focused.isConnected) {
674
+ return;
675
+ }
676
+ bodyFocusRef.current = null;
677
+ const active = document.activeElement;
678
+ if (active === null || active === document.body) {
679
+ searchRef.current?.focus();
680
+ }
681
+ }, [visibleRows]);
323
682
 
324
683
  return (
325
684
  <View
326
- className={cn(dataGridRootVariants({ autoHeight }), className)}
685
+ ref={rootRef}
686
+ className={cn(
687
+ dataGridRootVariants({ autoHeight }),
688
+ className,
689
+ filterPanelOpen && !filterPanelAnchored && 'overflow-visible',
690
+ )}
327
691
  style={style}
328
692
  {...mergeDataAttributes({}, dataAttributes({ slot: 'data-grid' }))}
329
693
  >
@@ -333,6 +697,7 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
333
697
  <Input size="small" className={dataGridToolbarSearchVariants()}>
334
698
  <Input.Icon as={Search} />
335
699
  <Input.Field
700
+ ref={searchRef}
336
701
  value={resolvedQuickFilter}
337
702
  onChangeText={setQuickFilterWithPageReset}
338
703
  placeholder="Search"
@@ -340,17 +705,47 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
340
705
  />
341
706
  </Input>
342
707
  ) : null}
708
+ {showFilterPanelButton ? (
709
+ <IconButton
710
+ ref={filterPanelButtonRef}
711
+ as={FilterList}
712
+ variant="ghost"
713
+ color="neutral"
714
+ size="small"
715
+ accessibilityLabel="Filters"
716
+ accessibilityState={{ expanded: filterPanelOpen }}
717
+ onPress={() => filterPanel.toggle(filterPanelButtonRef)}
718
+ />
719
+ ) : null}
343
720
  {toolbarActions ? (
344
721
  <View className={dataGridToolbarActionsVariants()}>{toolbarActions}</View>
345
722
  ) : null}
346
723
  </View>
347
724
  ) : null}
725
+ {filterPanelOpen && filterPanel.opener ? (
726
+ <DataGridFilterPanel
727
+ columns={filterPanelColumns}
728
+ filterModel={resolvedFilterModel}
729
+ onFilterModelChange={setFilterModelWithPageReset}
730
+ logicOperator={resolvedFilterLogicOperator}
731
+ onLogicOperatorChange={(operator) => {
732
+ setFilterLogicOperator(operator);
733
+ if (resolvedPagination.page !== 0) {
734
+ setPaginationModel({ ...resolvedPagination, page: 0 });
735
+ }
736
+ }}
737
+ onClose={filterPanel.close}
738
+ opener={filterPanel.opener}
739
+ />
740
+ ) : null}
348
741
 
349
742
  <View
743
+ ref={mainRef}
350
744
  className={dataGridMainVariants({
351
745
  autoHeight,
352
746
  overlay: isEmpty || (loading && loadingOverlay != null),
353
747
  })}
748
+ style={mainScrollStyle}
354
749
  >
355
750
  <Table
356
751
  className={cn(
@@ -370,28 +765,52 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
370
765
  {label ? <Table.Caption>{label}</Table.Caption> : null}
371
766
  {Platform.OS === 'web' && hasFlexColumns ? (
372
767
  <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
- ))}
768
+ {(
769
+ orderedLeafColumnIds ?? [
770
+ ...leadingColumnIds,
771
+ ...visibleColumns.map((column) => column.field),
772
+ ]
773
+ ).map((columnId) =>
774
+ columnId === SELECT_COLUMN_ID ? (
775
+ <col key={columnId} style={{ width: SELECT_COLUMN_WIDTH }} />
776
+ ) : columnId === DETAIL_PANEL_TOGGLE_ID ? (
777
+ <col key={columnId} style={{ width: DETAIL_PANEL_TOGGLE_WIDTH }} />
778
+ ) : (
779
+ <col
780
+ key={columnId}
781
+ style={{
782
+ width: columnWidths.get(columnId),
783
+ minWidth: colByField.get(columnId)?.minWidth,
784
+ }}
785
+ />
786
+ ),
787
+ )}
383
788
  </colgroup>
384
789
  ) : null}
385
790
  <Table.Header className={dataGridHeaderVariants()}>
386
- {table.getHeaderGroups().map((headerGroup) => (
791
+ {(pinningActive
792
+ ? [
793
+ {
794
+ id: 'pinned',
795
+ headers: [
796
+ ...table.getStartLeafHeaders(),
797
+ ...table.getCenterLeafHeaders(),
798
+ ...table.getEndLeafHeaders(),
799
+ ],
800
+ },
801
+ ]
802
+ : table.getHeaderGroups()
803
+ ).map((headerGroup) => (
387
804
  <Table.Row key={headerGroup.id}>
388
805
  {headerGroup.headers.map((header) => {
806
+ const pin = pinMetaFor(header.column.id, columnPinning);
389
807
  if (header.id === SELECT_COLUMN_ID) {
390
808
  return (
391
809
  <Table.Head
392
810
  key={header.id}
393
811
  align="center"
394
- className={dataGridSelectHeadVariants()}
812
+ className={cn(dataGridSelectHeadVariants(), pinnedClassName(pin, true))}
813
+ style={mergePinnedStyle(undefined, pin, pinnedOffsets, header.id, true)}
395
814
  >
396
815
  <Checkbox
397
816
  value="select-all"
@@ -404,11 +823,25 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
404
823
  </Table.Head>
405
824
  );
406
825
  }
826
+ if (header.id === DETAIL_PANEL_TOGGLE_ID) {
827
+ return (
828
+ <Table.Cell
829
+ key={header.id}
830
+ className={cn(
831
+ dataGridHeadVariants(),
832
+ dataGridDetailToggleCellVariants(),
833
+ pinnedClassName(pin, true),
834
+ )}
835
+ style={mergePinnedStyle(undefined, pin, pinnedOffsets, header.id, true)}
836
+ />
837
+ );
838
+ }
407
839
 
408
840
  const col = colByField.get(header.column.id);
841
+ const isTreeColumn = header.column.id === treeField;
409
842
  const sorted = header.column.getIsSorted();
410
843
  const canSort = !disableColumnSorting && header.column.getCanSort();
411
- const headerAlign = normalizeGridAlignment(col?.headerAlign ?? col?.align);
844
+ const headerAlign = resolveColumnHeaderAlign(col);
412
845
  const headerName = col?.headerName ?? header.column.id;
413
846
  const SortIcon = sorted === 'asc' ? KeyboardArrowUp : KeyboardArrowDown;
414
847
  const sortButton = canSort ? (
@@ -428,13 +861,28 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
428
861
  })}
429
862
  />
430
863
  ) : null;
864
+ const headerLabel = (
865
+ <>
866
+ {headerAlign === 'end' ? sortButton : null}
867
+ <Text numberOfLines={1} className={dataGridHeaderLabelVariants()}>
868
+ {col?.renderHeader ? col.renderHeader() : headerName}
869
+ </Text>
870
+ {headerAlign === 'end' ? null : sortButton}
871
+ </>
872
+ );
431
873
 
432
874
  return (
433
875
  <Table.Head
434
876
  key={header.id}
435
877
  align={headerAlign}
436
- className={dataGridHeadVariants()}
437
- style={cellStyle(col, columnWidths.get(header.column.id))}
878
+ className={cn(dataGridHeadVariants(), pinnedClassName(pin, true))}
879
+ style={mergePinnedStyle(
880
+ cellStyle(col, columnWidths.get(header.column.id)),
881
+ pin,
882
+ pinnedOffsets,
883
+ header.column.id,
884
+ true,
885
+ )}
438
886
  {...(Platform.OS === 'web'
439
887
  ? {
440
888
  title: col?.renderHeader ? undefined : headerName,
@@ -451,18 +899,39 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
451
899
  }
452
900
  : {})}
453
901
  >
454
- <View
455
- className={dataGridHeaderCellInnerVariants({
456
- align: headerAlign,
457
- fitToColumn: Platform.OS === 'web' && columnWidths.has(header.column.id),
458
- })}
459
- >
460
- {headerAlign === 'end' ? sortButton : null}
461
- <Text numberOfLines={1} className={dataGridHeaderLabelVariants()}>
462
- {col?.renderHeader ? col.renderHeader() : headerName}
463
- </Text>
464
- {headerAlign === 'end' ? null : sortButton}
465
- </View>
902
+ {isTreeColumn ? (
903
+ <View className={dataGridTreeCellInnerVariants()}>
904
+ <View className={dataGridTreeToggleSlotVariants()} />
905
+ {treeCheckbox ? (
906
+ <Checkbox
907
+ value="select-all"
908
+ isChecked={headerSelectState.checked}
909
+ isIndeterminate={headerSelectState.indeterminate}
910
+ onChange={() => tree?.toggleAllRowsSelected()}
911
+ accessibilityLabel="Select all"
912
+ aria-label="Select all"
913
+ />
914
+ ) : null}
915
+ <View
916
+ className={cn(
917
+ dataGridHeaderCellInnerVariants({ align: headerAlign }),
918
+ 'flex-1',
919
+ )}
920
+ >
921
+ {headerLabel}
922
+ </View>
923
+ </View>
924
+ ) : (
925
+ <View
926
+ className={dataGridHeaderCellInnerVariants({
927
+ align: headerAlign,
928
+ fitToColumn:
929
+ Platform.OS === 'web' && columnWidths.has(header.column.id),
930
+ })}
931
+ >
932
+ {headerLabel}
933
+ </View>
934
+ )}
466
935
  </Table.Head>
467
936
  );
468
937
  })}
@@ -470,34 +939,58 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
470
939
  ))}
471
940
  {headerFilters ? (
472
941
  <Table.Row>
473
- {table.getAllLeafColumns().map((column) => {
474
- if (column.id === SELECT_COLUMN_ID) {
942
+ {(pinningActive
943
+ ? [
944
+ ...table.getStartVisibleLeafColumns(),
945
+ ...table.getCenterVisibleLeafColumns(),
946
+ ...table.getEndVisibleLeafColumns(),
947
+ ]
948
+ : table.getVisibleLeafColumns()
949
+ ).map((column) => {
950
+ const pin = pinMetaFor(column.id, columnPinning);
951
+ if (column.id === SELECT_COLUMN_ID || column.id === DETAIL_PANEL_TOGGLE_ID) {
475
952
  return (
476
953
  <Table.Cell
477
954
  key={column.id}
478
955
  align="center"
479
- className={dataGridSelectHeadVariants()}
956
+ className={cn(
957
+ dataGridSelectHeadVariants(),
958
+ column.id === DETAIL_PANEL_TOGGLE_ID && 'w-12 px-2',
959
+ pinnedClassName(pin, true),
960
+ )}
961
+ style={mergePinnedStyle(undefined, pin, pinnedOffsets, column.id, true)}
480
962
  />
481
963
  );
482
964
  }
483
965
  const col = colByField.get(column.id);
484
- const headerAlign = col?.headerAlign ?? col?.align;
485
966
  if (col?.filterable === false) {
486
967
  return (
487
968
  <Table.Head
488
969
  key={column.id}
489
- align={normalizeGridAlignment(headerAlign)}
490
- className={dataGridHeadVariants()}
491
- style={cellStyle(col, columnWidths.get(column.id))}
970
+ align={resolveColumnHeaderAlign(col)}
971
+ className={cn(dataGridHeadVariants(), pinnedClassName(pin, true))}
972
+ style={mergePinnedStyle(
973
+ cellStyle(col, columnWidths.get(column.id)),
974
+ pin,
975
+ pinnedOffsets,
976
+ column.id,
977
+ true,
978
+ )}
492
979
  />
493
980
  );
494
981
  }
495
982
  return (
496
983
  <Table.Head
497
984
  key={column.id}
498
- align={normalizeGridAlignment(headerAlign)}
499
- className={dataGridHeadVariants()}
500
- style={cellStyle(col, columnWidths.get(column.id))}
985
+ align={resolveColumnHeaderAlign(col)}
986
+ className={cn(dataGridHeadVariants(), pinnedClassName(pin, true))}
987
+ style={mergePinnedStyle(
988
+ cellStyle(col, columnWidths.get(column.id)),
989
+ pin,
990
+ pinnedOffsets,
991
+ column.id,
992
+ true,
993
+ )}
501
994
  >
502
995
  <Input size="small" className={dataGridFilterInputVariants()}>
503
996
  <Input.Field
@@ -516,23 +1009,86 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
516
1009
  <Table.Body className={dataGridBodyVariants()}>
517
1010
  {loading && loadingOverlay == null ? (
518
1011
  <DefaultLoadingRows
519
- columns={columns}
520
- checkboxSelection={checkboxSelection}
1012
+ columns={visibleColumns}
1013
+ leadingColumnIds={leadingColumnIds}
521
1014
  rowCount={Math.min(resolvedPagination.pageSize, MAX_SKELETON_ROW_COUNT)}
522
1015
  columnWidths={columnWidths}
1016
+ orderedColumnIds={orderedLeafColumnIds}
1017
+ pinning={columnPinning}
1018
+ offsets={pinnedOffsets}
523
1019
  />
524
1020
  ) : isEmpty ? null : (
525
- visibleRows.map((row) => {
1021
+ visibleRows.flatMap((row) => {
526
1022
  const rowId = resolveRowId(row.original, getRowId);
527
- return (
528
- <Table.Row key={row.id}>
529
- {row.getAllCells().map((cell) => {
1023
+ const cells = pinningActive
1024
+ ? [
1025
+ ...row.getStartVisibleCells(),
1026
+ ...row.getCenterVisibleCells(),
1027
+ ...row.getEndVisibleCells(),
1028
+ ]
1029
+ : row.getVisibleCells();
1030
+ const detailContent = getDetailPanelContent?.({ id: rowId, row: row.original });
1031
+ const hasDetailPanel = detailContent != null;
1032
+ const detailOpen = hasDetailPanel && resolvedDetailIds.has(rowId);
1033
+ const treeLabel = treeActive
1034
+ ? [...getTreeDataPath(row.original)].reverse().join(', ')
1035
+ : '';
1036
+ const dataRow = (
1037
+ <Table.Row
1038
+ key={row.id}
1039
+ className={detailOpen ? 'web:[&>*]:border-b-0' : undefined}
1040
+ >
1041
+ {cells.map((cell) => {
1042
+ const pin = pinMetaFor(cell.column.id, columnPinning);
1043
+ if (cell.column.id === DETAIL_PANEL_TOGGLE_ID) {
1044
+ return (
1045
+ <Table.Cell
1046
+ key={cell.id}
1047
+ align="center"
1048
+ className={cn(
1049
+ dataGridDetailToggleCellVariants(),
1050
+ pinnedClassName(pin, false),
1051
+ )}
1052
+ style={mergePinnedStyle(
1053
+ undefined,
1054
+ pin,
1055
+ pinnedOffsets,
1056
+ cell.column.id,
1057
+ false,
1058
+ )}
1059
+ >
1060
+ {hasDetailPanel ? (
1061
+ <IconButton
1062
+ as={detailOpen ? KeyboardArrowDown : KeyboardArrowRight}
1063
+ variant="ghost"
1064
+ color="neutral"
1065
+ size="small"
1066
+ iconClassName={detailOpen ? undefined : 'rtl:-scale-x-100'}
1067
+ accessibilityLabel={`${detailOpen ? 'Collapse' : 'Expand'} row ${String(rowId)}`}
1068
+ accessibilityState={{ expanded: detailOpen }}
1069
+ aria-expanded={detailOpen}
1070
+ onPress={() => toggleDetailPanel(rowId)}
1071
+ />
1072
+ ) : null}
1073
+ </Table.Cell>
1074
+ );
1075
+ }
530
1076
  if (cell.column.id === SELECT_COLUMN_ID) {
531
1077
  return (
532
1078
  <Table.Cell
533
1079
  key={cell.id}
534
1080
  align="center"
535
- className={dataGridSelectCellVariants()}
1081
+ className={cn(
1082
+ dataGridSelectCellVariants(),
1083
+ pinnedClassName(pin, false),
1084
+ )}
1085
+ style={mergePinnedStyle(
1086
+ undefined,
1087
+ pin,
1088
+ pinnedOffsets,
1089
+ cell.column.id,
1090
+ false,
1091
+ )}
536
1092
  >
537
1093
  <Checkbox
538
1094
  value={String(rowId)}
@@ -554,26 +1110,120 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
554
1110
  row: row.original,
555
1111
  field: col.field,
556
1112
  id: rowId,
1113
+ col,
557
1114
  });
558
1115
  } else if (col?.valueFormatter) {
559
1116
  content = col.valueFormatter(value, row.original, col);
1117
+ } else if (col?.type === 'boolean' && typeof value === 'boolean') {
1118
+ content = <BooleanCell value={value} />;
560
1119
  } else {
561
- content = defaultCellContent(value);
1120
+ const typedText = getTypedCellText(col, value, row.original);
1121
+ content = typedText !== undefined ? typedText : defaultCellContent(value);
562
1122
  }
563
1123
 
1124
+ if (tree && cell.column.id === treeField) {
1125
+ const canExpand = row.subRows.length > 0;
1126
+ const expanded = canExpand && tree.isRowExpanded(row.id);
1127
+ const status = tree.selectionStatus(row.id);
1128
+ content = (
1129
+ <View
1130
+ className={dataGridTreeCellInnerVariants()}
1131
+ style={{ paddingStart: row.depth * TREE_INDENT }}
1132
+ >
1133
+ <View className={dataGridTreeToggleSlotVariants()}>
1134
+ {canExpand ? (
1135
+ <IconButton
1136
+ as={expanded ? KeyboardArrowDown : KeyboardArrowRight}
1137
+ variant="ghost"
1138
+ color="neutral"
1139
+ size="small"
1140
+ iconClassName={expanded ? undefined : 'rtl:-scale-x-100'}
1141
+ accessibilityLabel={`${expanded ? 'Collapse' : 'Expand'} ${treeLabel}`}
1142
+ accessibilityState={{ expanded }}
1143
+ aria-expanded={expanded}
1144
+ onPress={() => tree.toggleRowExpanded(row.id)}
1145
+ />
1146
+ ) : null}
1147
+ </View>
1148
+ {treeCheckbox ? (
1149
+ <Checkbox
1150
+ value={String(rowId)}
1151
+ isChecked={status === 'all'}
1152
+ isIndeterminate={status === 'some'}
1153
+ onChange={() => tree.toggleRowSelected(row)}
1154
+ accessibilityLabel={`Select ${treeLabel}`}
1155
+ aria-label={`Select ${treeLabel}`}
1156
+ />
1157
+ ) : null}
1158
+ <View className={dataGridTreeContentVariants()}>
1159
+ {typeof content === 'string' || typeof content === 'number' ? (
1160
+ <Text numberOfLines={1}>{content}</Text>
1161
+ ) : (
1162
+ content
1163
+ )}
1164
+ </View>
1165
+ </View>
1166
+ );
1167
+ }
1168
+
1169
+ const align = resolveColumnAlign(col);
1170
+ const cellText =
1171
+ !col?.renderCell &&
1172
+ (typeof content === 'string' || typeof content === 'number')
1173
+ ? String(content)
1174
+ : undefined;
1175
+
564
1176
  return (
565
1177
  <Table.Cell
566
1178
  key={cell.id}
567
- align={normalizeGridAlignment(col?.align)}
568
- className={dataGridCellVariants()}
569
- style={cellStyle(col, columnWidths.get(cell.column.id))}
1179
+ align={align}
1180
+ {...(Platform.OS === 'web' && cellText ? { title: cellText } : {})}
1181
+ className={cn(dataGridCellVariants(), pinnedClassName(pin, false))}
1182
+ style={mergePinnedStyle(
1183
+ cellStyle(col, columnWidths.get(cell.column.id)),
1184
+ pin,
1185
+ pinnedOffsets,
1186
+ cell.column.id,
1187
+ false,
1188
+ )}
570
1189
  >
571
- {content}
1190
+ {cellText !== undefined ? (
1191
+ <Text
1192
+ numberOfLines={1}
1193
+ className={dataGridCellTextVariants({
1194
+ align,
1195
+ fitToColumn:
1196
+ Platform.OS === 'web' && columnWidths.has(cell.column.id),
1197
+ })}
1198
+ >
1199
+ {cellText}
1200
+ </Text>
1201
+ ) : (
1202
+ content
1203
+ )}
572
1204
  </Table.Cell>
573
1205
  );
574
1206
  })}
575
1207
  </Table.Row>
576
1208
  );
1209
+ if (!detailOpen) {
1210
+ return [dataRow];
1211
+ }
1212
+ return [
1213
+ dataRow,
1214
+ <Table.Row
1215
+ key={`${row.id}:detail`}
1216
+ className="web:hover:bg-transparent"
1217
+ testID={`data-grid-detail-panel-${String(rowId)}`}
1218
+ >
1219
+ <Table.Cell
1220
+ colSpan={leafColumnCount}
1221
+ className={dataGridDetailPanelCellVariants()}
1222
+ >
1223
+ <View style={detailPanelContentStyle}>{detailContent}</View>
1224
+ </Table.Cell>
1225
+ </Table.Row>,
1226
+ ];
577
1227
  })
578
1228
  )}
579
1229
  </Table.Body>