@cdx-ui/data-grid 0.3.12 → 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 (67) hide show
  1. package/coverage.svg +1 -1
  2. package/lib/commonjs/DataGrid.js +279 -40
  3. package/lib/commonjs/DataGrid.js.map +1 -1
  4. package/lib/commonjs/DataGridFilterPanel.js +136 -9
  5. package/lib/commonjs/DataGridFilterPanel.js.map +1 -1
  6. package/lib/commonjs/adapter/columns.js +16 -8
  7. package/lib/commonjs/adapter/columns.js.map +1 -1
  8. package/lib/commonjs/adapter/pinning.js +5 -1
  9. package/lib/commonjs/adapter/pinning.js.map +1 -1
  10. package/lib/commonjs/adapter/selectColumn.js +11 -0
  11. package/lib/commonjs/adapter/selectColumn.js.map +1 -1
  12. package/lib/commonjs/adapter/treeData.js +195 -0
  13. package/lib/commonjs/adapter/treeData.js.map +1 -0
  14. package/lib/commonjs/styles.js +48 -2
  15. package/lib/commonjs/styles.js.map +1 -1
  16. package/lib/commonjs/types.js +11 -1
  17. package/lib/commonjs/types.js.map +1 -1
  18. package/lib/commonjs/useDataGridTable.js +143 -12
  19. package/lib/commonjs/useDataGridTable.js.map +1 -1
  20. package/lib/module/DataGrid.js +285 -46
  21. package/lib/module/DataGrid.js.map +1 -1
  22. package/lib/module/DataGridFilterPanel.js +139 -13
  23. package/lib/module/DataGridFilterPanel.js.map +1 -1
  24. package/lib/module/adapter/columns.js +17 -9
  25. package/lib/module/adapter/columns.js.map +1 -1
  26. package/lib/module/adapter/pinning.js +5 -1
  27. package/lib/module/adapter/pinning.js.map +1 -1
  28. package/lib/module/adapter/selectColumn.js +11 -1
  29. package/lib/module/adapter/selectColumn.js.map +1 -1
  30. package/lib/module/adapter/treeData.js +187 -0
  31. package/lib/module/adapter/treeData.js.map +1 -0
  32. package/lib/module/styles.js +47 -1
  33. package/lib/module/styles.js.map +1 -1
  34. package/lib/module/types.js +10 -0
  35. package/lib/module/types.js.map +1 -1
  36. package/lib/module/useDataGridTable.js +147 -16
  37. package/lib/module/useDataGridTable.js.map +1 -1
  38. package/lib/typescript/DataGrid.d.ts.map +1 -1
  39. package/lib/typescript/DataGridFilterPanel.d.ts +39 -4
  40. package/lib/typescript/DataGridFilterPanel.d.ts.map +1 -1
  41. package/lib/typescript/adapter/columns.d.ts +1 -1
  42. package/lib/typescript/adapter/columns.d.ts.map +1 -1
  43. package/lib/typescript/adapter/pinning.d.ts +1 -0
  44. package/lib/typescript/adapter/pinning.d.ts.map +1 -1
  45. package/lib/typescript/adapter/selectColumn.d.ts +8 -0
  46. package/lib/typescript/adapter/selectColumn.d.ts.map +1 -1
  47. package/lib/typescript/adapter/treeData.d.ts +36 -0
  48. package/lib/typescript/adapter/treeData.d.ts.map +1 -0
  49. package/lib/typescript/index.d.ts +1 -1
  50. package/lib/typescript/index.d.ts.map +1 -1
  51. package/lib/typescript/styles.d.ts +12 -1
  52. package/lib/typescript/styles.d.ts.map +1 -1
  53. package/lib/typescript/types.d.ts +68 -0
  54. package/lib/typescript/types.d.ts.map +1 -1
  55. package/lib/typescript/useDataGridTable.d.ts +25 -3
  56. package/lib/typescript/useDataGridTable.d.ts.map +1 -1
  57. package/package.json +5 -5
  58. package/src/DataGrid.tsx +399 -59
  59. package/src/DataGridFilterPanel.tsx +188 -17
  60. package/src/adapter/columns.ts +21 -7
  61. package/src/adapter/pinning.ts +10 -1
  62. package/src/adapter/selectColumn.ts +12 -1
  63. package/src/adapter/treeData.ts +228 -0
  64. package/src/index.ts +4 -0
  65. package/src/styles.ts +57 -1
  66. package/src/types.ts +76 -0
  67. package/src/useDataGridTable.ts +217 -15
