@nexub/react 5.3.0 → 5.4.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.3.0",
2
+ "version": "5.4.0",
3
3
  "standard": "WCAG 2.2 Level AA",
4
4
  "method": {
5
5
  "automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark on every pull request; a new violation fails the build.",
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState } from '@tanstack/react-table';
2
+ import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState, type ColumnSizingState, type ColumnOrderState, type ColumnPinningState } from '@tanstack/react-table';
3
3
  declare module '@tanstack/react-table' {
4
4
  interface ColumnMeta<TData extends RowData, TValue> {
5
5
  /** Horizontal alignment of the header and cells. Use 'end' for numbers. @default 'start' */
@@ -9,6 +9,18 @@ declare module '@tanstack/react-table' {
9
9
  /** A DataTable column: TanStack's `ColumnDef`, with `meta.align` for numeric columns. */
10
10
  export type DataTableColumn<TData> = ColumnDef<TData, unknown>;
11
11
  export type DataTableDensity = 'comfortable' | 'compact';
12
+ /** Which rows to render, and the space to leave above and below them. */
13
+ export interface DataTableRowWindow {
14
+ indexes: number[];
15
+ before: number;
16
+ after: number;
17
+ /** Ref for each rendered row, to measure its real height. */
18
+ measure?: (el: HTMLElement | null) => void;
19
+ /** True when rows are windowed: rows get aria-rowindex, the table aria-rowcount. */
20
+ virtual?: boolean;
21
+ }
22
+ /** A hook choosing the rendered rows. DataTableVirtual passes one; the default renders all. */
23
+ export type DataTableRowWindowHook = (count: number, getScrollElement: () => HTMLElement | null) => DataTableRowWindow;
12
24
  export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
13
25
  columns: DataTableColumn<TData>[];
14
26
  /** Names the table for assistive tech (and is shown below it unless `captionHidden`). */
@@ -39,8 +51,23 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
39
51
  viewOptions?: boolean;
40
52
  /** Content at the start of the row above the table (filters, a title). */
41
53
  toolbar?: React.ReactNode;
54
+ /** Drag a header's edge, or focus it and press Arrow keys, to resize columns. */
55
+ enableColumnResizing?: boolean;
56
+ columnSizing?: ColumnSizingState;
57
+ onColumnSizingChange?: (sizing: ColumnSizingState) => void;
58
+ /** Drag headers to reorder columns, or press Alt+Arrow on a focused header. */
59
+ enableColumnReordering?: boolean;
60
+ columnOrder?: ColumnOrderState;
61
+ onColumnOrderChange?: (order: ColumnOrderState) => void;
62
+ /** Columns that stay in view while the rest scroll sideways: `{ left: ['name'], right: ['actions'] }`. */
63
+ columnPinning?: ColumnPinningState;
64
+ onColumnPinningChange?: (pinning: ColumnPinningState) => void;
65
+ /** Cap the table's height. Rows scroll and the header stays in view. */
66
+ maxHeight?: number | string;
67
+ /** Rows to render; set by DataTableVirtual. */
68
+ rowWindow?: DataTableRowWindowHook;
42
69
  pagination?: {
43
70
  pageSize?: number;
44
71
  };
45
72
  }
46
- export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
73
+ export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, rowWindow, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
@@ -14,6 +14,7 @@ import { cn } from "../../utils/cn.js";
14
14
  import { disabledContainer, pressable } from "../../utils/control.js";
15
15
  import { floatingSurface } from "../../utils/surface.js";
16
16
  import { useControllableState } from "../../hooks/useControllableState.js";
17
+ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
17
18
  import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
18
19
  import { Table } from "../Table/Table.js";
19
20
  import { Checkbox } from "../Checkbox/Checkbox.js";
@@ -84,6 +85,23 @@ const TableCell = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
84
85
  }
85
86
  ));
86
87
  TableCell.displayName = "DataTable.TableCell";
