@nexub/react 5.2.0 → 5.3.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.2.0",
2
+ "version": "5.3.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 RowData } from '@tanstack/react-table';
2
+ import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState } 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' */
@@ -8,6 +8,7 @@ declare module '@tanstack/react-table' {
8
8
  }
9
9
  /** A DataTable column: TanStack's `ColumnDef`, with `meta.align` for numeric columns. */
10
10
  export type DataTableColumn<TData> = ColumnDef<TData, unknown>;
11
+ export type DataTableDensity = 'comfortable' | 'compact';
11
12
  export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
12
13
  columns: DataTableColumn<TData>[];
13
14
  /** Names the table for assistive tech (and is shown below it unless `captionHidden`). */
@@ -17,8 +18,29 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
17
18
  data: TData[];
18
19
  enableSorting?: boolean;
19
20
  enableRowSelection?: boolean;
21
+ /**
22
+ * With `enableRowSelection`: a checkbox column with select-page in its
23
+ * header, and "Select all N" across pages. On by default from 6.0.
24
+ */
25
+ selectionColumn?: boolean;
26
+ /** Controlled selection, keyed by row id (see `getRowId`). */
27
+ rowSelection?: RowSelectionState;
28
+ onRowSelectionChange?: (selection: RowSelectionState) => void;
29
+ /** Stable row ids for selection; defaults to the row index. */
30
+ getRowId?: (row: TData, index: number) => string;
31
+ /** A floating bar while rows are selected: render your actions for the selected rows. */
32
+ bulkActions?: (rows: TData[], clear: () => void) => React.ReactNode;
33
+ density?: DataTableDensity;
34
+ defaultDensity?: DataTableDensity;
35
+ onDensityChange?: (density: DataTableDensity) => void;
36
+ columnVisibility?: VisibilityState;
37
+ onColumnVisibilityChange?: (visibility: VisibilityState) => void;
38
+ /** A "View" menu above the table: density and which columns show. */
39
+ viewOptions?: boolean;
40
+ /** Content at the start of the row above the table (filters, a title). */
41
+ toolbar?: React.ReactNode;
20
42
  pagination?: {
21
43
  pageSize?: number;
22
44
  };
23
45
  }