package/src/DataGrid.tsx CHANGED
@@ -1,5 +1,13 @@
1
- import { useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
2
- import { I18nManager, 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,
5
13
  Icon,
@@ -15,6 +23,7 @@ import {
15
23
  Close,
16
24
  FilterList,
17
25
  KeyboardArrowDown,
26
+ KeyboardArrowRight,
18
27
  KeyboardArrowUp,
19
28
  Search,
20
29
  } from '@cdx-ui/icons';
@@ -27,6 +36,7 @@ import {
27
36
  } from './adapter/columns';
28
37
  import {
29
38
  DEFAULT_PINNED_COLUMN_WIDTH,
39
+ DETAIL_PANEL_TOGGLE_WIDTH,
30
40
  SELECT_COLUMN_WIDTH,
31
41
  buildPinnedOffsets,
32
42
  isColumnPinningActive,
@@ -38,10 +48,13 @@ import {
38
48
  type PinnedOffsetMaps,
39
49
  } from './adapter/pinning';
40
50
  import { getTypedCellText } from './adapter/columnTypes';
41
- import { DataGridFilterPanel } from './DataGridFilterPanel';
51
+ import { DataGridFilterPanel, useDataGridFilterPanel } from './DataGridFilterPanel';
42
52
  import {
43
53
  dataGridBodyVariants,
54
+ dataGridCellTextVariants,
44
55
  dataGridCellVariants,
56
+ dataGridDetailPanelCellVariants,
57
+ dataGridDetailToggleCellVariants,
45
58
  dataGridEmptyOverlayVariants,
46
59
  dataGridFilterInputVariants,
47
60
  dataGridFooterVariants,
@@ -63,19 +76,29 @@ import {
63
76
  dataGridToolbarActionsVariants,
64
77
  dataGridToolbarSearchVariants,
65
78
  dataGridToolbarVariants,
79
+ dataGridTreeCellInnerVariants,
80
+ dataGridTreeContentVariants,
81
+ dataGridTreeToggleSlotVariants,
66
82
  } from './styles';
67
83
  import {
68
84
  DEFAULT_PAGE_SIZE_OPTIONS,
69
85
  DEFAULT_PAGINATION_MODEL,
86
+ DETAIL_PANEL_TOGGLE_ID,
87
+ EMPTY_ROW_ID_SET,
70
88
  EMPTY_SELECTION,
71
89
  SELECT_COLUMN_ID,
72
90
  type DataGridProps,
73
91
  type GridColDef,
92
+ type GridRowId,
74
93
  type GridValidRowModel,
75
94
  } from './types';
76
95
  import { resolveRowId, useDataGridTable } from './useDataGridTable';
77
96
 
78
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;
79
102
  const DEFAULT_WEB_COLUMN_WIDTH = 100;
80
103
  const WEB_TABLE_CONTAINER_STYLE: ViewStyle = { overflow: 'visible' };
81
104
  // Same token as the pinned cells' `border-stroke-secondary` divider.
@@ -124,7 +147,7 @@ function effectiveFlex<R extends GridValidRowModel>(
124
147
 
125
148
  function webColumnWidths<R extends GridValidRowModel>(
126
149
  columns: readonly GridColDef<R>[],
127
- checkboxSelection: boolean,
150
+ leadingWidth: number,
128
151
  pinned: ReadonlySet<string> = new Set(),
129
152
  ): Map<string, CSSProperties['width']> {
130
153
  const totalFlex = columns.reduce((total, column) => total + effectiveFlex(column, pinned), 0);
@@ -145,7 +168,7 @@ function webColumnWidths<R extends GridValidRowModel>(
145
168
  }
146
169
 
147
170
  const fixedWidth =
148
- (checkboxSelection ? SELECT_COLUMN_WIDTH : 0) +
171
+ leadingWidth +
149
172
  columns.reduce((total, column) => {
150
173
  if (effectiveFlex(column, pinned) > 0) {
151
174
  return total;
@@ -248,7 +271,7 @@ function defaultCellContent(value: unknown): ReactNode {
248
271
 
249
272
  function DefaultLoadingRows<R extends GridValidRowModel>({
250
273
  columns,
251
- checkboxSelection,
274
+ leadingColumnIds,
252
275
  rowCount,
253
276
  columnWidths,
254
277
  orderedColumnIds,
@@ -256,7 +279,7 @@ function DefaultLoadingRows<R extends GridValidRowModel>({
256
279
  offsets,
257
280
  }: {
258
281
  columns: readonly GridColDef<R>[];
259
- checkboxSelection: boolean;
282
+ leadingColumnIds: readonly string[];
260
283
  rowCount: number;
261
284
  columnWidths: ReadonlyMap<string, CSSProperties['width']>;
262
285
  orderedColumnIds?: readonly string[];
@@ -264,11 +287,7 @@ function DefaultLoadingRows<R extends GridValidRowModel>({
264
287
  offsets: PinnedOffsetMaps | null;
265
288
  }) {
266
289
  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));
290
+ const ids = orderedColumnIds ?? [...leadingColumnIds, ...columns.map((column) => column.field)];
272
291
 
273
292
  return (
274
293
  <>
@@ -292,6 +311,15 @@ function DefaultLoadingRows<R extends GridValidRowModel>({
292
311
  </Table.Cell>
293
312
  );
294
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
+ }
295
323
  const column = colByField.get(columnId);
296
324
  if (!column) {
297
325
  return null;
@@ -356,6 +384,7 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
356
384
  showToolbar = false,
357
385
  headerFilters = false,
358
386
  pinnedColumns,
387
+ columnVisibilityModel,
359
388
  pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
360
389
  noRowsOverlay,
361
390
  loadingOverlay,
@@ -365,8 +394,66 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
365
394
  label,
366
395
  'aria-label': ariaLabel,
367
396
  'aria-labelledby': ariaLabelledBy,
397
+ treeData = false,
398
+ getTreeDataPath,
399
+ groupingColDef,
400
+ defaultGroupingExpansionDepth,
401
+ isGroupExpandedByDefault,
402
+ rowSelectionPropagation,
403
+ getDetailPanelContent,
368
404
  } = props;
369
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
+
370
457
  const [sortModel, setSortModel] = useControllableState({
371
458
  prop: props.sortModel,
372
459
  defaultProp: props.defaultSortModel ?? [],
@@ -407,8 +494,8 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
407
494
 
408
495
  const knownFields = useMemo(() => new Set(columns.map((column) => column.field)), [columns]);
409
496
  const columnPinning = useMemo(
410
- () => normalizePinnedColumns(pinnedColumns, knownFields, checkboxSelection),
411
- [pinnedColumns, knownFields, checkboxSelection],
497
+ () => normalizePinnedColumns(pinnedColumns, knownFields, selectColumn, detailPanel),
498
+ [pinnedColumns, knownFields, selectColumn, detailPanel],
412
499
  );
413
500
  const pinningActive = isColumnPinningActive(columnPinning);
414
501
  const pinnedFields = useMemo(() => pinnedFieldSet(columnPinning), [columnPinning]);
@@ -418,10 +505,17 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
418
505
  setQuickFilter: setQuickFilterWithPageReset,
419
506
  setColumnFilter,
420
507
  setFilterModel: setFilterModelWithPageReset,
508
+ tree,
421
509
  } = useDataGridTable({
422
510
  rows,
423
- columns,
511
+ columns: gridColumns,
424
512
  getRowId,
513
+ detailPanel,
514
+ treeData: treeActive,
515
+ getTreeDataPath,
516
+ defaultGroupingExpansionDepth,
517
+ isGroupExpandedByDefault,
518
+ rowSelectionPropagation,
425
519
  checkboxSelection,
426
520
  disableColumnSorting,
427
521
  paginationMode,
@@ -440,25 +534,26 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
440
534
  rowSelectionModel: resolvedSelection,
441
535
  onRowSelectionModelChange: (model) => setRowSelectionModel(model),
442
536
  columnPinning,
537
+ columnVisibility: columnVisibilityModel,
443
538
  });
444
539
 
445
540
  const colByField = useMemo(() => {
446
541
  const map = new Map<string, GridColDef<R>>();
447
- for (const col of columns) {
542
+ for (const col of gridColumns) {
448
543
  map.set(col.field, col);
449
544
  }
450
545
  return map;
451
- }, [columns]);
546
+ }, [gridColumns]);
452
547
 
453
- warnPinnedFlexColumns(columns, pinnedFields);
548
+ warnPinnedFlexColumns(visibleColumns, pinnedFields);
454
549
 
455
550
  const columnWidths = useMemo(
456
- () => webColumnWidths(columns, checkboxSelection, pinnedFields),
457
- [checkboxSelection, columns, pinnedFields],
551
+ () => webColumnWidths(visibleColumns, leadingWidth, pinnedFields),
552
+ [leadingWidth, visibleColumns, pinnedFields],
458
553
  );
459
554
  const hasFlexColumns = useMemo(
460
- () => columns.some((column) => effectiveFlex(column, pinnedFields) > 0),
461
- [columns, pinnedFields],
555
+ () => visibleColumns.some((column) => effectiveFlex(column, pinnedFields) > 0),
556
+ [visibleColumns, pinnedFields],
462
557
  );
463
558
  const pinnedOffsets = useMemo(
464
559
  () => (pinningActive ? buildPinnedOffsets(columnPinning, colByField) : null),
@@ -496,11 +591,13 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
496
591
  const selectedCount = table.getFilteredSelectedRowModel().rows.length;
497
592
  const filterableCount = table.getFilteredRowModel().rows.length;
498
593
  const headerSelectState =
499
- selectedCount === 0
594
+ tree?.headerSelection ??
595
+ (selectedCount === 0
500
596
  ? { checked: false, indeterminate: false }
501
597
  : selectedCount === filterableCount
502
598
  ? { checked: true, indeterminate: false }
503
- : { checked: false, indeterminate: true };
599
+ : { checked: false, indeterminate: true });
600
+ const leafColumnCount = table.getVisibleLeafColumns().length;
504
601
 
505
602
  const accessibleName = ariaLabel ?? label;
506
603
  const totalCount = paginationMode === 'server' ? (rowCount ?? rows.length) : filterableCount;
@@ -512,19 +609,84 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
512
609
 
513
610
  const showToolbarBar = showToolbar || toolbarActions != null;
514
611
  const filterPanelColumns = useMemo(
515
- () => columns.filter((column) => column.filterable !== false),
516
- [columns],
612
+ () => gridColumns.filter((column) => column.filterable !== false),
613
+ [gridColumns],
517
614
  );
518
- const [filterPanelOpen, setFilterPanelOpen] = useState(false);
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;
519
623
  const filterPanelButtonRef = useRef<View>(null);
520
624
  const showFilterPanelButton = showToolbar && filterPanelColumns.length > 0;
521
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]);
682
+
522
683
  return (
523
684
  <View
685
+ ref={rootRef}
524
686
  className={cn(
525
687
  dataGridRootVariants({ autoHeight }),
526
688
  className,
527
- filterPanelOpen && 'overflow-visible',
689
+ filterPanelOpen && !filterPanelAnchored && 'overflow-visible',
528
690
  )}
529
691
  style={style}
530
692
  {...mergeDataAttributes({}, dataAttributes({ slot: 'data-grid' }))}
@@ -535,6 +697,7 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
535
697
  <Input size="small" className={dataGridToolbarSearchVariants()}>
536
698
  <Input.Icon as={Search} />
537
699
  <Input.Field
700
+ ref={searchRef}
538
701
  value={resolvedQuickFilter}
539
702
  onChangeText={setQuickFilterWithPageReset}
540
703
  placeholder="Search"
@@ -551,7 +714,7 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
551
714
  size="small"
552
715
  accessibilityLabel="Filters"
553
716
  accessibilityState={{ expanded: filterPanelOpen }}
554
- onPress={() => setFilterPanelOpen((open) => !open)}
717
+ onPress={() => filterPanel.toggle(filterPanelButtonRef)}
555
718
  />
556
719
  ) : null}
557
720
  {toolbarActions ? (
@@ -559,7 +722,7 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
559
722
  ) : null}
560
723
  </View>
561
724
  ) : null}
562
- {showFilterPanelButton && filterPanelOpen ? (
725
+ {filterPanelOpen && filterPanel.opener ? (
563
726
  <DataGridFilterPanel
564
727
  columns={filterPanelColumns}
565
728
  filterModel={resolvedFilterModel}
@@ -571,12 +734,13 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
571
734
  setPaginationModel({ ...resolvedPagination, page: 0 });
572
735
  }
573
736
  }}
574
- onClose={() => setFilterPanelOpen(false)}
575
- triggerRef={filterPanelButtonRef}
737
+ onClose={filterPanel.close}
738
+ opener={filterPanel.opener}
576
739
  />
577
740
  ) : null}
578
741
 
579
742
  <View
743
+ ref={mainRef}
580
744
  className={dataGridMainVariants({
581
745
  autoHeight,
582
746
  overlay: isEmpty || (loading && loadingOverlay != null),
@@ -602,13 +766,15 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
602
766
  {Platform.OS === 'web' && hasFlexColumns ? (
603
767
  <colgroup>
604
768
  {(
605
- orderedLeafColumnIds ??
606
- (checkboxSelection
607
- ? [SELECT_COLUMN_ID, ...columns.map((column) => column.field)]
608
- : columns.map((column) => column.field))
769
+ orderedLeafColumnIds ?? [
770
+ ...leadingColumnIds,
771
+ ...visibleColumns.map((column) => column.field),
772
+ ]
609
773
  ).map((columnId) =>
610
774
  columnId === SELECT_COLUMN_ID ? (
611
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 }} />
612
778
  ) : (
613
779
  <col
614
780
  key={columnId}
@@ -657,8 +823,22 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
657
823
  </Table.Head>
658
824
  );
659
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
+ }
660
839
 
661
840
  const col = colByField.get(header.column.id);
841
+ const isTreeColumn = header.column.id === treeField;
662
842
  const sorted = header.column.getIsSorted();
663
843
  const canSort = !disableColumnSorting && header.column.getCanSort();
664
844
  const headerAlign = resolveColumnHeaderAlign(col);
@@ -681,6 +861,15 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
681
861
  })}
682
862
  />
683
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
+ );
684
873
 
685
874
  return (
686
875
  <Table.Head
@@ -710,18 +899,39 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
710
899
  }
711
900
  : {})}
712
901
  >
713
- <View
714
- className={dataGridHeaderCellInnerVariants({
715
- align: headerAlign,
716
- fitToColumn: Platform.OS === 'web' && columnWidths.has(header.column.id),
717
- })}
718
- >
719
- {headerAlign === 'end' ? sortButton : null}
720
- <Text numberOfLines={1} className={dataGridHeaderLabelVariants()}>
721
- {col?.renderHeader ? col.renderHeader() : headerName}
722
- </Text>
723
- {headerAlign === 'end' ? null : sortButton}
724
- </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
+ )}
725
935
  </Table.Head>
726
936
  );
727
937
  })}
@@ -735,15 +945,19 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
735
945
  ...table.getCenterVisibleLeafColumns(),
736
946
  ...table.getEndVisibleLeafColumns(),
737
947
  ]
