@bmsuisse/datagrid 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2076,7 +2076,59 @@ import { BarsArrowDownIcon, BarsArrowUpIcon, FunnelIcon as FunnelIcon7 } from "@
2076
2076
  import { columnResizingFeature, columnSizingFeature, flexRender, tableFeatures, useTable } from "@tanstack/react-table";
2077
2077
  import { useVirtualizer } from "@tanstack/react-virtual";
2078
2078
  import { ChevronRight, Loader2 } from "lucide-react";
2079
- import { Fragment as Fragment2, useEffect as useEffect9, useLayoutEffect, useMemo as useMemo6, useRef as useRef7, useState as useState8 } from "react";
2079
+ import { Fragment as Fragment2, useEffect as useEffect9, useMemo as useMemo7, useRef as useRef8, useState as useState10 } from "react";
2080
+
2081
+ // src/hooks/useGroupExpansion.ts
2082
+ import { useCallback as useCallback2, useState as useState7 } from "react";
2083
+ function useGroupExpansion(defaultGroupsExpanded, controlledExpandedGroups, onExpandedGroupsChange) {
2084
+ const [internalExpandedGroups, setInternalExpandedGroups] = useState7({});
2085
+ const expandedGroups = controlledExpandedGroups ?? internalExpandedGroups;
2086
+ const updateExpandedGroups = useCallback2(
2087
+ (next) => {
2088
+ setInternalExpandedGroups(next);
2089
+ onExpandedGroupsChange?.(next);
2090
+ },
2091
+ [onExpandedGroupsChange]
2092
+ );
2093
+ const isGroupExpanded = useCallback2(
2094
+ (key) => expandedGroups[key] ?? defaultGroupsExpanded,
2095
+ [expandedGroups, defaultGroupsExpanded]
2096
+ );
2097
+ const toggleGroupExpanded = useCallback2(
2098
+ (key) => {
2099
+ updateExpandedGroups({ ...expandedGroups, [key]: !isGroupExpanded(key) });
2100
+ },
2101
+ [expandedGroups, isGroupExpanded, updateExpandedGroups]
2102
+ );
2103
+ return { expandedGroups, isGroupExpanded, toggleGroupExpanded };
2104
+ }
2105
+
2106
+ // src/hooks/useStickyGroupHeaderTop.ts
2107
+ import { useLayoutEffect, useRef as useRef6, useState as useState8 } from "react";
2108
+ function useStickyGroupHeaderTop(enabled) {
2109
+ const theadRef = useRef6(null);
2110
+ const [groupHeaderTop, setGroupHeaderTop] = useState8(0);
2111
+ useLayoutEffect(() => {
2112
+ if (!enabled) return;
2113
+ const theadEl = theadRef.current;
2114
+ if (!theadEl) return;
2115
+ const updateHeight = () => setGroupHeaderTop(theadEl.getBoundingClientRect().height);
2116
+ updateHeight();
2117
+ const observer = new ResizeObserver(updateHeight);
2118
+ observer.observe(theadEl);
2119
+ return () => observer.disconnect();
2120
+ }, [enabled]);
2121
+ return { theadRef, groupHeaderTop };
2122
+ }
2123
+
2124
+ // src/hooks/useVisibleColumns.ts
2125
+ import { useMemo as useMemo4 } from "react";
2126
+ function useVisibleColumns(columns, columnVisibility) {
2127
+ return useMemo4(
2128
+ () => columnVisibility ? columns.filter((column) => isColumnVisible(columnVisibility, column.id)) : columns,
2129
+ [columns, columnVisibility]
2130
+ );
2131
+ }
2080
2132
 
2081
2133
  // src/lib/structuralColumns.ts
2082
2134
  var SELECTION_COLUMN_WIDTH = 44;
@@ -2119,20 +2171,20 @@ function processClientData(data, state) {
2119
2171
  }
2120
2172
 
2121
2173
  // src/grid/useGridState.ts
2122
- import { useCallback as useCallback2, useEffect as useEffect8, useMemo as useMemo5, useState as useState7 } from "react";
2174
+ import { useCallback as useCallback3, useEffect as useEffect8, useMemo as useMemo6, useState as useState9 } from "react";
2123
2175
 
2124
2176
  // src/hooks/useDebouncedCallback.ts