24
- export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
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;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import {
5
5
  useReactTable,
@@ -9,11 +9,16 @@ import {
9
9
  getPaginationRowModel,
10
10
  flexRender
11
11
  } from "@tanstack/react-table";
12
- import { ArrowUp, ArrowDown, ChevronsUpDown } from "@nexub/icons";
12
+ import { ArrowUp, ArrowDown, ChevronsUpDown, SlidersHorizontal, X } from "@nexub/icons";
13
13
  import { cn } from "../../utils/cn.js";
14
14
  import { disabledContainer, pressable } from "../../utils/control.js";
15
- import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
15
+ import { floatingSurface } from "../../utils/surface.js";
16
+ import { useControllableState } from "../../hooks/useControllableState.js";
17
+ import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
16
18
  import { Table } from "../Table/Table.js";
19
+ import { Checkbox } from "../Checkbox/Checkbox.js";
20
+ import { Button } from "../Button/Button.js";
21
+ import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
17
22
  const alignText = {
18
23
  start: "text-start",
19
24
  end: "text-end",
@@ -47,7 +52,7 @@ const TableRow = React.forwardRef(
47
52
  {
48
53
  ref,
49
54
  className: cn(
50
- "border-b border-[var(--color-outline-variant)] transition-colors hover:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)] data-[state=selected]:bg-[var(--color-surface-container-low)]",
55
+ "border-b border-[var(--color-outline-variant)] transition-colors hover:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)] data-[state=selected]:bg-[color-mix(in_srgb,var(--color-primary)_10%,transparent)]",
51
56
  className
52
57
  ),
53
58
  ...props
@@ -79,11 +84,25 @@ const TableCell = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
79
84
  }
80
85
  ));
81
86
  TableCell.displayName = "DataTable.TableCell";
87
+ const apply = (u, prev) => typeof u === "function" ? u(prev) : u;
88
+ const columnLabel = (c) => typeof c.columnDef.header === "string" ? c.columnDef.header : c.id;
82
89
  function DataTable({
83
90
  columns,
84
91
  data,
85
92
  enableSorting = false,
86
93
  enableRowSelection = false,
94
+ selectionColumn = false,
95
+ rowSelection: rowSelectionProp,
96
+ onRowSelectionChange,
97
+ getRowId,
98
+ bulkActions,
99
+ density: densityProp,
100
+ defaultDensity = "comfortable",
101
+ onDensityChange,
102
+ columnVisibility: columnVisibilityProp,
103
+ onColumnVisibilityChange,
104
+ viewOptions = false,
105
+ toolbar,
87
106
  pagination,
88
107
  caption,
89
108
  captionHidden = false,
@@ -93,26 +112,73 @@ function DataTable({
93
112
  ...props
94
113
  }) {
95
114
  const messages = useNexubMessages();
115
+ const { locale } = useLocale();
96
116
  const [sorting, setSorting] = React.useState([]);
97
- const [rowSelection, setRowSelection] = React.useState({});
117
+ const [rowSelection = {}, setRowSelection] = useControllableState({
118
+ value: rowSelectionProp,
119
+ defaultValue: {},
120
+ onChange: onRowSelectionChange
121
+ });
122
+ const [density = "comfortable", setDensity] = useControllableState({
123
+ value: densityProp,
124
+ defaultValue: defaultDensity,
125
+ onChange: onDensityChange
126
+ });
127
+ const [columnVisibility = {}, setColumnVisibility] = useControllableState({
128
+ value: columnVisibilityProp,
129
+ defaultValue: {},
130
+ onChange: onColumnVisibilityChange
131
+ });
98
132
  const [paginationState, setPaginationState] = React.useState({
99
133
  pageIndex: 0,
100
134
  pageSize: pagination?.pageSize ?? 20
101
135
  });
136
+ const withSelectColumn = enableRowSelection && selectionColumn;
137
+ const allColumns = React.useMemo(
138
+ () => withSelectColumn ? [
139
+ {
140
+ id: "__select",
141
+ enableSorting: false,
142
+ enableHiding: false,
143
+ header: ({ table: table2 }) => /* @__PURE__ */ jsx(
144
+ Checkbox,
145
+ {
146
+ "aria-label": messages.selectAllOnPage,
147
+ checked: table2.getIsAllPageRowsSelected() ? true : table2.getIsSomePageRowsSelected() ? "indeterminate" : false,
148
+ onCheckedChange: (v) => table2.toggleAllPageRowsSelected(v === true)
149
+ }
150
+ ),
151
+ cell: ({ row }) => /* @__PURE__ */ jsx(
152
+ Checkbox,
153
+ {
154
+ "aria-label": messages.selectRow,
155
+ checked: row.getIsSelected(),
156
+ disabled: !row.getCanSelect(),
157
+ onCheckedChange: (v) => row.toggleSelected(v === true)
158
+ }
159
+ )
160
+ },
161
+ ...columns
162
+ ] : columns,
163
+ [withSelectColumn, columns, messages.selectAllOnPage, messages.selectRow]
164
+ );
102
165
  const tableState = React.useMemo(
103
166
  () => ({
104
167
  sorting: enableSorting ? sorting : void 0,
105
168
  rowSelection: enableRowSelection ? rowSelection : void 0,
106
- pagination: pagination ? paginationState : void 0
169
+ pagination: pagination ? paginationState : void 0,
170
+ columnVisibility
107
171
  }),
108
- [enableSorting, sorting, enableRowSelection, rowSelection, pagination, paginationState]
172
+ [enableSorting, sorting, enableRowSelection, rowSelection, pagination, paginationState, columnVisibility]
109
173
  );
110
174
  const table = useReactTable({
111
175
  data,
112
- columns,
176
+ columns: allColumns,
113
177
  state: tableState,
178
+ getRowId,
114
179
  onSortingChange: enableSorting ? setSorting : void 0,
115
- onRowSelectionChange: enableRowSelection ? setRowSelection : void 0,
180
+ onRowSelectionChange: enableRowSelection ? (u) => setRowSelection(apply(u, rowSelection)) : void 0,
181
+ onColumnVisibilityChange: (u) => setColumnVisibility(apply(u, columnVisibility)),
116
182
  onPaginationChange: pagination ? setPaginationState : void 0,
117
183
  getCoreRowModel: getCoreRowModel(),
118
184
  getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
@@ -121,7 +187,49 @@ function DataTable({
121
187
  enableRowSelection,
122
188
  manualPagination: false
123
189
  });
124
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), ...props, children: [
190
+ const compact = density === "compact";
191
+ const selectedRows = enableRowSelection ? table.getSelectedRowModel().rows : [];
192
+ const totalRows = table.getCoreRowModel().rows.length;
193
+ const fmt = (n) => new Intl.NumberFormat(locale).format(n);
194
+ const multiSort = sorting.length > 1;
195
+ const offerAll = withSelectColumn && !!pagination && table.getIsAllPageRowsSelected() && totalRows > table.getRowModel().rows.length;
196
+ const clear = () => table.resetRowSelection(true);
197
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), "data-density": density, ...props, children: [
198
+ (toolbar != null || viewOptions) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
199
+ toolbar,
200
+ viewOptions && /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
201
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ms-auto", children: [
202
+ /* @__PURE__ */ jsx(SlidersHorizontal, { size: "sm", "aria-hidden": "true" }),
203
+ messages.view
204
+ ] }) }),
205
+ /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
206
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.density }),
207
+ /* @__PURE__ */ jsxs(DropdownMenu.RadioGroup, { value: density, onValueChange: (v) => setDensity(v), children: [
208
+ /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "comfortable", children: messages.comfortable }),
209
+ /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "compact", children: messages.compact })
210
+ ] }),
211
+ /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
212
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.columns }),
213
+ table.getAllLeafColumns().filter((c) => c.getCanHide()).map((c) => /* @__PURE__ */ jsx(
214
+ DropdownMenu.CheckboxItem,
215
+ {
216
+ checked: c.getIsVisible(),
217
+ onCheckedChange: (v) => c.toggleVisibility(v === true),
218
+ onSelect: (e) => e.preventDefault(),
219
+ children: columnLabel(c)
220
+ },
221
+ c.id
222
+ ))
223
+ ] })
224
+ ] })
225
+ ] }),
226
+ offerAll && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 rounded-shape-container bg-[var(--color-surface-container-low)] px-4 py-2 text-body-sm", children: table.getIsAllRowsSelected() ? /* @__PURE__ */ jsxs(Fragment, { children: [
227
+ /* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(totalRows)) }),
228
+ /* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: clear, children: messages.clearSelection })
229
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
230
+ /* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(selectedRows.length)) }),
231
+ /* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: () => table.toggleAllRowsSelected(true), children: messages.selectAllRows(fmt(totalRows)) })
232
+ ] }) }),
125
233
  /* @__PURE__ */ jsxs(TableRoot, { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: [
126
234
  caption != null && /* @__PURE__ */ jsx(Table.Caption, { className: cn(captionHidden && "sr-only"), children: caption }),
127
235
  /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: headerGroup.headers.map((header) => {
@@ -129,7 +237,7 @@ function DataTable({
129
237
  return /* @__PURE__ */ jsx(
130
238
  TableHead,
131
239
  {
132
- className: alignText[align],
240
+ className: cn(alignText[align], compact && "h-8", header.column.id === "__select" && "w-10"),
133
241
  "aria-sort": enableSorting && header.column.getCanSort() ? header.column.getIsSorted() === "asc" ? "ascending" : header.column.getIsSorted() === "desc" ? "descending" : "none" : void 0,
134
242
  children: header.isPlaceholder ? null : enableSorting && header.column.getCanSort() ? /* @__PURE__ */ jsxs(
135
243
  "button",
@@ -142,6 +250,10 @@ function DataTable({
142
250
  ),
143
251
  children: [
144
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
+ ] }),
145
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" })
146
258
  ]
147
259
  }
@@ -157,7 +269,7 @@ function DataTable({
157
269
  children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(
158
270
  TableCell,
159
271
  {
160
- className: alignText[cell.column.columnDef.meta?.align ?? "start"],
272
+ className: cn(alignText[cell.column.columnDef.meta?.align ?? "start"], compact && "py-1.5"),
161
273
  children: flexRender(cell.column.columnDef.cell, cell.getContext())
162
274
  },
163
275
  cell.id
@@ -167,7 +279,7 @@ function DataTable({
167
279
  )) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
168
280
  TableCell,
169
281
  {
170
- colSpan: columns.length,
282
+ colSpan: table.getVisibleLeafColumns().length,
171
283
  className: "h-24 text-center text-[var(--color-on-surface-muted)]",
172
284
  children: messages.noResults
173
285
  }
@@ -197,7 +309,27 @@ function DataTable({
197
309
  }
198
310
  )
199
311
  ] })
200
- ] })
312
+ ] }),
313
+ bulkActions && selectedRows.length > 0 && /* @__PURE__ */ jsxs(
314
+ "div",
315
+ {
316
+ role: "region",
317
+ "aria-label": messages.selectedCount(fmt(selectedRows.length)),
318
+ className: cn(
319
+ floatingSurface,
320
+ "nx-motion sticky bottom-4 z-10 mx-auto flex w-fit max-w-full flex-wrap items-center gap-3 px-4 py-2"
321
+ ),
322
+ "data-state": "open",
323
+ children: [
324
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "text-label-lg tabular-nums", children: messages.selectedCount(fmt(selectedRows.length)) }),
325
+ bulkActions(
326
+ selectedRows.map((r) => r.original),
327
+ clear
328
+ ),
329
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: clear, "aria-label": messages.clearSelection, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
330
+ ]
331
+ }
332
+ )
201
333
  ] });
202
334
  }
203
335
  export {