738
- : table.getAllLeafColumns()
948
+ : table.getVisibleLeafColumns()
739
949
  ).map((column) => {
740
950
  const pin = pinMetaFor(column.id, columnPinning);
741
- if (column.id === SELECT_COLUMN_ID) {
951
+ if (column.id === SELECT_COLUMN_ID || column.id === DETAIL_PANEL_TOGGLE_ID) {
742
952
  return (
743
953
  <Table.Cell
744
954
  key={column.id}
745
955
  align="center"
746
- className={cn(dataGridSelectHeadVariants(), pinnedClassName(pin, true))}
956
+ className={cn(
957
+ dataGridSelectHeadVariants(),
958
+ column.id === DETAIL_PANEL_TOGGLE_ID && 'w-12 px-2',
959
+ pinnedClassName(pin, true),
960
+ )}
747
961
  style={mergePinnedStyle(undefined, pin, pinnedOffsets, column.id, true)}
748
962
  />
749
963
  );
@@ -795,8 +1009,8 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
795
1009
  <Table.Body className={dataGridBodyVariants()}>
796
1010
  {loading && loadingOverlay == null ? (
797
1011
  <DefaultLoadingRows
798
- columns={columns}
799
- checkboxSelection={checkboxSelection}
1012
+ columns={visibleColumns}
1013
+ leadingColumnIds={leadingColumnIds}
800
1014
  rowCount={Math.min(resolvedPagination.pageSize, MAX_SKELETON_ROW_COUNT)}
801
1015
  columnWidths={columnWidths}
802
1016
  orderedColumnIds={orderedLeafColumnIds}
@@ -804,7 +1018,7 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
804
1018
  offsets={pinnedOffsets}
805
1019
  />
806
1020
  ) : isEmpty ? null : (
807
- visibleRows.map((row) => {
1021
+ visibleRows.flatMap((row) => {
808
1022
  const rowId = resolveRowId(row.original, getRowId);
809
1023
  const cells = pinningActive
810
1024
  ? [
@@ -812,11 +1026,53 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
812
1026
  ...row.getCenterVisibleCells(),
813
1027
  ...row.getEndVisibleCells(),
814
1028
  ]
815
- : row.getAllCells();
816
- return (
817
- <Table.Row key={row.id}>
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
+ >
818
1041
  {cells.map((cell) => {
819
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
+ }
820
1076
  if (cell.column.id === SELECT_COLUMN_ID) {
821
1077
  return (
822
1078
  <Table.Cell
@@ -865,10 +1121,63 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
865
1121
  content = typedText !== undefined ? typedText : defaultCellContent(value);
866
1122
  }
867
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
+
868
1176
  return (
869
1177
  <Table.Cell
870
1178
  key={cell.id}
871
- align={resolveColumnAlign(col)}
1179
+ align={align}
1180
+ {...(Platform.OS === 'web' && cellText ? { title: cellText } : {})}
872
1181
  className={cn(dataGridCellVariants(), pinnedClassName(pin, false))}
873
1182
  style={mergePinnedStyle(
874
1183
  cellStyle(col, columnWidths.get(cell.column.id)),
@@ -878,12 +1187,43 @@ function DataGridInner<R extends GridValidRowModel>(props: DataGridProps<R>) {
878
1187
  false,
879
1188
  )}
880
1189
  >
881
- {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
+ )}
882
1204
  </Table.Cell>
883
1205
  );
884
1206
  })}
885
1207
  </Table.Row>
886
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
+ ];
887
1227
  })
888
1228
  )}
889
1229
  </Table.Body>