2125
- import { useEffect as useEffect7, useMemo as useMemo4, useRef as useRef6 } from "react";
2177
+ import { useEffect as useEffect7, useMemo as useMemo5, useRef as useRef7 } from "react";
2126
2178
  function useDebouncedCallback(callback, delayMs) {
2127
- const callbackRef = useRef6(callback);
2179
+ const callbackRef = useRef7(callback);
2128
2180
  callbackRef.current = callback;
2129
- const timeoutRef = useRef6(void 0);
2181
+ const timeoutRef = useRef7(void 0);
2130
2182
  useEffect7(() => {
2131
2183
  return () => {
2132
2184
  clearTimeout(timeoutRef.current);
2133
2185
  };
2134
2186
  }, []);
2135
- return useMemo4(() => {
2187
+ return useMemo5(() => {
2136
2188
  return {
2137
2189
  run: (...args) => {
2138
2190
  clearTimeout(timeoutRef.current);
@@ -2157,10 +2209,10 @@ function nextSortDir(current, descFirst) {
2157
2209
  return void 0;
2158
2210
  }
2159
2211
  function useGridState(dataSource, initialState, externalState) {
2160
- const [state, setState] = useState7(
2212
+ const [state, setState] = useState9(
2161
2213
  () => externalState ?? { ...DEFAULT_STATE, ...initialState }
2162
2214
  );
2163
- const notifyRaw = useCallback2(
2215
+ const notifyRaw = useCallback3(
2164
2216
  (next) => {
2165
2217
  if (dataSource.mode === "server") dataSource.onStateChange(next);
2166
2218
  },
@@ -2170,7 +2222,7 @@ function useGridState(dataSource, initialState, externalState) {
2170
2222
  notifyRaw,
2171
2223
  FILTER_CHANGE_DEBOUNCE_MS
2172
2224
  );
2173
- const notifyNow = useCallback2(
2225
+ const notifyNow = useCallback3(
2174
2226
  (next) => {
2175
2227
  cancelDebouncedNotify();
2176
2228
  notifyRaw(next);
@@ -2182,14 +2234,14 @@ function useGridState(dataSource, initialState, externalState) {
2182
2234
  cancelDebouncedNotify();
2183
2235
  setState(externalState);
2184
2236
  }, [externalState, cancelDebouncedNotify]);
2185
- const filtersByColumn = useMemo5(() => {
2237
+ const filtersByColumn = useMemo6(() => {
2186
2238
  const map = /* @__PURE__ */ new Map();
2187
2239
  for (const child of state.filter?.filters ?? []) {
2188
2240
  if ("operator" in child) map.set(fieldKey(child.field), child);
2189
2241
  }
2190
2242
  return map;
2191
2243
  }, [state.filter]);
2192
- const setColumnFilter = useCallback2(
2244
+ const setColumnFilter = useCallback3(
2193
2245
  (columnId, next) => {
2194
2246
  setState((prev) => {
2195
2247
  const remaining = (prev.filter?.filters ?? []).filter(
@@ -2207,7 +2259,7 @@ function useGridState(dataSource, initialState, externalState) {
2207
2259
  },
2208
2260
  [notifyDebounced]
2209
2261
  );
2210
- const toggleSort = useCallback2(
2262
+ const toggleSort = useCallback3(
2211
2263
  (columnId, additive, descFirst = false) => {
2212
2264
  setState((prev) => {
2213
2265
  const existing = prev.sort.find((s) => s.field === columnId);
@@ -2222,7 +2274,7 @@ function useGridState(dataSource, initialState, externalState) {
2222
2274
  },
2223
2275
  [notifyNow]
2224
2276
  );
2225
- const setPage = useCallback2(
2277
+ const setPage = useCallback3(
2226
2278
  (page) => {
2227
2279
  setState((prev) => {
2228
2280
  const nextState = { ...prev, page };
@@ -2232,7 +2284,7 @@ function useGridState(dataSource, initialState, externalState) {
2232
2284
  },
2233
2285
  [notifyNow]
2234
2286
  );
2235
- const setPageSize = useCallback2(
2287
+ const setPageSize = useCallback3(
2236
2288
  (pageSize) => {
2237
2289
  setState((prev) => {
2238
2290
  const nextState = { ...prev, pageSize, page: 0 };
@@ -2322,19 +2374,19 @@ function DataGrid({
2322
2374
  initialState,
2323
2375
  gridState
2324
2376
  );
2325
- const [internalSelectedIds, setInternalSelectedIds] = useState8(/* @__PURE__ */ new Set());
2377
+ const [internalSelectedIds, setInternalSelectedIds] = useState10(/* @__PURE__ */ new Set());
2326
2378
  const selectedIds = controlledSelectedIds ?? internalSelectedIds;
2327
2379
  function updateSelectedIds(next) {
2328
2380
  setInternalSelectedIds(next);
2329
2381
  onSelectedIdsChange?.(next);
2330
2382
  }
2331
- const [internalColumnSizing, setInternalColumnSizing] = useState8({});
2383
+ const [internalColumnSizing, setInternalColumnSizing] = useState10({});
2332
2384
  const columnSizing = controlledColumnSizing ?? internalColumnSizing;
2333
2385
  function updateColumnSizing(next) {
2334
2386
  setInternalColumnSizing(next);
2335
2387
  onColumnSizingChange?.(next);
2336
2388
  }
2337
- const [expandedIds, setExpandedIds] = useState8(/* @__PURE__ */ new Set());
2389
+ const [expandedIds, setExpandedIds] = useState10(/* @__PURE__ */ new Set());
2338
2390
  function toggleRowExpanded(id) {
2339
2391
  setExpandedIds((prev) => {
2340
2392
  const next = new Set(prev);
@@ -2343,32 +2395,22 @@ function DataGrid({
2343
2395
  return next;
2344
2396
  });
2345
2397
  }
2346
- const [internalExpandedGroups, setInternalExpandedGroups] = useState8({});
2347
- const expandedGroupsState = controlledExpandedGroups ?? internalExpandedGroups;
2348
- function updateExpandedGroups(next) {
2349
- setInternalExpandedGroups(next);
2350
- onExpandedGroupsChange?.(next);
2351
- }
2352
- function isGroupExpanded(key) {
2353
- return expandedGroupsState[key] ?? defaultGroupsExpanded;
2354
- }
2355
- function toggleGroupExpanded(key) {
2356
- updateExpandedGroups({ ...expandedGroupsState, [key]: !isGroupExpanded(key) });
2357
- }
2398
+ const { isGroupExpanded, toggleGroupExpanded } = useGroupExpansion(
2399
+ defaultGroupsExpanded,
2400
+ controlledExpandedGroups,
2401
+ onExpandedGroupsChange
2402
+ );
2358
2403
  const { pendingEdits, editErrors, ctxRef: editingCtxRef, handleSaveEdits, handleDiscardEdits } = useEditingState(
2359
2404
  editing,
2360
2405
  getRowId
2361
2406
  );
2362
- const visibleColumns = useMemo6(
2363
- () => columnVisibility ? columns.filter((column) => isColumnVisible(columnVisibility, column.id)) : columns,
2364
- [columns, columnVisibility]
2365
- );
2407
+ const visibleColumns = useVisibleColumns(columns, columnVisibility);
2366
2408
  const serverRowCount = dataSource.mode === "server" ? dataSource.rowCount : void 0;
2367
- const { rows, rowCount } = useMemo6(() => {
2409
+ const { rows, rowCount } = useMemo7(() => {
2368
2410
  if (dataSource.mode === "client") return processClientData(dataSource.data, state);
2369
2411
  return { rows: dataSource.data, rowCount: serverRowCount ?? 0 };
2370
2412
  }, [dataSource.mode, dataSource.data, serverRowCount, state]);
2371
- const selectedRows = useMemo6(
2413
+ const selectedRows = useMemo7(
2372
2414
  () => rows.filter((row) => selectedIds.has(getRowId(row))),
2373
2415
  [rows, selectedIds, getRowId]
2374
2416
  );
@@ -2387,8 +2429,8 @@ function DataGrid({
2387
2429
  }
2388
2430
  updateSelectedIds(next);
2389
2431
  }
2390
- const tanstackColumns = useMemo6(() => toTanstackColumns(visibleColumns, editingCtxRef), [visibleColumns]);
2391
- const columnById = useMemo6(() => {
2432
+ const tanstackColumns = useMemo7(() => toTanstackColumns(visibleColumns, editingCtxRef), [visibleColumns]);
2433
+ const columnById = useMemo7(() => {
2392
2434
  const map = /* @__PURE__ */ new Map();
2393
2435
  for (const column of visibleColumns) map.set(column.id, column);
2394
2436
  return map;
@@ -2413,7 +2455,7 @@ function DataGrid({
2413
2455
  const showDetailColumn = Boolean(renderDetail);
2414
2456
  const totalColumnCount = visibleColumns.length + (showDetailColumn ? 1 : 0) + (showSelectionColumn ? 1 : 0) + (showRowActionsColumn ? 1 : 0);
2415
2457
  const hasFilterRow = visibleColumns.some((column) => isFilterable(column) && column.filterDisplay === "row");
2416
- const headerRuns = useMemo6(() => computeHeaderRuns(visibleColumns), [visibleColumns]);
2458
+ const headerRuns = useMemo7(() => computeHeaderRuns(visibleColumns), [visibleColumns]);
2417
2459
  const hasHeaderGroups = visibleColumns.some((column) => column.headerGroup);
2418
2460
  function renderFilterWidget(column) {
2419
2461
  const value = filtersByColumn.get(column.id);
@@ -2423,11 +2465,11 @@ function DataGrid({
2423
2465
  const columnSize = (columnId) => table.getColumn(columnId)?.getSize() ?? DEFAULT_COLUMN_SIZE;
2424
2466
  const leadingStructuralWidth = (showDetailColumn ? DETAIL_COLUMN_WIDTH : 0) + (showSelectionColumn ? SELECTION_COLUMN_WIDTH : 0);
2425
2467
  const trailingStructuralWidth = showRowActionsColumn ? ROW_ACTIONS_COLUMN_WIDTH : 0;
2426
- const leftPinnedOffsets = useMemo6(
2468
+ const leftPinnedOffsets = useMemo7(
2427
2469
  () => pinnedOffsets(visibleColumns, "left", columnSize, leadingStructuralWidth),
2428
2470
  [visibleColumns, columnSizing, leadingStructuralWidth]
2429
2471
  );
2430
- const rightPinnedOffsets = useMemo6(
2472
+ const rightPinnedOffsets = useMemo7(
2431
2473
  () => pinnedOffsets(visibleColumns, "right", columnSize, trailingStructuralWidth),
2432
2474
  [visibleColumns, columnSizing, trailingStructuralWidth]
2433
2475
  );
@@ -2475,7 +2517,7 @@ function DataGrid({
2475
2517
  style: pinnedProps?.style
2476
2518
  };
2477
2519
  }
2478
- const bodyCellPropsByColumn = useMemo6(() => {
2520
+ const bodyCellPropsByColumn = useMemo7(() => {
2479
2521
  const map = /* @__PURE__ */ new Map();
2480
2522
  for (const column of visibleColumns) map.set(column.id, bodyCellClassAndStyle(column));
2481
2523
  return map;
@@ -2491,7 +2533,7 @@ function DataGrid({
2491
2533
  );
2492
2534
  return { className, style: pinnedProps.style };
2493
2535
  }
2494
- const leafHeaderCellPropsByColumn = useMemo6(() => {
2536
+ const leafHeaderCellPropsByColumn = useMemo7(() => {
2495
2537
  const map = /* @__PURE__ */ new Map();
2496
2538
  for (const column of visibleColumns) {
2497
2539
  map.set(
@@ -2501,7 +2543,7 @@ function DataGrid({
2501
2543
  }
2502
2544
  return map;
2503
2545
  }, [visibleColumns, enableColumnResizing, leftPinnedOffsets, rightPinnedOffsets, columnSizing]);
2504
- const filterHeaderCellPropsByColumn = useMemo6(() => {
2546
+ const filterHeaderCellPropsByColumn = useMemo7(() => {
2505
2547
  const map = /* @__PURE__ */ new Map();
2506
2548
  for (const column of visibleColumns) {
2507
2549
  map.set(column.id, headerCellClassAndStyle(column, "border-b border-border p-2 align-top font-normal"));
@@ -2509,26 +2551,28 @@ function DataGrid({
2509
2551
  return map;
2510
2552
  }, [visibleColumns, enableColumnResizing, leftPinnedOffsets, rightPinnedOffsets, columnSizing]);
2511
2553
  const tableRows = table.getRowModel().rows;
2512
- const shouldVirtualize = Boolean(virtualize) && !groupBy && tableRows.length > (virtualize?.threshold ?? 100);
2513
- const groupedBuckets = useMemo6(
2554
+ const groupedBuckets = useMemo7(
2514
2555
  () => groupBy ? groupRows(tableRows, (row) => groupBy(row.original)) : void 0,
2515
2556
  [tableRows, groupBy]
2516
2557
  );
2517
- const theadRef = useRef7(null);
2518
- const [groupHeaderTop, setGroupHeaderTop] = useState8(0);
2519
- useLayoutEffect(() => {
2520
- if (!groupBy) return;
2521
- const theadEl = theadRef.current;
2522
- if (!theadEl) return;
2523
- const updateHeight = () => setGroupHeaderTop(theadEl.getBoundingClientRect().height);
2524
- updateHeight();
2525
- const observer = new ResizeObserver(updateHeight);
2526
- observer.observe(theadEl);
2527
- return () => observer.disconnect();
2528
- }, [groupBy]);
2529
- const scrollRef = useRef7(null);
2558
+ const { theadRef, groupHeaderTop } = useStickyGroupHeaderTop(Boolean(groupBy));
2559
+ const shouldVirtualize = Boolean(virtualize) && tableRows.length > (virtualize?.threshold ?? 100);
2560
+ const flatItems = useMemo7(() => {
2561
+ if (!shouldVirtualize) return [];
2562
+ if (!groupedBuckets) return tableRows.map((row) => ({ kind: "row", row }));
2563
+ const items = [];
2564
+ for (const bucket of groupedBuckets) {
2565
+ const originalRows = bucket.items.map((row) => row.original);
2566
+ items.push({ kind: "header", key: bucket.key, rows: originalRows });
2567
+ if (isGroupExpanded(bucket.key)) {
2568
+ for (const row of bucket.items) items.push({ kind: "row", row });
2569
+ }
2570
+ }
2571
+ return items;
2572
+ }, [shouldVirtualize, groupedBuckets, tableRows, isGroupExpanded]);
2573
+ const scrollRef = useRef8(null);
2530
2574
  const virtualizer = useVirtualizer({
2531
- count: tableRows.length,
2575
+ count: flatItems.length,
2532
2576
  getScrollElement: () => scrollRef.current,
2533
2577
  estimateSize: () => virtualize?.estimatedRowHeight ?? 40,
2534
2578
  overscan: virtualize?.overscan ?? 10,
@@ -2538,21 +2582,21 @@ function DataGrid({
2538
2582
  const paddingTop = virtualItems.length > 0 ? virtualItems[0].start : 0;
2539
2583
  const paddingBottom = virtualItems.length > 0 ? virtualizer.getTotalSize() - virtualItems[virtualItems.length - 1].end : 0;
2540
2584
  const lastVisibleIndex = virtualItems.length > 0 ? virtualItems[virtualItems.length - 1].index : -1;
2541
- const notifiedForLengthRef = useRef7(-1);
2585
+ const notifiedForLengthRef = useRef8(-1);
2542
2586
  useEffect9(() => {
2543
2587
  if (!virtualize?.onEndReached) return;
2544
- if (lastVisibleIndex < 0 || lastVisibleIndex < tableRows.length - 1) return;
2588
+ if (lastVisibleIndex < 0 || lastVisibleIndex < flatItems.length - 1) return;
2545
2589
  if (virtualize.hasMore === false) return;
2546
2590
  if (notifiedForLengthRef.current === tableRows.length) return;
2547
2591
  notifiedForLengthRef.current = tableRows.length;
2548
2592
  virtualize.onEndReached();
2549
- }, [lastVisibleIndex, tableRows.length, virtualize]);
2550
- function renderRow(row, measureRef) {
2593
+ }, [lastVisibleIndex, flatItems.length, tableRows.length, virtualize]);
2594
+ function renderRow(row, measureRef, dataIndex = row.index) {
2551
2595
  const isExpanded = expandedIds.has(row.id);
2552
2596
  const rowProps = getRowProps?.(row.original);
2553
2597
  const rowTestId = getRowTestId?.(row.original) ?? `row-${row.id}`;
2554
2598
  const isOddRow = zebra && row.index % 2 === 1;
2555
- return /* @__PURE__ */ jsxs18("tbody", { ref: measureRef, "data-index": row.index, children: [
2599
+ return /* @__PURE__ */ jsxs18("tbody", { ref: measureRef, "data-index": dataIndex, children: [
2556
2600
  /* @__PURE__ */ jsxs18(
2557
2601
  "tr",
2558
2602
  {
@@ -2623,43 +2667,48 @@ function DataGrid({
2623
2667
  showDetailColumn && isExpanded && renderDetail && /* @__PURE__ */ jsx28("tr", { "data-testid": `${rowTestId}-detail`, children: /* @__PURE__ */ jsx28("td", { colSpan: totalColumnCount, className: "border-b border-border p-2", children: renderDetail(row.original) }) })
2624
2668
  ] }, row.id);
2625
2669
  }
2670
+ function renderGroupHeaderTbody(key, rows2, measureRef, dataIndex) {
2671
+ const expanded = isGroupExpanded(key);
2672
+ return /* @__PURE__ */ jsx28("tbody", { ref: measureRef, "data-index": dataIndex, children: /* @__PURE__ */ jsx28("tr", { "data-testid": `group-header-${key}`, children: /* @__PURE__ */ jsx28(
2673
+ "td",
2674
+ {
2675
+ colSpan: totalColumnCount,
2676
+ className: cn("sticky z-20 border-b p-2 font-medium", zebra ? "bg-muted" : "bg-background"),
2677
+ style: { top: groupHeaderTop },
2678
+ children: /* @__PURE__ */ jsxs18(
2679
+ "button",
2680
+ {
2681
+ type: "button",
2682
+ className: "flex w-full items-center gap-1 text-left",
2683
+ onClick: () => toggleGroupExpanded(key),
2684
+ "aria-expanded": expanded,
2685
+ children: [
2686
+ /* @__PURE__ */ jsx28(
2687
+ ChevronRight,
2688
+ {
2689
+ className: `h-4 w-4 shrink-0 transition-transform${expanded ? " rotate-90" : ""}`,
2690
+ "aria-hidden": true
2691
+ }
2692
+ ),
2693
+ renderGroupHeader ? renderGroupHeader(key, rows2, expanded) : `${key} (${rows2.length})`
2694
+ ]
2695
+ }
2696
+ )
2697
+ }
2698
+ ) }) }, `group-${key}`);
2699
+ }
2626
2700
  function renderGroupedBucket(bucket) {
2627
2701
  const expanded = isGroupExpanded(bucket.key);
2628
2702
  const originalRows = bucket.items.map((row) => row.original);
2629
2703
  return /* @__PURE__ */ jsxs18(Fragment2, { children: [
2630
- /* @__PURE__ */ jsx28("tbody", { children: /* @__PURE__ */ jsx28("tr", { "data-testid": `group-header-${bucket.key}`, children: /* @__PURE__ */ jsx28(
2631
- "td",
2632
- {
2633
- colSpan: totalColumnCount,
2634
- className: cn(
2635
- "sticky z-20 border-b p-2 font-medium",
2636
- zebra ? "bg-muted" : "bg-background"
2637
- ),
2638
- style: { top: groupHeaderTop },
2639
- children: /* @__PURE__ */ jsxs18(
2640
- "button",
2641
- {
2642
- type: "button",
2643
- className: "flex w-full items-center gap-1 text-left",
2644
- onClick: () => toggleGroupExpanded(bucket.key),
2645
- "aria-expanded": expanded,
2646
- children: [
2647
- /* @__PURE__ */ jsx28(
2648
- ChevronRight,
2649
- {
2650
- className: `h-4 w-4 shrink-0 transition-transform${expanded ? " rotate-90" : ""}`,
2651
- "aria-hidden": true
2652
- }
2653
- ),
2654
- renderGroupHeader ? renderGroupHeader(bucket.key, originalRows, expanded) : `${bucket.key} (${originalRows.length})`
2655
- ]
2656
- }
2657
- )
2658
- }
2659
- ) }) }),
2704
+ renderGroupHeaderTbody(bucket.key, originalRows),
2660
2705
  expanded && bucket.items.map((row) => renderRow(row))
2661
2706
  ] }, `group-${bucket.key}`);
2662
2707
  }
2708
+ function renderFlatItem(item, index, measureRef) {
2709
+ if (item.kind === "header") return renderGroupHeaderTbody(item.key, item.rows, measureRef, index);
2710
+ return renderRow(item.row, measureRef, index);
2711
+ }
2663
2712
  return (
2664
2713
  // `h-full`/`min-h-0` here and on the two wrappers below are no-ops unless
2665
2714
  // a consumer itself gives <DataGrid> a bounded height (e.g. wraps it in
@@ -2881,7 +2930,11 @@ function DataGrid({
2881
2930
  rows.length === 0 ? /* @__PURE__ */ jsx28("tbody", { children: /* @__PURE__ */ jsx28("tr", { children: /* @__PURE__ */ jsx28("td", { colSpan: totalColumnCount, className: "p-4 text-center text-muted-foreground", children: loading ? "Loading..." : "No results." }) }) }) : shouldVirtualize ? /* @__PURE__ */ jsxs18(Fragment3, { children: [
2882
2931
  paddingTop > 0 && /* @__PURE__ */ jsx28("tbody", { children: /* @__PURE__ */ jsx28("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx28("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingTop } }) }) }),
2883
2932
  virtualItems.map(
2884
- (virtualItem) => renderRow(tableRows[virtualItem.index], (el) => virtualizer.measureElement(el))
2933
+ (virtualItem) => renderFlatItem(
2934
+ flatItems[virtualItem.index],
2935
+ virtualItem.index,
2936
+ (el) => virtualizer.measureElement(el)
2937
+ )
2885
2938
  ),
2886
2939
  paddingBottom > 0 && /* @__PURE__ */ jsx28("tbody", { children: /* @__PURE__ */ jsx28("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx28("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingBottom } }) }) })
2887
2940
  ] }) : groupedBuckets ? groupedBuckets.map((bucket) => renderGroupedBucket(bucket)) : tableRows.map((row) => renderRow(row))
@@ -2931,7 +2984,7 @@ function DataGrid({
2931
2984
  // src/tree/TreeDataGrid.tsx
2932
2985
  import { useVirtualizer as useVirtualizer2 } from "@tanstack/react-virtual";
2933
2986
  import { AlertCircle, ChevronDown as ChevronDown2, ChevronRight as ChevronRight2, Loader2 as Loader22 } from "lucide-react";
2934
- import { useEffect as useEffect10, useMemo as useMemo8, useRef as useRef8 } from "react";
2987
+ import { Fragment as Fragment4, useEffect as useEffect10, useMemo as useMemo9, useRef as useRef9 } from "react";
2935
2988
 
2936
2989
  // src/tree/flattenTree.ts
2937
2990
  function flattenTree(roots, accessors, expanded, childrenOverride) {
@@ -2976,7 +3029,7 @@ function computeSelectionStates(roots, accessors, childrenOverride, selectedIds)
2976
3029
  }
2977
3030
 
2978
3031
  // src/tree/useTreeState.ts
2979
- import { useCallback as useCallback3, useMemo as useMemo7, useState as useState9 } from "react";
3032
+ import { useCallback as useCallback4, useMemo as useMemo8, useState as useState11 } from "react";
2980
3033
  function useTreeState({
2981
3034
  data,
2982
3035
  getRowId,
@@ -2988,9 +3041,9 @@ function useTreeState({
2988
3041
  childrenMap: controlledChildrenMap,
2989
3042
  onChildrenMapChange
2990
3043
  }) {
2991
- const [internalExpanded, setInternalExpanded] = useState9({});
3044
+ const [internalExpanded, setInternalExpanded] = useState11({});
2992
3045
  const expanded = controlledExpanded ?? internalExpanded;
2993
- const setExpanded = useCallback3(
3046
+ const setExpanded = useCallback4(
2994
3047
  (updater) => {
2995
3048
  if (controlledExpanded) {
2996
3049
  onExpandedChange?.(updater(controlledExpanded));
@@ -3000,9 +3053,9 @@ function useTreeState({
3000
3053
  },
3001
3054
  [controlledExpanded, onExpandedChange]
3002
3055
  );
3003
- const [internalChildrenMap, setInternalChildrenMap] = useState9(/* @__PURE__ */ new Map());
3056
+ const [internalChildrenMap, setInternalChildrenMap] = useState11(/* @__PURE__ */ new Map());
3004
3057
  const childrenMap = controlledChildrenMap ?? internalChildrenMap;
3005
- const setChildrenMap = useCallback3(
3058
+ const setChildrenMap = useCallback4(
3006
3059
  (updater) => {
3007
3060
  if (controlledChildrenMap) {
3008
3061
  onChildrenMapChange?.(updater(controlledChildrenMap));
@@ -3012,13 +3065,13 @@ function useTreeState({
3012
3065
  },
3013
3066
  [controlledChildrenMap, onChildrenMapChange]
3014
3067
  );
3015
- const [loadingIds, setLoadingIds] = useState9(/* @__PURE__ */ new Set());
3016
- const [errorIds, setErrorIds] = useState9(/* @__PURE__ */ new Map());
3017
- const resolveHasChildren = useMemo7(
3068
+ const [loadingIds, setLoadingIds] = useState11(/* @__PURE__ */ new Set());
3069
+ const [errorIds, setErrorIds] = useState11(/* @__PURE__ */ new Map());
3070
+ const resolveHasChildren = useMemo8(
3018
3071
  () => hasChildren ?? ((row) => (getChildren(row)?.length ?? 0) > 0),
3019
3072
  [hasChildren, getChildren]
3020
3073
  );
3021
- const loadChildrenFor = useCallback3(
3074
+ const loadChildrenFor = useCallback4(
3022
3075
  async (row) => {
3023
3076
  const id = getRowId(row);
3024
3077
  const cached = childrenMap.get(id);
@@ -3051,7 +3104,7 @@ function useTreeState({
3051
3104
  },
3052
3105
  [childrenMap, getChildren, getRowId, onLoadChildren, setChildrenMap]
3053
3106
  );
3054
- const toggleExpand = useCallback3(
3107
+ const toggleExpand = useCallback4(
3055
3108
  (row) => {
3056
3109
  const id = getRowId(row);
3057
3110
  const willExpand = !expanded[id];
@@ -3063,14 +3116,14 @@ function useTreeState({
3063
3116
  },
3064
3117
  [expanded, getRowId, loadChildrenFor, resolveHasChildren, setExpanded]
3065
3118
  );
3066
- const retry = useCallback3(
3119
+ const retry = useCallback4(
3067
3120
  (row) => {
3068
3121
  loadChildrenFor(row).catch(() => {
3069
3122
  });
3070
3123
  },
3071
3124
  [loadChildrenFor]
3072
3125
  );
3073
- const expandToLevel = useCallback3(
3126
+ const expandToLevel = useCallback4(
3074
3127
  async (level) => {
3075
3128
  const nextExpanded = { ...expanded };
3076
3129
  async function visit(row, depth) {
@@ -3093,7 +3146,7 @@ function useTreeState({
3093
3146
  }
3094
3147
 
3095
3148
  // src/tree/TreeDataGrid.tsx
3096
- import { Fragment as Fragment4, jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
3149
+ import { Fragment as Fragment5, jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
3097
3150
  var EMPTY_SELECTED_IDS = /* @__PURE__ */ new Set();
3098
3151
  var UNCHECKED_STATE = { checked: false, indeterminate: false };
3099
3152
  function renderCell(column, row, editingCtx) {
@@ -3137,6 +3190,7 @@ function TreeDataGrid({
3137
3190
  columns,
3138
3191
  data,
3139
3192
  treeColumnId,
3193
+ columnVisibility,
3140
3194
  getRowId,
3141
3195
  getChildren,
3142
3196
  hasChildren,
@@ -3157,10 +3211,15 @@ function TreeDataGrid({
3157
3211
  onSelectedIdsChange,
3158
3212
  getRowSelectionState,
3159
3213
  isRowSelectionDisabled,
3214
+ groupBy,
3215
+ renderGroupHeader,
3216
+ defaultGroupsExpanded = true,
3217
+ expandedGroups: controlledExpandedGroups,
3218
+ onExpandedGroupsChange,
3160
3219
  zebra = true,
3161
3220
  editing
3162
3221
  }) {
3163
- const accessors = useMemo8(
3222
+ const accessors = useMemo9(
3164
3223
  () => ({ getRowId, getChildren, hasChildren }),
3165
3224
  [getRowId, getChildren, hasChildren]
3166
3225
  );
@@ -3176,21 +3235,48 @@ function TreeDataGrid({
3176
3235
  childrenMap: controlledChildrenMap,
3177
3236
  onChildrenMapChange
3178
3237
  });
3179
- const didAutoExpand = useRef8(false);
3238
+ const didAutoExpand = useRef9(false);
3180
3239
  useEffect10(() => {
3181
3240
  if (didAutoExpand.current) return;
3182
3241
  didAutoExpand.current = true;
3183
3242
  if (initialExpandedLevel > 0) void expandToLevel(initialExpandedLevel);
3184
3243
  }, []);
3185
- const flatRows = useMemo8(
3186
- () => flattenTree(data, accessors, expanded, childrenMap),
3187
- [data, accessors, expanded, childrenMap]
3244
+ const visibleColumns = useVisibleColumns(columns, columnVisibility);
3245
+ const { isGroupExpanded, toggleGroupExpanded } = useGroupExpansion(
3246
+ defaultGroupsExpanded,
3247
+ controlledExpandedGroups,
3248
+ onExpandedGroupsChange
3249
+ );
3250
+ const flatRows = useMemo9(
3251
+ () => groupBy ? [] : flattenTree(data, accessors, expanded, childrenMap),
3252
+ [groupBy, data, accessors, expanded, childrenMap]
3253
+ );
3254
+ const groupedBuckets = useMemo9(
3255
+ () => groupBy ? groupRows(data, groupBy).map((bucket) => ({
3256
+ key: bucket.key,
3257
+ roots: bucket.items,
3258
+ flatRows: flattenTree(bucket.items, accessors, expanded, childrenMap)
3259
+ })) : void 0,
3260
+ [groupBy, data, accessors, expanded, childrenMap]
3188
3261
  );
3262
+ const groupedRender = useMemo9(() => {
3263
+ if (!groupedBuckets) return void 0;
3264
+ const startIndexByBucket = /* @__PURE__ */ new Map();
3265
+ const rows = [];
3266
+ for (const bucket of groupedBuckets) {
3267
+ if (!isGroupExpanded(bucket.key)) continue;
3268
+ startIndexByBucket.set(bucket.key, rows.length);
3269
+ rows.push(...bucket.flatRows);
3270
+ }
3271
+ return { startIndexByBucket, rows };
3272
+ }, [groupedBuckets, isGroupExpanded]);
3273
+ const allFlatRows = groupedRender ? groupedRender.rows : flatRows;
3189
3274
  const treeColId = treeColumnId ?? columns[0]?.id;
3190
3275
  const showRowActionsColumn = Boolean(rowActions?.length);
3191
3276
  const showSelectionColumn = controlledSelectedIds !== void 0;
3192
- const totalColumnCount = columns.length + (showRowActionsColumn ? 1 : 0) + (showSelectionColumn ? 1 : 0);
3193
- const selectionStates = useMemo8(
3277
+ const totalColumnCount = visibleColumns.length + (showRowActionsColumn ? 1 : 0) + (showSelectionColumn ? 1 : 0);
3278
+ const { theadRef, groupHeaderTop } = useStickyGroupHeaderTop(Boolean(groupBy));
3279
+ const selectionStates = useMemo9(
3194
3280
  () => showSelectionColumn ? computeSelectionStates(data, accessors, childrenMap, controlledSelectedIds ?? EMPTY_SELECTED_IDS) : void 0,
3195
3281
  [showSelectionColumn, data, accessors, childrenMap, controlledSelectedIds]
3196
3282
  );
@@ -3206,9 +3292,9 @@ function TreeDataGrid({
3206
3292
  else next.add(id);
3207
3293
  onSelectedIdsChange?.(next);
3208
3294
  }
3209
- const selectableFlatRows = useMemo8(
3210
- () => flatRows.filter((flatRow) => !(isRowSelectionDisabled?.(flatRow.row) ?? false)),
3211
- [flatRows, isRowSelectionDisabled]
3295
+ const selectableFlatRows = useMemo9(
3296
+ () => allFlatRows.filter((flatRow) => !(isRowSelectionDisabled?.(flatRow.row) ?? false)),
3297
+ [allFlatRows, isRowSelectionDisabled]
3212
3298
  );
3213
3299
  const allVisibleSelected = showSelectionColumn && selectableFlatRows.length > 0 && selectableFlatRows.every((flatRow) => controlledSelectedIds.has(flatRow.id));
3214
3300
  function toggleAllVisibleSelected() {
@@ -3220,13 +3306,13 @@ function TreeDataGrid({
3220
3306
  }
3221
3307
  onSelectedIdsChange?.(next);
3222
3308
  }
3223
- const bodyTdClassByColumn = useMemo8(() => {
3309
+ const bodyTdClassByColumn = useMemo9(() => {
3224
3310
  const map = /* @__PURE__ */ new Map();
3225
- for (const column of columns) map.set(column.id, `p-2 ${alignClassName(column)}`);
3311
+ for (const column of visibleColumns) map.set(column.id, `p-2 ${alignClassName(column)}`);
3226
3312
  return map;
3227
- }, [columns]);
3228
- const scrollRef = useRef8(null);
3229
- const shouldVirtualize = flatRows.length > virtualizeThreshold;
3313
+ }, [visibleColumns]);
3314
+ const scrollRef = useRef9(null);
3315
+ const shouldVirtualize = !groupBy && flatRows.length > virtualizeThreshold;
3230
3316
  const virtualizer = useVirtualizer2({
3231
3317
  count: flatRows.length,
3232
3318
  getScrollElement: () => scrollRef.current,
@@ -3264,7 +3350,7 @@ function TreeDataGrid({
3264
3350
  }
3265
3351
  );
3266
3352
  })() }),
3267
- columns.map((column) => /* @__PURE__ */ jsx29("td", { className: bodyTdClassByColumn.get(column.id), children: column.id === treeColId ? /* @__PURE__ */ jsx29(
3353
+ visibleColumns.map((column) => /* @__PURE__ */ jsx29("td", { className: bodyTdClassByColumn.get(column.id), children: column.id === treeColId ? /* @__PURE__ */ jsx29(
3268
3354
  TreeCell,
3269
3355
  {
3270
3356
  flatRow,
@@ -3312,7 +3398,7 @@ function TreeDataGrid({
3312
3398
  className: shouldVirtualize ? "overflow-y-auto" : void 0,
3313
3399
  style: shouldVirtualize ? { maxHeight: maxBodyHeight } : void 0,
3314
3400
  children: /* @__PURE__ */ jsxs19("table", { className: "w-full border-collapse text-sm", children: [
3315
- /* @__PURE__ */ jsx29("thead", { children: /* @__PURE__ */ jsxs19("tr", { className: "divide-x divide-border", children: [
3401
+ /* @__PURE__ */ jsx29("thead", { ref: theadRef, children: /* @__PURE__ */ jsxs19("tr", { className: "divide-x divide-border", children: [
3316
3402
  showSelectionColumn && /* @__PURE__ */ jsx29("th", { className: "border-b border-border p-1", style: { width: SELECTION_COLUMN_WIDTH }, children: /* @__PURE__ */ jsx29(
3317
3403
  Checkbox,
3318
3404
  {
@@ -3321,7 +3407,7 @@ function TreeDataGrid({
3321
3407
  onCheckedChange: toggleAllVisibleSelected
3322
3408
  }
3323
3409
  ) }),
3324
- columns.map((column) => /* @__PURE__ */ jsx29(
3410
+ visibleColumns.map((column) => /* @__PURE__ */ jsx29(
3325
3411
  "th",
3326
3412
  {
3327
3413
  style: column.width ? { width: column.width } : void 0,
@@ -3332,7 +3418,45 @@ function TreeDataGrid({
3332
3418
  )),
3333
3419
  showRowActionsColumn && /* @__PURE__ */ jsx29("th", { className: "border-b border-border p-2", "aria-label": "Row actions" })
3334
3420
  ] }) }),
3335
- /* @__PURE__ */ jsx29("tbody", { children: flatRows.length === 0 ? /* @__PURE__ */ jsx29("tr", { children: /* @__PURE__ */ jsx29("td", { colSpan: totalColumnCount, className: "p-4 text-center text-muted-foreground", children: "No results." }) }) : shouldVirtualize ? /* @__PURE__ */ jsxs19(Fragment4, { children: [
3421
+ /* @__PURE__ */ jsx29("tbody", { children: data.length === 0 ? /* @__PURE__ */ jsx29("tr", { children: /* @__PURE__ */ jsx29("td", { colSpan: totalColumnCount, className: "p-4 text-center text-muted-foreground", children: "No results." }) }) : groupedBuckets ? groupedBuckets.map((bucket) => {
3422
+ const groupExpanded = isGroupExpanded(bucket.key);
3423
+ return /* @__PURE__ */ jsxs19(Fragment4, { children: [
3424
+ /* @__PURE__ */ jsx29("tr", { "data-testid": `group-header-${bucket.key}`, children: /* @__PURE__ */ jsx29(
3425
+ "td",
3426
+ {
3427
+ colSpan: totalColumnCount,
3428
+ className: cn(
3429
+ "sticky z-20 border-b p-2 font-medium",
3430
+ zebra ? "bg-muted" : "bg-background"
3431
+ ),
3432
+ style: { top: groupHeaderTop },
3433
+ children: /* @__PURE__ */ jsxs19(
3434
+ "button",
3435
+ {
3436
+ type: "button",
3437
+ className: "flex w-full items-center gap-1 text-left",
3438
+ onClick: () => toggleGroupExpanded(bucket.key),
3439
+ "aria-expanded": groupExpanded,
3440
+ children: [
3441
+ /* @__PURE__ */ jsx29(
3442
+ ChevronRight2,
3443
+ {
3444
+ className: `h-4 w-4 shrink-0 transition-transform${groupExpanded ? " rotate-90" : ""}`,
3445
+ "aria-hidden": true
3446
+ }
3447
+ ),
3448
+ renderGroupHeader ? renderGroupHeader(bucket.key, bucket.roots, groupExpanded) : `${bucket.key} (${bucket.roots.length})`
3449
+ ]
3450
+ }
3451
+ )
3452
+ }
3453
+ ) }),
3454
+ groupExpanded && (() => {
3455
+ const startIndex = groupedRender?.startIndexByBucket.get(bucket.key) ?? 0;
3456
+ return bucket.flatRows.map((flatRow, i) => renderRow(flatRow, startIndex + i));
3457
+ })()
3458
+ ] }, `group-${bucket.key}`);
3459
+ }) : shouldVirtualize ? /* @__PURE__ */ jsxs19(Fragment5, { children: [
3336
3460
  paddingTop > 0 && /* @__PURE__ */ jsx29("tr", { "aria-hidden": "true", children: /* @__PURE__ */ jsx29("td", { colSpan: totalColumnCount, style: { padding: 0, border: "none", height: paddingTop } }) }),
3337
3461
  virtualItems.map(
3338
3462
  (virtualItem) => renderRow(flatRows[virtualItem.index], virtualItem.index, (el) => virtualizer.measureElement(el))
@@ -3399,6 +3523,9 @@ export {
3399
3523
  useDebouncedCallback,
3400
3524
  useEditingState,
3401
3525
  useGridState,
3402
- useTreeState
3526
+ useGroupExpansion,
3527
+ useStickyGroupHeaderTop,
3528
+ useTreeState,
3529
+ useVisibleColumns
3403
3530
  };
3404
3531
  //# sourceMappingURL=index.js.map