88
+ function Spacer({ height }) {
89
+ return /* @__PURE__ */ jsx("tr", { "aria-hidden": "true", className: "border-0", children: /* @__PURE__ */ jsx(
90
+ "td",
91
+ {
92
+ ref: (el) => {
93
+ if (el) el.style.height = px(height);
94
+ },
95
+ className: "p-0"
96
+ }
97
+ ) });
98
+ }
99
+ const useAllRows = (count) => ({
100
+ indexes: Array.from({ length: count }, (_, i) => i),
101
+ before: 0,
102
+ after: 0
103
+ });
104
+ const px = (n) => typeof n === "number" ? `${n}px` : n;
87
105
  const apply = (u, prev) => typeof u === "function" ? u(prev) : u;
88
106
  const columnLabel = (c) => typeof c.columnDef.header === "string" ? c.columnDef.header : c.id;
89
107
  function DataTable({
@@ -103,6 +121,16 @@ function DataTable({
103
121
  onColumnVisibilityChange,
104
122
  viewOptions = false,
105
123
  toolbar,
124
+ enableColumnResizing = false,
125
+ columnSizing: columnSizingProp,
126
+ onColumnSizingChange,
127
+ enableColumnReordering = false,
128
+ columnOrder: columnOrderProp,
129
+ onColumnOrderChange,
130
+ columnPinning: columnPinningProp,
131
+ onColumnPinningChange,
132
+ maxHeight,
133
+ rowWindow,
106
134
  pagination,
107
135
  caption,
108
136
  captionHidden = false,
@@ -112,7 +140,7 @@ function DataTable({
112
140
  ...props
113
141
  }) {
114
142
  const messages = useNexubMessages();
115
- const { locale } = useLocale();
143
+ const { locale, dir } = useLocale();
116
144
  const [sorting, setSorting] = React.useState([]);
117
145
  const [rowSelection = {}, setRowSelection] = useControllableState({
118
146
  value: rowSelectionProp,
@@ -129,11 +157,33 @@ function DataTable({
129
157
  defaultValue: {},
130
158
  onChange: onColumnVisibilityChange
131
159
  });
160
+ const [columnSizing = {}, setColumnSizing] = useControllableState({
161
+ value: columnSizingProp,
162
+ defaultValue: {},
163
+ onChange: onColumnSizingChange
164
+ });
165
+ const [columnOrder = [], setColumnOrder] = useControllableState({
166
+ value: columnOrderProp,
167
+ defaultValue: [],
168
+ onChange: onColumnOrderChange
169
+ });
170
+ const [pinningIn = {}, setColumnPinning] = useControllableState({
171
+ value: columnPinningProp,
172
+ defaultValue: {},
173
+ onChange: onColumnPinningChange
174
+ });
175
+ const [announcement, setAnnouncement] = React.useState("");
176
+ const tableRef = React.useRef(null);
177
+ const dragged = React.useRef(null);
132
178
  const [paginationState, setPaginationState] = React.useState({
133
179
  pageIndex: 0,
134
180
  pageSize: pagination?.pageSize ?? 20
135
181
  });
136
182
  const withSelectColumn = enableRowSelection && selectionColumn;
183
+ const columnPinning = React.useMemo(
184
+ () => withSelectColumn && pinningIn.left?.length && !pinningIn.left.includes("__select") ? { ...pinningIn, left: ["__select", ...pinningIn.left] } : pinningIn,
185
+ [withSelectColumn, pinningIn]
186
+ );
137
187
  const allColumns = React.useMemo(
138
188
  () => withSelectColumn ? [
139
189
  {
@@ -167,9 +217,23 @@ function DataTable({
167
217
  sorting: enableSorting ? sorting : void 0,
168
218
  rowSelection: enableRowSelection ? rowSelection : void 0,
169
219
  pagination: pagination ? paginationState : void 0,
170
- columnVisibility
220
+ columnVisibility,
221
+ columnSizing,
222
+ columnOrder,
223
+ columnPinning
171
224
  }),
172
- [enableSorting, sorting, enableRowSelection, rowSelection, pagination, paginationState, columnVisibility]
225
+ [
226
+ enableSorting,
227
+ sorting,
228
+ enableRowSelection,
229
+ rowSelection,
230
+ pagination,
231
+ paginationState,
232
+ columnVisibility,
233
+ columnSizing,
234
+ columnOrder,
235
+ columnPinning
236
+ ]
173
237
  );
174
238
  const table = useReactTable({
175
239
  data,
@@ -179,6 +243,12 @@ function DataTable({
179
243
  onSortingChange: enableSorting ? setSorting : void 0,
180
244
  onRowSelectionChange: enableRowSelection ? (u) => setRowSelection(apply(u, rowSelection)) : void 0,
181
245
  onColumnVisibilityChange: (u) => setColumnVisibility(apply(u, columnVisibility)),
246
+ onColumnSizingChange: (u) => setColumnSizing(apply(u, columnSizing)),
247
+ onColumnOrderChange: (u) => setColumnOrder(apply(u, columnOrder)),
248
+ onColumnPinningChange: (u) => setColumnPinning(apply(u, columnPinning)),
249
+ enableColumnResizing,
250
+ columnResizeMode: "onChange",
251
+ columnResizeDirection: dir,
182
252
  onPaginationChange: pagination ? setPaginationState : void 0,
183
253
  getCoreRowModel: getCoreRowModel(),
184
254
  getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
@@ -194,6 +264,50 @@ function DataTable({
194
264
  const multiSort = sorting.length > 1;
195
265
  const offerAll = withSelectColumn && !!pagination && table.getIsAllPageRowsSelected() && totalRows > table.getRowModel().rows.length;
196
266
  const clear = () => table.resetRowSelection(true);
267
+ const rows = table.getRowModel().rows;
268
+ const useRowWindow = rowWindow ?? useAllRows;
269
+ const rowWin = useRowWindow(rows.length, () => tableRef.current?.parentElement ?? null);
270
+ const pinned = (columnPinning.left?.length ?? 0) + (columnPinning.right?.length ?? 0) > 0;
271
+ useIsomorphicLayoutEffect(() => {
272
+ const t = tableRef.current;
273
+ if (!t) return;
274
+ t.style.width = enableColumnResizing ? px(table.getTotalSize()) : "";
275
+ const scroller = t.parentElement;
276
+ if (scroller) scroller.style.maxHeight = maxHeight == null ? "" : px(maxHeight);
277
+ });
278
+ const placeCell = (column) => (el) => {
279
+ if (!el) return;
280
+ el.style.width = enableColumnResizing ? px(column.getSize()) : "";
281
+ const side = column.getIsPinned();
282
+ el.style.insetInlineStart = side === "left" ? px(column.getStart("left")) : "";
283
+ el.style.insetInlineEnd = side === "right" ? px(column.getAfter("right")) : "";
284
+ };
285
+ const pinClass = (column) => {
286
+ const side = column.getIsPinned();
287
+ if (!side) return void 0;
288
+ return cn(
289
+ "sticky z-[1] bg-[var(--color-surface)]",
290
+ "[tr:hover>&]:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,var(--color-surface))]",
291
+ "[tr[data-state=selected]>&]:bg-[color-mix(in_srgb,var(--color-primary)_10%,var(--color-surface))]",
292
+ side === "left" && column.getIsLastColumn("left") && "border-e border-[var(--color-outline-variant)]",
293
+ side === "right" && column.getIsFirstColumn("right") && "border-s border-[var(--color-outline-variant)]"
294
+ );
295
+ };
296
+ const moveColumn = (id, to) => {
297
+ const ids = table.getAllLeafColumns().map((c) => c.id);
298
+ const order = (columnOrder.length ? columnOrder : ids).filter((c) => c !== "__select" && ids.includes(c));
299
+ for (const c of ids) if (c !== "__select" && !order.includes(c)) order.push(c);
300
+ const from = order.indexOf(id);
301
+ const target = typeof to === "number" ? from + to : order.indexOf(to);
302
+ if (from < 0 || target < 0 || target >= order.length || target === from) return;
303
+ order.splice(from, 1);
304
+ order.splice(target, 0, id);
305
+ setColumnOrder(withSelectColumn ? ["__select", ...order] : order);
306
+ if (typeof to === "number") {
307
+ const col = table.getColumn(id);
308
+ setAnnouncement(messages.columnMoved(col ? columnLabel(col) : id, target + 1, order.length));
309
+ }
310
+ };
197
311
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), "data-density": density, ...props, children: [
198
312
  (toolbar != null || viewOptions) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
199
313
  toolbar,
@@ -230,61 +344,151 @@ function DataTable({
230
344
  /* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(selectedRows.length)) }),
231
345
  /* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: () => table.toggleAllRowsSelected(true), children: messages.selectAllRows(fmt(totalRows)) })
232
346
  ] }) }),
233
- /* @__PURE__ */ jsxs(TableRoot, { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: [
234
- caption != null && /* @__PURE__ */ jsx(Table.Caption, { className: cn(captionHidden && "sr-only"), children: caption }),
235
- /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: headerGroup.headers.map((header) => {
236
- const align = header.column.columnDef.meta?.align ?? "start";
237
- return /* @__PURE__ */ jsx(
238
- TableHead,
239
- {
240
- className: cn(alignText[align], compact && "h-8", header.column.id === "__select" && "w-10"),
241
- "aria-sort": enableSorting && header.column.getCanSort() ? header.column.getIsSorted() === "asc" ? "ascending" : header.column.getIsSorted() === "desc" ? "descending" : "none" : void 0,
242
- children: header.isPlaceholder ? null : enableSorting && header.column.getCanSort() ? /* @__PURE__ */ jsxs(
243
- "button",
347
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "sr-only", children: announcement }),
348
+ /* @__PURE__ */ jsxs(
349
+ TableRoot,
350
+ {
351
+ ref: tableRef,
352
+ "aria-label": ariaLabel,
353
+ "aria-labelledby": ariaLabelledBy,
354
+ "aria-rowcount": rowWin.virtual ? rows.length + 1 : void 0,
355
+ className: cn(enableColumnResizing && "table-fixed"),
356
+ children: [
357
+ caption != null && /* @__PURE__ */ jsx(Table.Caption, { className: cn(captionHidden && "sr-only"), children: caption }),
358
+ /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: headerGroup.headers.map((header) => {
359
+ const align = header.column.columnDef.meta?.align ?? "start";
360
+ const col = header.column;
361
+ const reorderable = enableColumnReordering && col.id !== "__select" && !col.getIsPinned();
362
+ const sortable = enableSorting && col.getCanSort();
363
+ return /* @__PURE__ */ jsxs(
364
+ TableHead,
244
365
  {
245
- type: "button",
246
- onClick: header.column.getToggleSortingHandler(),
366
+ ref: placeCell(col),
247
367
  className: cn(
248
- "flex w-full items-center gap-1 hover:text-[var(--color-on-surface)]",
249
- alignJustify[align]
368
+ alignText[align],
369
+ compact && "h-8",
370
+ col.id === "__select" && "w-10",
371
+ // Anchors the resize handle; sticky (below) wins when set and anchors it too.
372
+ (enableColumnResizing || reorderable) && "relative",
373
+ maxHeight != null && "sticky top-0 z-[2] bg-[var(--color-surface)]",
374
+ pinClass(col),
375
+ maxHeight != null && col.getIsPinned() && "z-[3] [tr:hover>&]:bg-[var(--color-surface)]",
376
+ reorderable && "cursor-grab"
250
377
  ),
378
+ tabIndex: reorderable && !sortable ? 0 : void 0,
379
+ draggable: reorderable || void 0,
380
+ onDragStart: reorderable ? () => dragged.current = col.id : void 0,
381
+ onDragOver: reorderable ? (e) => e.preventDefault() : void 0,
382
+ onDrop: reorderable ? (e) => {
383
+ e.preventDefault();
384
+ if (dragged.current) moveColumn(dragged.current, col.id);
385
+ dragged.current = null;
386
+ } : void 0,
387
+ onKeyDown: reorderable ? (e) => {
388
+ if (!e.altKey || e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
389
+ e.preventDefault();
390
+ const forward = e.key === "ArrowRight" === (dir !== "rtl");
391
+ moveColumn(col.id, forward ? 1 : -1);
392
+ } : void 0,
393
+ "aria-sort": enableSorting && header.column.getCanSort() ? header.column.getIsSorted() === "asc" ? "ascending" : header.column.getIsSorted() === "desc" ? "descending" : "none" : void 0,
251
394
  children: [
252
- flexRender(header.column.columnDef.header, header.getContext()),
253
- multiSort && header.column.getIsSorted() && /* @__PURE__ */ jsxs("span", { className: "text-[10px] tabular-nums leading-none", children: [
254
- /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: header.column.getSortIndex() + 1 }),
255
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: messages.sortOrder(header.column.getSortIndex() + 1) })
256
- ] }),
257
- header.column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { size: "xs", "aria-hidden": "true" }) : header.column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { size: "xs", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { size: "xs", className: "opacity-40", "aria-hidden": "true" })
395
+ header.isPlaceholder ? null : enableSorting && header.column.getCanSort() ? /* @__PURE__ */ jsxs(
396
+ "button",
397
+ {
398
+ type: "button",
399
+ onClick: header.column.getToggleSortingHandler(),
400
+ className: cn(
401
+ "flex w-full items-center gap-1 hover:text-[var(--color-on-surface)]",
402
+ alignJustify[align]
403
+ ),
404
+ children: [
405
+ flexRender(header.column.columnDef.header, header.getContext()),
406
+ multiSort && header.column.getIsSorted() && /* @__PURE__ */ jsxs("span", { className: "text-[10px] tabular-nums leading-none", children: [
407
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: header.column.getSortIndex() + 1 }),
408
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: messages.sortOrder(header.column.getSortIndex() + 1) })
409
+ ] }),
410
+ header.column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { size: "xs", "aria-hidden": "true" }) : header.column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { size: "xs", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { size: "xs", className: "opacity-40", "aria-hidden": "true" })
411
+ ]
412
+ }
413
+ ) : flexRender(header.column.columnDef.header, header.getContext()),
414
+ enableColumnResizing && col.getCanResize() && /* @__PURE__ */ jsx(
415
+ "div",
416
+ {
417
+ role: "separator",
418
+ "aria-orientation": "vertical",
419
+ "aria-label": messages.resizeColumn(columnLabel(col)),
420
+ "aria-valuenow": header.getSize(),
421
+ "aria-valuemin": col.columnDef.minSize ?? 20,
422
+ "aria-valuemax": col.columnDef.maxSize ?? 1e3,
423
+ tabIndex: 0,
424
+ draggable: false,
425
+ onMouseDown: header.getResizeHandler(),
426
+ onTouchStart: header.getResizeHandler(),
427
+ onDoubleClick: () => col.resetSize(),
428
+ onKeyDown: (e) => {
429
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
430
+ e.preventDefault();
431
+ e.stopPropagation();
432
+ const grow = e.key === "ArrowRight" === (dir !== "rtl");
433
+ const min = col.columnDef.minSize ?? 20;
434
+ const max = col.columnDef.maxSize ?? 1e3;
435
+ const size = Math.min(max, Math.max(min, header.getSize() + (grow ? 16 : -16)));
436
+ table.setColumnSizing((s) => ({ ...s, [col.id]: size }));
437
+ },
438
+ className: cn(
439
+ "absolute inset-y-0 end-0 z-[1] w-2 cursor-col-resize touch-none select-none outline-none",
440
+ "after:absolute after:inset-y-2 after:end-0 after:w-px after:bg-[var(--color-outline-variant)]",
441
+ "hover:after:w-0.5 hover:after:bg-[var(--color-primary)] focus-visible:after:w-0.5 focus-visible:after:bg-[var(--color-primary)]",
442
+ col.getIsResizing() && "after:w-0.5 after:bg-[var(--color-primary)]"
443
+ )
444
+ }
445
+ )
258
446
  ]
259
- }
260
- ) : flexRender(header.column.columnDef.header, header.getContext())
261
- },
262
- header.id
263
- );
264
- }) }, headerGroup.id)) }),
265
- /* @__PURE__ */ jsx(TableBody, { children: table.getRowModel().rows.length ? table.getRowModel().rows.map((row) => /* @__PURE__ */ jsx(
266
- TableRow,
267
- {
268
- "data-state": enableRowSelection && row.getIsSelected() ? "selected" : void 0,
269
- children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(
447
+ },
448
+ header.id
449
+ );
450
+ }) }, headerGroup.id)) }),
451
+ /* @__PURE__ */ jsx(TableBody, { children: rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
452
+ rowWin.before > 0 && /* @__PURE__ */ jsx(Spacer, { height: rowWin.before }),
453
+ rowWin.indexes.map((i) => {
454
+ const row = rows[i];
455
+ return /* @__PURE__ */ jsx(
456
+ TableRow,
457
+ {
458
+ ref: rowWin.measure,
459
+ "data-index": i,
460
+ "aria-rowindex": rowWin.virtual ? i + 2 : void 0,
461
+ "data-state": enableRowSelection && row.getIsSelected() ? "selected" : void 0,
462
+ children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(
463
+ TableCell,
464
+ {
465
+ ref: pinned || enableColumnResizing ? placeCell(cell.column) : void 0,
466
+ className: cn(
467
+ alignText[cell.column.columnDef.meta?.align ?? "start"],
468
+ compact && "py-1.5",
469
+ pinClass(cell.column),
470
+ enableColumnResizing && "truncate"
471
+ ),
472
+ children: flexRender(cell.column.columnDef.cell, cell.getContext())
473
+ },
474
+ cell.id
475
+ ))
476
+ },
477
+ row.id
478
+ );
479
+ }),
480
+ rowWin.after > 0 && /* @__PURE__ */ jsx(Spacer, { height: rowWin.after })
481
+ ] }) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
270
482
  TableCell,
271
483
  {
272
- className: cn(alignText[cell.column.columnDef.meta?.align ?? "start"], compact && "py-1.5"),
273
- children: flexRender(cell.column.columnDef.cell, cell.getContext())
274
- },
275
- cell.id
276
- ))
277
- },
278
- row.id
279
- )) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
280
- TableCell,
281
- {
282
- colSpan: table.getVisibleLeafColumns().length,
283
- className: "h-24 text-center text-[var(--color-on-surface-muted)]",
284
- children: messages.noResults
285
- }
286
- ) }) })
287
- ] }),
484
+ colSpan: table.getVisibleLeafColumns().length,
485
+ className: "h-24 text-center text-[var(--color-on-surface-muted)]",
486
+ children: messages.noResults
487
+ }
488
+ ) }) })
489
+ ]
490
+ }
491
+ ),
288
492
  pagination && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 text-sm text-[var(--color-on-surface-muted)]", children: [
