@bmsuisse/datagrid 0.28.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.d.ts CHANGED
@@ -1103,10 +1103,29 @@ interface DataGridProps<TRow> {
1103
1103
  * split across pages. Omit entirely to disable — no grouping, no header
1104
1104
  * rows, unchanged default rendering.
1105
1105
  *
1106
- * Not supported together with `virtualize` yet — when both are set,
1107
- * virtualization is silently disabled and the grid renders as a plain,
1108
- * fully-rendered (but still grouped) `<table>` instead. See AGENTS.md's
1109
- * "Known limitations."
1106
+ * Composes with `virtualize` — header rows and member rows are flattened
1107
+ * into one virtualizable list together, so a large grouped dataset windows
1108
+ * the same way an ungrouped one does. `virtualize.threshold` still gates
1109
+ * on the real row count (`groupBy`'s synthetic header entries never push a
1110
+ * grid over a threshold its actual data didn't cross), and
1111
+ * `virtualize.onEndReached`'s own "won't fire again for the same data"
1112
+ * dedup is keyed off that same real row count too — a pure expand/collapse
1113
+ * click, with no new data loaded, never re-fires it.
1114
+ *
1115
+ * Two gaps this doesn't close, both pre-existing/out of scope for this
1116
+ * pass rather than introduced by composing with `virtualize`: `zebra`'s
1117
+ * odd/even striping is keyed off a row's position in the flat, ungrouped
1118
+ * row list, not its rendered position within its own bucket, so grouped
1119
+ * rows can stripe unevenly (`groupBy` alone already had this, virtualizing
1120
+ * doesn't fix or worsen it); and a sticky group header stays pinned below
1121
+ * the real `<thead>` only while its own `<tbody>` (or one of its member
1122
+ * rows') is actually mounted in the virtualized window — scrolling deep
1123
+ * into one bucket whose row count itself exceeds `virtualize`'s overscan
1124
+ * will eventually unmount that bucket's header and let it scroll away
1125
+ * rather than staying pinned, since `position: sticky` needs the element
1126
+ * mounted to stick at all. Both are harmless for the common case (buckets
1127
+ * no bigger than a couple dozen rows); see AGENTS.md's "Known limitations"
1128
+ * for the full writeup.
1110
1129
  */
1111
1130
  groupBy?: (row: TRow) => string;
1112
1131
  /** Customizes a group-header row's content. Defaults to `` `${key} (${rows.length})` ``. */
package/dist/index.js CHANGED
@@ -2551,15 +2551,28 @@ function DataGrid({
2551
2551
  return map;
2552
2552
  }, [visibleColumns, enableColumnResizing, leftPinnedOffsets, rightPinnedOffsets, columnSizing]);
2553
2553
  const tableRows = table.getRowModel().rows;
2554
- const shouldVirtualize = Boolean(virtualize) && !groupBy && tableRows.length > (virtualize?.threshold ?? 100);
2555
2554
  const groupedBuckets = useMemo7(
2556
2555
  () => groupBy ? groupRows(tableRows, (row) => groupBy(row.original)) : void 0,
2557
2556
  [tableRows, groupBy]
2558
2557
  );
2559
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]);
2560
2573
  const scrollRef = useRef8(null);
2561
2574
  const virtualizer = useVirtualizer({
2562
- count: tableRows.length,
2575
+ count: flatItems.length,
2563
2576
  getScrollElement: () => scrollRef.current,
2564
2577
  estimateSize: () => virtualize?.estimatedRowHeight ?? 40,
2565
2578
  overscan: virtualize?.overscan ?? 10,
@@ -2572,18 +2585,18 @@ function DataGrid({
2572
2585
  const notifiedForLengthRef = useRef8(-1);
2573
2586
  useEffect9(() => {
2574
2587
  if (!virtualize?.onEndReached) return;
2575
- if (lastVisibleIndex < 0 || lastVisibleIndex < tableRows.length - 1) return;
2588
+ if (lastVisibleIndex < 0 || lastVisibleIndex < flatItems.length - 1) return;
2576
2589
  if (virtualize.hasMore === false) return;
2577
2590
  if (notifiedForLengthRef.current === tableRows.length) return;
2578
2591
  notifiedForLengthRef.current = tableRows.length;
2579
2592
  virtualize.onEndReached();
2580
- }, [lastVisibleIndex, tableRows.length, virtualize]);
2581
- function renderRow(row, measureRef) {
2593
+ }, [lastVisibleIndex, flatItems.length, tableRows.length, virtualize]);
2594
+ function renderRow(row, measureRef, dataIndex = row.index) {
2582
2595
  const isExpanded = expandedIds.has(row.id);
2583
2596
  const rowProps = getRowProps?.(row.original);
2584
2597
  const rowTestId = getRowTestId?.(row.original) ?? `row-${row.id}`;
2585
2598
  const isOddRow = zebra && row.index % 2 === 1;
2586
- return /* @__PURE__ */ jsxs18("tbody", { ref: measureRef, "data-index": row.index, children: [
2599
+ return /* @__PURE__ */ jsxs18("tbody", { ref: measureRef, "data-index": dataIndex, children: [
2587
2600
  /* @__PURE__ */ jsxs18(
2588
2601
  "tr",
2589
2602
  {
@@ -2654,43 +2667,48 @@ function DataGrid({
2654
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) }) })
2655
2668
  ] }, row.id);
2656
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
+ }
2657
2700
  function renderGroupedBucket(bucket) {
2658
2701
  const expanded = isGroupExpanded(bucket.key);
2659
2702
  const originalRows = bucket.items.map((row) => row.original);
2660
2703
  return /* @__PURE__ */ jsxs18(Fragment2, { children: [
2661
- /* @__PURE__ */ jsx28("tbody", { children: /* @__PURE__ */ jsx28("tr", { "data-testid": `group-header-${bucket.key}`, children: /* @__PURE__ */ jsx28(
2662
- "td",
2663
- {
2664
- colSpan: totalColumnCount,
2665
- className: cn(
2666
- "sticky z-20 border-b p-2 font-medium",
2667
- zebra ? "bg-muted" : "bg-background"
2668
- ),
2669
- style: { top: groupHeaderTop },
2670
- children: /* @__PURE__ */ jsxs18(
2671
- "button",
2672
- {
2673
- type: "button",
2674
- className: "flex w-full items-center gap-1 text-left",
2675
- onClick: () => toggleGroupExpanded(bucket.key),
2676
- "aria-expanded": expanded,
2677
- children: [
2678
- /* @__PURE__ */ jsx28(
2679
- ChevronRight,
2680
- {
2681
- className: `h-4 w-4 shrink-0 transition-transform${expanded ? " rotate-90" : ""}`,
2682
- "aria-hidden": true
2683
- }
2684
- ),
2685
- renderGroupHeader ? renderGroupHeader(bucket.key, originalRows, expanded) : `${bucket.key} (${originalRows.length})`
2686
- ]
2687
- }
2688
- )
2689
- }
2690
- ) }) }),
2704
+ renderGroupHeaderTbody(bucket.key, originalRows),
2691
2705
  expanded && bucket.items.map((row) => renderRow(row))
2692
2706
  ] }, `group-${bucket.key}`);
2693
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
+ }
2694
2712
  return (
2695
2713
  // `h-full`/`min-h-0` here and on the two wrappers below are no-ops unless
2696
2714
  // a consumer itself gives <DataGrid> a bounded height (e.g. wraps it in
@@ -2912,7 +2930,11 @@ function DataGrid({
2912
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: [
2913
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 } }) }) }),
2914
2932
  virtualItems.map(
2915
- (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
+ )
2916
2938
  ),
2917
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 } }) }) })
2918
2940
  ] }) : groupedBuckets ? groupedBuckets.map((bucket) => renderGroupedBucket(bucket)) : tableRows.map((row) => renderRow(row))