289
493
  /* @__PURE__ */ jsx("span", { children: messages.pageOf(table.getState().pagination.pageIndex + 1, table.getPageCount()) }),
290
494
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
@@ -1,2 +1,2 @@
1
1
  export { DataTable } from './DataTable.js';
2
- export type { DataTableProps, DataTableColumn } from './DataTable.js';
2
+ export type { DataTableProps, DataTableColumn, DataTableDensity, DataTableRowWindow, DataTableRowWindowHook } from './DataTable.js';
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { type DataTableProps } from '../DataTable/DataTable.js';
3
+ export interface DataTableVirtualProps<TData> extends Omit<DataTableProps<TData>, 'pagination' | 'rowWindow'> {
4
+ /** Height of the scrolling area. Required: rows are windowed within it. */
5
+ maxHeight: number | string;
6
+ /** Row height before measuring, in px. Default 49, or 37 when compact. */
7
+ estimateRowHeight?: number;
8
+ /** Rows rendered beyond the visible ones, each way. Default 8. */
9
+ overscan?: number;
10
+ }
11
+ /**
12
+ * DataTable for thousands of rows: only the rows in view (plus `overscan`)
13
+ * are in the DOM. Rows carry aria-rowindex and the table aria-rowcount, so
14
+ * screen readers still report the full size. Needs `@tanstack/react-virtual`.
15
+ */
16
+ export declare function DataTableVirtual<TData>({ estimateRowHeight, overscan, ...props }: DataTableVirtualProps<TData>): React.JSX.Element;
17
+ export declare namespace DataTableVirtual {
18
+ var displayName: string;
19
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useVirtualizer } from "@tanstack/react-virtual";
5
+ import { DataTable } from "../DataTable/DataTable.js";
6
+ const Options = React.createContext({ estimate: 49, overscan: 8, height: 600 });
7
+ const useVirtualRows = (count, getScrollElement) => {
8
+ const { estimate, overscan, height } = React.useContext(Options);
9
+ const virtualizer = useVirtualizer({
10
+ count,
11
+ getScrollElement,
12
+ estimateSize: () => estimate,
13
+ overscan,
14
+ // First render (and the server) assumes the full height, so rows show before measuring.
15
+ initialRect: { width: 0, height }
16
+ });
17
+ const items = virtualizer.getVirtualItems();
18
+ const last = items[items.length - 1];
19
+ return {
20
+ indexes: items.map((i) => i.index),
21
+ before: items[0]?.start ?? 0,
22
+ after: last ? virtualizer.getTotalSize() - last.end : 0,
23
+ measure: virtualizer.measureElement,
24
+ virtual: true
25
+ };
26
+ };
27
+ function DataTableVirtual({ estimateRowHeight, overscan = 8, ...props }) {
28
+ const compact = (props.density ?? props.defaultDensity) === "compact";
29
+ const estimate = estimateRowHeight ?? (compact ? 37 : 49);
30
+ const height = typeof props.maxHeight === "number" ? props.maxHeight : 600;
31
+ const options = React.useMemo(() => ({ estimate, overscan, height }), [estimate, overscan, height]);
32
+ return /* @__PURE__ */ jsx(Options.Provider, { value: options, children: /* @__PURE__ */ jsx(DataTable, { ...props, rowWindow: useVirtualRows }) });
33
+ }
34
+ DataTableVirtual.displayName = "DataTableVirtual";
35
+ export {
36
+ DataTableVirtual
37
+ };
@@ -0,0 +1,2 @@
1
+ export { DataTableVirtual } from './DataTableVirtual.js';
2
+ export type { DataTableVirtualProps } from './DataTableVirtual.js';
@@ -0,0 +1,4 @@
1
+ import { DataTableVirtual } from "./DataTableVirtual.js";
2
+ export {
3
+ DataTableVirtual
4
+ };
package/dist/index.d.ts CHANGED
@@ -200,7 +200,7 @@ export { CommandPalette } from './components/CommandPalette/index.js';
200
200
  export type { CommandPaletteRootProps } from './components/CommandPalette/index.js';
201
201
  export { Table } from './components/Table/index.js';
202
202
  export { DataTable } from './components/DataTable/index.js';
203
- export type { DataTableProps } from './components/DataTable/index.js';
203
+ export type { DataTableProps, DataTableDensity } from './components/DataTable/index.js';
204
204
  export { EmptyState } from './components/EmptyState/index.js';
205
205
  export { ErrorState } from './components/ErrorState/index.js';
206
206
  export type { ErrorStateProps } from './components/ErrorState/index.js';