@nova-design-system/nova-react 3.28.0 → 3.30.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.
Files changed (89) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/{constants-BReL3Lsa-DpB_ghPF.js → constants-gOKBmbgZ-CSoZ8z-G.js} +5 -0
  3. package/dist/cjs/generated/components.server.js +33 -1
  4. package/dist/cjs/index-DXwd3F_r.js +10391 -0
  5. package/dist/cjs/index.js +7 -1
  6. package/dist/cjs/{nv-accordion-item.entry-fds_kk_3.js → nv-accordion-item.entry-CTbduehY.js} +3 -3
  7. package/dist/cjs/{nv-accordion.entry-BLs4N5ZL.js → nv-accordion.entry-DeaPwIUP.js} +2 -2
  8. package/dist/cjs/{nv-alert.entry-Bx1BiC8F.js → nv-alert.entry-B5k8RXXD.js} +8 -8
  9. package/dist/cjs/{nv-avatar.entry-DS88LME3.js → nv-avatar.entry-CJh9bGfN.js} +8 -8
  10. package/dist/cjs/{nv-badge_2.entry-vBPxmUmg.js → nv-badge_2.entry-BnPPW4rW.js} +8 -7
  11. package/dist/cjs/{nv-breadcrumb.entry-BOo3hA5y.js → nv-breadcrumb.entry-DS5GJVCJ.js} +2 -2
  12. package/dist/cjs/{nv-breadcrumbs.entry-igHC_6Bd.js → nv-breadcrumbs.entry-CJ835Ba8.js} +2 -2
  13. package/dist/cjs/{nv-button.entry-BRQPmQbs.js → nv-button.entry-8U0o_X3d.js} +7 -7
  14. package/dist/cjs/{nv-buttongroup.entry-D3tG2EZ1.js → nv-buttongroup.entry-D5I5d9y0.js} +2 -2
  15. package/dist/cjs/{nv-calendar.entry-BpNHMTKr.js → nv-calendar.entry-C0ggIM3A.js} +9 -9
  16. package/dist/cjs/{nv-col.entry-lyIZqDsW.js → nv-col.entry-tIk723nS.js} +2 -2
  17. package/dist/cjs/{nv-datagrid.entry-BeemONKu.js → nv-datagrid.entry-Cal_usTi.js} +8 -8
  18. package/dist/cjs/{nv-datagridcolumn.entry-B6kE4eVC.js → nv-datagridcolumn.entry-BLbpwW7Q.js} +2 -2
  19. package/dist/cjs/{nv-datetest.entry-C1uuC-ZG.js → nv-datetest.entry-NQzySOox.js} +2 -2
  20. package/dist/cjs/{nv-datetimetest.entry-Hthxbjj9.js → nv-datetimetest.entry-Br6j4eAR.js} +1 -1
  21. package/dist/cjs/{nv-dialog.entry-Cr9zwMPo.js → nv-dialog.entry-GTDQJaoQ.js} +3 -3
  22. package/dist/cjs/{nv-dialogfooter_2.entry-DaaKojyE.js → nv-dialogfooter_2.entry-CHY36NHl.js} +10 -8
  23. package/dist/cjs/{nv-drawer.entry-0UaYxCjh.js → nv-drawer.entry-C-TZDi4-.js} +3 -3
  24. package/dist/cjs/{nv-drawerfooter_2.entry-CqtuC7xP.js → nv-drawerfooter_2.entry-DTY4qAnd.js} +8 -6
  25. package/dist/cjs/{nv-fieldcheckbox.entry-DK3aO8C8.js → nv-fieldcheckbox.entry-Qf4gdEnq.js} +5 -5
  26. package/dist/cjs/{nv-fielddate.entry-BY-xF3KN.js → nv-fielddate.entry-D1aJmKbh.js} +8 -8
  27. package/dist/cjs/{nv-fielddaterange.entry-BT6qCQc3.js → nv-fielddaterange.entry-Vc-gHwf8.js} +8 -8
  28. package/dist/cjs/{nv-fielddropdown.entry-CsVD067i.js → nv-fielddropdown.entry-BVVsM71m.js} +6 -6
  29. package/dist/cjs/{nv-fielddropdownitem.entry-Crtfwlx7.js → nv-fielddropdownitem.entry-DTRonCZJ.js} +2 -2
  30. package/dist/cjs/{nv-fieldmultiselect.entry-C2uoE60e.js → nv-fieldmultiselect.entry-Cvsh_jn7.js} +115 -86
  31. package/dist/cjs/{nv-fieldnumber.entry-DzW5SiiZ.js → nv-fieldnumber.entry-jVFZ0wj3.js} +6 -6
  32. package/dist/cjs/{nv-fieldpassword.entry-D4r9Qxos.js → nv-fieldpassword.entry-CQKbWqMe.js} +6 -6
  33. package/dist/cjs/{nv-fieldradio.entry-DX0Ghx--.js → nv-fieldradio.entry-FPGOqFBo.js} +4 -4
  34. package/dist/cjs/{nv-fieldselect.entry-_CglE66i.js → nv-fieldselect.entry-DJmW4pff.js} +7 -7
  35. package/dist/cjs/{nv-fieldslider.entry-6zt75uDU.js → nv-fieldslider.entry-D71HjSto.js} +4 -4
  36. package/dist/cjs/{nv-fieldtext.entry-B4X110wn.js → nv-fieldtext.entry-BxgbvxEW.js} +6 -6
  37. package/dist/cjs/{nv-fieldtextarea.entry-CeAMhA8Y.js → nv-fieldtextarea.entry-BDS37zhJ.js} +5 -5
  38. package/dist/cjs/{nv-fieldtime.entry-BvzncwNd.js → nv-fieldtime.entry-DkWokxdT.js} +55 -52
  39. package/dist/cjs/{nv-icon.entry-C2md2kmq.js → nv-icon.entry-CUemAv89.js} +8 -8
  40. package/dist/cjs/{nv-iconbutton_2.entry-D-zRpLjT.js → nv-iconbutton_2.entry-BLdJr3QV.js} +3 -3
  41. package/dist/cjs/{nv-menu.entry-DN_DkosX.js → nv-menu.entry-z4cDNCeX.js} +2 -2
  42. package/dist/cjs/{nv-menuitem.entry-Cj6w33rq.js → nv-menuitem.entry-4XdONuGl.js} +2 -2
  43. package/dist/cjs/{nv-notification-bullet.entry-DagStJ3K.js → nv-notification-bullet.entry-B1EK4G_X.js} +1 -1
  44. package/dist/cjs/{nv-notification.entry-oV69FpxE.js → nv-notification.entry-BBAQ72f7.js} +19 -19
  45. package/dist/cjs/{nv-notificationcontainer.entry-gQGHHeer.js → nv-notificationcontainer.entry-CtFunFA9.js} +2 -2
  46. package/dist/cjs/{nv-pagination-nav.entry-BYvcVj1M.js → nv-pagination-nav.entry-DbzEBjoa.js} +2 -2
  47. package/dist/cjs/{nv-paginationtable.entry-CwCFQwbl.js → nv-paginationtable.entry-DRGsk0DH.js} +19 -3
  48. package/dist/cjs/{nv-popover.entry-DySToeSB.js → nv-popover.entry-w9o3Y_vT.js} +2 -2
  49. package/dist/cjs/{nv-row.entry-46ghuEeG.js → nv-row.entry-Cop4QB1Y.js} +2 -2
  50. package/dist/cjs/nv-sidebar.entry-96HyTMt9.js +355 -0
  51. package/dist/cjs/{nv-sidebarcontent.entry-Pb8c2QoA.js → nv-sidebarcontent.entry-RCkZZp9I.js} +2 -2
  52. package/dist/cjs/{nv-sidebardivider.entry-LCCO53Z5.js → nv-sidebardivider.entry-DY25WqI7.js} +2 -2
  53. package/dist/cjs/{nv-sidebarfooter.entry-DG5fkLHd.js → nv-sidebarfooter.entry-9BFpNPLP.js} +2 -2
  54. package/dist/cjs/{nv-sidebargroup.entry-DRqkSyQi.js → nv-sidebargroup.entry-Cj52DXba.js} +2 -2
  55. package/dist/cjs/{nv-sidebarheader.entry-D6WvH2wG.js → nv-sidebarheader.entry-_WZgNwoi.js} +2 -2
  56. package/dist/cjs/{nv-sidebarlogo.entry-BorScwI-.js → nv-sidebarlogo.entry-4Zy6k3V5.js} +2 -2
  57. package/dist/cjs/{nv-sidebarnavitem.entry-BEW74Rw3.js → nv-sidebarnavitem.entry-fQ9sqxDj.js} +3 -3
  58. package/dist/cjs/{nv-sidebarnavsubitem.entry-EgaIlUfP.js → nv-sidebarnavsubitem.entry-1iBobkME.js} +2 -2
  59. package/dist/cjs/{nv-split.entry-CEC5Tuwz.js → nv-split.entry-Dfzpge0r.js} +2 -2
  60. package/dist/cjs/{nv-stack.entry-BR8lYaoI.js → nv-stack.entry-BnCput1S.js} +2 -2
  61. package/dist/cjs/nv-statusindicator.entry-ClPHcTkz.js +42 -0
  62. package/dist/cjs/{nv-table.entry-CISZFst5.js → nv-table.entry-DsllD6Bz.js} +3 -3
  63. package/dist/cjs/{nv-tableheader.entry-DbdpTJGC.js → nv-tableheader.entry-AWCKrpLj.js} +2 -2
  64. package/dist/cjs/nv-tag.entry-BwENpoSV.js +85 -0
  65. package/dist/cjs/{nv-timetest.entry-Dg1JEgAv.js → nv-timetest.entry-BQsV7Qb2.js} +2 -2
  66. package/dist/cjs/{nv-toggle.entry-DDfRpC1R.js → nv-toggle.entry-a0tSJ6GE.js} +3 -3
  67. package/dist/cjs/{nv-togglebutton.entry-D7NkdIXP.js → nv-togglebutton.entry-CT8fZr74.js} +2 -2
  68. package/dist/cjs/{nv-togglebuttongroup.entry-L8gitSUS.js → nv-togglebuttongroup.entry-BN7WkQ_L.js} +2 -2
  69. package/dist/cjs/{nv-tooltip.entry-BElnrEqE.js → nv-tooltip.entry-xpFqRgZM.js} +2 -2
  70. package/dist/components/NvDatatable/NvDatatable.js +294 -88
  71. package/dist/components/NvDatatable/expandState.js +8 -0
  72. package/dist/components/NvDatatable/expandState.test.js +41 -0
  73. package/dist/components/NvDatatable/paginationState.js +9 -0
  74. package/dist/components/NvDatatable/paginationState.test.js +84 -0
  75. package/dist/generated/components.js +20 -1
  76. package/dist/generated/components.server.js +31 -1
  77. package/dist/types/components/NvDatatable/NvDatatable.d.ts +1 -1
  78. package/dist/types/components/NvDatatable/expandState.d.ts +5 -0
  79. package/dist/types/components/NvDatatable/expandState.test.d.ts +1 -0
  80. package/dist/types/components/NvDatatable/paginationState.d.ts +9 -0
  81. package/dist/types/components/NvDatatable/paginationState.test.d.ts +1 -0
  82. package/dist/types/components/NvDatatable/types.d.ts +21 -1
  83. package/dist/types/generated/components.d.ts +12 -1
  84. package/dist/types/generated/components.server.d.ts +12 -1
  85. package/dist/types/index.d.ts +1 -0
  86. package/dist/types/providers/NotificationProvider.d.ts +2 -2
  87. package/package.json +1 -1
  88. package/dist/cjs/index-Kxp9mv-Q.js +0 -10119
  89. package/dist/cjs/nv-sidebar.entry-B6opNG2r.js +0 -181
@@ -1,14 +1,15 @@
1
1
  import React, { useMemo, useState, useEffect, useRef, useCallback, } from 'react';
2
- import { flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, useReactTable, } from '@tanstack/react-table';
3
- import { NvTable, NvTableheader, NvPaginationtable, NvFieldcheckbox, } from '../../generated/components';
2
+ import { flexRender, getCoreRowModel, getPaginationRowModel, getSortedRowModel, getFilteredRowModel, getExpandedRowModel, useReactTable, } from '@tanstack/react-table';
3
+ import { NvTable, NvTableheader, NvPaginationtable, NvFieldcheckbox, NvIconbutton, } from '../../generated/components';
4
4
  import { defineCustomElement as defineNvPaginationtable } from '@nova-design-system/nova-webcomponents/dist/components/nv-paginationtable.js';
5
5
  if (typeof window !== 'undefined' &&
6
6
  !customElements.get('nv-paginationtable')) {
7
7
  defineNvPaginationtable();
8
8
  }
9
9
  import { applyRowSelection, applySelectAllSelection, shouldIgnoreSelectAllEvent, } from './selectionState';
10
- function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filtering, selection, renderPagination, renderFiltering, stickyHeader, ...htmlProps }) {
11
- const SELECTION_EVENT_SUPPRESSION_MS = 16;
10
+ import { shouldShowExpandedContent } from './expandState';
11
+ import { resolvePaginationRowCount } from './paginationState';
12
+ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filtering, selection, renderPagination, renderFiltering, onRowClick, expandable, stickyHeader, ...htmlProps }) {
12
13
  const areSetsEqual = useCallback((a, b) => {
13
14
  if (a.size !== b.size) {
14
15
  return false;
@@ -25,16 +26,29 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
25
26
  pageIndex: 0,
26
27
  pageSize: pagination?.initialPageSize || 10,
27
28
  });
28
- const [sortingState, setSortingState] = useState(sorting?.sortState || []);
29
+ const [sortingState, setSortingState] = useState(sorting?.sortState ?? sorting?.initialSortState ?? []);
29
30
  const [globalFilterState, setGlobalFilterState] = useState(filtering?.filterState);
30
31
  const [selectedRowIdsState, setSelectedRowIdsState] = useState(() => new Set(selection?.selectedRowIds ?? []));
31
32
  const selectedRowIdsRef = useRef(new Set(selection?.selectedRowIds ?? []));
32
33
  const selectionMode = selection?.mode ?? 'none';
33
34
  const selectionEnabled = selectionMode !== 'none';
34
35
  const selectAllEnabled = selection?.enableSelectAll ?? true;
36
+ const rowClickEnabled = typeof onRowClick === 'function';
37
+ const hasExpandable = !!expandable;
38
+ const isCollapsible = expandable?.collapsible ?? false;
39
+ const [expandedState, setExpandedState] = useState(expandable?.expandedState ?? {});
40
+ useEffect(() => {
41
+ if (expandable?.expandedState !== undefined) {
42
+ setExpandedState(expandable.expandedState);
43
+ }
44
+ }, [expandable?.expandedState]);
35
45
  const getSelectionRowId = useCallback((row) => selection?.getRowId?.(row) ?? '', [selection]);
36
46
  const lastRowRef = useRef(null);
37
- const suppressRowSelectionEventsRef = useRef(false);
47
+ const selectAllIntentRef = useRef(false);
48
+ const rowSelectionIntentRef = useRef(null);
49
+ const selectAllIntentResetTimerRef = useRef(null);
50
+ const rowSelectionIntentResetTimerRef = useRef(null);
51
+ const INTENT_RESET_DELAY_MS = 16;
38
52
  const debouncedSetFilter = useRef(null);
39
53
  const setGlobalFilterDebounced = useCallback((value) => {
40
54
  const debounceMs = filtering?.debounceMs ?? 300;
@@ -53,6 +67,12 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
53
67
  if (debouncedSetFilter.current) {
54
68
  clearTimeout(debouncedSetFilter.current);
55
69
  }
70
+ if (selectAllIntentResetTimerRef.current) {
71
+ clearTimeout(selectAllIntentResetTimerRef.current);
72
+ }
73
+ if (rowSelectionIntentResetTimerRef.current) {
74
+ clearTimeout(rowSelectionIntentResetTimerRef.current);
75
+ }
56
76
  };
57
77
  }, []);
58
78
  useEffect(() => {
@@ -130,29 +150,66 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
130
150
  }
131
151
  return columnDef;
132
152
  });
133
- if (!selectionEnabled) {
134
- return dataColumns;
135
- }
136
- const selectionColumn = {
137
- id: '__selection__',
138
- header: '',
139
- size: selection?.checkboxColumnWidth ?? 48,
140
- enableResizing: false,
141
- enableSorting: false,
142
- cell: () => null,
143
- };
144
- return [selectionColumn, ...dataColumns];
145
- }, [columns, sorting, selectionEnabled, selection?.checkboxColumnWidth]);
153
+ const prefixColumns = [];
154
+ const isCombinedColumn = selectionEnabled && isCollapsible;
155
+ if (isCombinedColumn) {
156
+ prefixColumns.push({
157
+ id: '__selection_expand__',
158
+ header: '',
159
+ size: (selection?.checkboxColumnWidth ?? 48) +
160
+ (expandable?.toggleColumnWidth ?? 48),
161
+ enableResizing: false,
162
+ enableSorting: false,
163
+ cell: () => null,
164
+ });
165
+ }
166
+ else {
167
+ if (selectionEnabled) {
168
+ prefixColumns.push({
169
+ id: '__selection__',
170
+ header: '',
171
+ size: selection?.checkboxColumnWidth ?? 48,
172
+ enableResizing: false,
173
+ enableSorting: false,
174
+ cell: () => null,
175
+ });
176
+ }
177
+ if (isCollapsible) {
178
+ prefixColumns.push({
179
+ id: '__expand__',
180
+ header: '',
181
+ size: expandable?.toggleColumnWidth ?? 48,
182
+ enableResizing: false,
183
+ enableSorting: false,
184
+ cell: () => null,
185
+ });
186
+ }
187
+ }
188
+ return [...prefixColumns, ...dataColumns];
189
+ }, [
190
+ columns,
191
+ sorting,
192
+ selectionEnabled,
193
+ selection?.checkboxColumnWidth,
194
+ isCollapsible,
195
+ expandable?.toggleColumnWidth,
196
+ ]);
146
197
  const tableConfig = useMemo(() => {
198
+ const tableState = {};
199
+ if (sorting) {
200
+ tableState.sorting = sorting.sortState || sortingState;
201
+ }
202
+ if (filtering) {
203
+ tableState.globalFilter = globalFilterState;
204
+ }
205
+ if (isCollapsible) {
206
+ tableState.expanded = expandable?.expandedState ?? expandedState;
207
+ }
147
208
  const baseConfig = {
148
209
  data: rows,
149
210
  columns: tableColumns,
150
211
  getCoreRowModel: getCoreRowModel(),
151
212
  ...(sorting && {
152
- state: {
153
- sorting: sorting.sortState || sortingState,
154
- ...(filtering && { globalFilter: globalFilterState }),
155
- },
156
213
  onSortingChange: mode === 'server'
157
214
  ? (updater) => {
158
215
  const newSort = typeof updater === 'function'
@@ -172,10 +229,6 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
172
229
  getSortedRowModel: mode === 'client' ? getSortedRowModel() : undefined,
173
230
  }),
174
231
  ...(filtering && {
175
- state: {
176
- ...(sorting && { sorting: sorting.sortState || sortingState }),
177
- globalFilter: globalFilterState,
178
- },
179
232
  onGlobalFilterChange: (updater) => {
180
233
  const newFilter = typeof updater === 'function'
181
234
  ? updater(globalFilterState)
@@ -188,6 +241,20 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
188
241
  globalFilterFn: filtering.globalFilterFn,
189
242
  getFilteredRowModel: mode === 'client' ? getFilteredRowModel() : undefined,
190
243
  }),
244
+ ...(isCollapsible && {
245
+ getExpandedRowModel: getExpandedRowModel(),
246
+ getRowCanExpand: expandable?.getRowCanExpand
247
+ ? (row) => expandable.getRowCanExpand(row.original)
248
+ : () => true,
249
+ onExpandedChange: (updater) => {
250
+ const newExpanded = typeof updater === 'function'
251
+ ? updater(expandedState)
252
+ : updater;
253
+ setExpandedState(newExpanded);
254
+ expandable?.onExpandedChange?.(newExpanded);
255
+ },
256
+ }),
257
+ ...(Object.keys(tableState).length > 0 && { state: tableState }),
191
258
  };
192
259
  if (!pagination) {
193
260
  return baseConfig;
@@ -233,12 +300,21 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
233
300
  sortingState,
234
301
  filtering,
235
302
  globalFilterState,
303
+ isCollapsible,
304
+ expandable,
305
+ expandedState,
236
306
  mode,
237
307
  ]);
238
308
  const table = useReactTable(tableConfig);
239
309
  const tableRows = table.getRowModel().rows;
310
+ const getCurrentVisibleRowIds = useCallback(() => {
311
+ return table
312
+ .getRowModel()
313
+ .rows.map((row) => getSelectionRowId(row.original))
314
+ .filter((id) => !!id);
315
+ }, [table, getSelectionRowId]);
240
316
  const setSelectionState = useCallback((nextSelectedIds) => {
241
- const current = new Set(selection?.selectedRowIds ?? selectedRowIdsRef.current);
317
+ const current = new Set(selectedRowIdsRef.current);
242
318
  const nextRaw = typeof nextSelectedIds === 'function'
243
319
  ? nextSelectedIds(new Set(current))
244
320
  : nextSelectedIds;
@@ -246,27 +322,14 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
246
322
  if (areSetsEqual(current, next)) {
247
323
  return;
248
324
  }
325
+ const hasExplicitSelectionIntent = selectAllIntentRef.current || rowSelectionIntentRef.current !== null;
326
+ if (!hasExplicitSelectionIntent && next.size < current.size) {
327
+ return;
328
+ }
249
329
  selectedRowIdsRef.current = next;
250
330
  setSelectedRowIdsState(next);
251
331
  selection?.onSelectionChange?.(new Set(next));
252
- }, [selection, selection?.selectedRowIds, areSetsEqual]);
253
- const resolveCheckedChange = useCallback((event) => {
254
- if (event && typeof event === 'object') {
255
- const eventLike = event;
256
- const target = eventLike.target;
257
- const currentTarget = eventLike.currentTarget;
258
- if (typeof eventLike.detail === 'boolean') {
259
- return eventLike.detail;
260
- }
261
- if (typeof target?.checked === 'boolean') {
262
- return target.checked;
263
- }
264
- if (typeof currentTarget?.checked === 'boolean') {
265
- return currentTarget.checked;
266
- }
267
- }
268
- return undefined;
269
- }, []);
332
+ }, [selection, areSetsEqual]);
270
333
  const isRowSelected = useCallback((row) => {
271
334
  if (!selectionEnabled) {
272
335
  return false;
@@ -277,22 +340,18 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
277
340
  const visibleRowIds = useMemo(() => tableRows
278
341
  .map((row) => getSelectionRowId(row.original))
279
342
  .filter((id) => !!id), [tableRows, getSelectionRowId]);
280
- const visibleRowIdsSet = useMemo(() => new Set(visibleRowIds), [visibleRowIds]);
281
343
  const setRowSelection = useCallback((row, isChecked) => {
282
344
  if (!selectionEnabled) {
283
345
  return;
284
346
  }
285
- if (suppressRowSelectionEventsRef.current) {
286
- return;
287
- }
288
347
  const rowId = getSelectionRowId(row);
289
348
  if (!rowId) {
290
349
  return;
291
350
  }
292
- if (!visibleRowIdsSet.has(rowId)) {
351
+ const currentVisibleRowIdsSet = new Set(getCurrentVisibleRowIds());
352
+ if (!currentVisibleRowIdsSet.has(rowId)) {
293
353
  return;
294
354
  }
295
- suppressRowSelectionEventsRef.current = true;
296
355
  setSelectionState((current) => {
297
356
  return applyRowSelection({
298
357
  currentSelectedIds: current,
@@ -301,15 +360,12 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
301
360
  selectionMode,
302
361
  });
303
362
  });
304
- setTimeout(() => {
305
- suppressRowSelectionEventsRef.current = false;
306
- }, SELECTION_EVENT_SUPPRESSION_MS);
307
363
  }, [
308
364
  selectionEnabled,
309
365
  getSelectionRowId,
310
- visibleRowIdsSet,
311
366
  selectionMode,
312
367
  setSelectionState,
368
+ getCurrentVisibleRowIds,
313
369
  ]);
314
370
  const selectAllState = useMemo(() => {
315
371
  if (!selectionEnabled ||
@@ -333,34 +389,37 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
333
389
  visibleRowIds,
334
390
  selectedRowIdsState,
335
391
  ]);
392
+ const selectAllStateRef = useRef(selectAllState);
393
+ selectAllStateRef.current = selectAllState;
336
394
  const handleSelectAllChanged = useCallback((isChecked) => {
337
395
  if (!selectionEnabled ||
338
396
  selectionMode !== 'multiple' ||
339
- !selectAllEnabled ||
340
- !visibleRowIds.length) {
397
+ !selectAllEnabled) {
398
+ return;
399
+ }
400
+ const currentVisibleRowIds = getCurrentVisibleRowIds();
401
+ if (!currentVisibleRowIds.length) {
341
402
  return;
342
403
  }
343
- if (shouldIgnoreSelectAllEvent({ isChecked, selectAllState })) {
404
+ if (shouldIgnoreSelectAllEvent({
405
+ isChecked,
406
+ selectAllState: selectAllStateRef.current,
407
+ })) {
344
408
  return;
345
409
  }
346
- suppressRowSelectionEventsRef.current = true;
347
410
  setSelectionState((current) => {
348
411
  return applySelectAllSelection({
349
412
  currentSelectedIds: current,
350
- visibleRowIds,
413
+ visibleRowIds: currentVisibleRowIds,
351
414
  isChecked,
352
415
  });
353
416
  });
354
- setTimeout(() => {
355
- suppressRowSelectionEventsRef.current = false;
356
- }, SELECTION_EVENT_SUPPRESSION_MS);
357
417
  }, [
358
418
  selectionEnabled,
359
419
  selectionMode,
360
420
  selectAllEnabled,
361
- visibleRowIds,
362
- selectAllState,
363
421
  setSelectionState,
422
+ getCurrentVisibleRowIds,
364
423
  ]);
365
424
  useEffect(() => {
366
425
  if (!selectionEnabled || selection?.deselectOnFilter === false) {
@@ -437,9 +496,14 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
437
496
  return null;
438
497
  }
439
498
  const pageCount = table.getPageCount();
440
- const rowCount = mode === 'server' && !pagination.infinite
441
- ? pagination.totalRowCount || rows.length
442
- : rows.length;
499
+ const rowCount = resolvePaginationRowCount({
500
+ mode,
501
+ isInfinite: !!pagination.infinite,
502
+ hasFiltering: !!filtering,
503
+ totalRowCount: pagination.totalRowCount,
504
+ rowsLength: rows.length,
505
+ filteredRowsLength: table.getFilteredRowModel().rows.length,
506
+ });
443
507
  const api = {
444
508
  pageIndex: tablePaginationState.pageIndex,
445
509
  pageSize: tablePaginationState.pageSize,
@@ -474,7 +538,11 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
474
538
  if (pagination?.labels?.pageSizeOption)
475
539
  labelProps.labelPageSizeOption = pagination.labels.pageSizeOption;
476
540
  return (React.createElement("div", { "data-testid": "default-pagination-wrapper", style: { marginTop: '16px' } },
477
- React.createElement(NvPaginationtable, { pageIndex: api.pageIndex, pageSize: api.pageSize, pageCount: api.pageCount, rowCount: api.rowCount, ...labelProps, onPageIndexChanged: (e) => api.setPageIndex(e.detail), onPageSizeChanged: (e) => api.setPageSize(e.detail) })));
541
+ React.createElement(NvPaginationtable, { pageIndex: api.pageIndex, pageSize: api.pageSize, pageCount: api.pageCount, rowCount: api.rowCount, ...labelProps, onPageIndexChanged: (e) => {
542
+ api.setPageIndex(e.detail);
543
+ }, onPageSizeChanged: (e) => {
544
+ api.setPageSize(e.detail);
545
+ } })));
478
546
  };
479
547
  const filteringAPI = useMemo(() => {
480
548
  if (!filtering) {
@@ -495,29 +563,122 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
495
563
  filteredRows: filteredRowModel.rows.length,
496
564
  };
497
565
  }, [filtering, globalFilterState, table, setGlobalFilterDebounced]);
566
+ const markSelectAllIntentFromKey = (event) => {
567
+ if (event.key === ' ' || event.key === 'Enter') {
568
+ if (selectAllIntentResetTimerRef.current) {
569
+ clearTimeout(selectAllIntentResetTimerRef.current);
570
+ }
571
+ selectAllIntentRef.current = true;
572
+ }
573
+ };
574
+ const scheduleSelectAllIntentReset = () => {
575
+ if (selectAllIntentResetTimerRef.current) {
576
+ clearTimeout(selectAllIntentResetTimerRef.current);
577
+ }
578
+ selectAllIntentResetTimerRef.current = setTimeout(() => {
579
+ selectAllIntentRef.current = false;
580
+ selectAllIntentResetTimerRef.current = null;
581
+ }, INTENT_RESET_DELAY_MS);
582
+ };
583
+ const markRowSelectionIntentFromKey = (event, rowId) => {
584
+ if (event.key === ' ' || event.key === 'Enter') {
585
+ if (rowSelectionIntentResetTimerRef.current) {
586
+ clearTimeout(rowSelectionIntentResetTimerRef.current);
587
+ }
588
+ rowSelectionIntentRef.current = rowId;
589
+ }
590
+ };
591
+ const scheduleRowSelectionIntentReset = (rowId) => {
592
+ if (rowSelectionIntentResetTimerRef.current) {
593
+ clearTimeout(rowSelectionIntentResetTimerRef.current);
594
+ }
595
+ rowSelectionIntentResetTimerRef.current = setTimeout(() => {
596
+ if (rowSelectionIntentRef.current === rowId) {
597
+ rowSelectionIntentRef.current = null;
598
+ }
599
+ rowSelectionIntentResetTimerRef.current = null;
600
+ }, INTENT_RESET_DELAY_MS);
601
+ };
602
+ const renderRowSelectionCheckbox = useCallback((row, rowIndex) => {
603
+ const rowId = getSelectionRowId(row);
604
+ return (React.createElement(NvFieldcheckbox, { "data-testid": `datatable-row-${rowIndex}-checkbox`, checked: isRowSelected(row), label: `Select row ${rowIndex + 1}`, hideLabel: true, onPointerDownCapture: () => {
605
+ if (rowSelectionIntentResetTimerRef.current) {
606
+ clearTimeout(rowSelectionIntentResetTimerRef.current);
607
+ }
608
+ rowSelectionIntentRef.current = rowId;
609
+ }, onKeyDownCapture: (event) => {
610
+ markRowSelectionIntentFromKey(event, rowId);
611
+ }, onBlurCapture: () => {
612
+ scheduleRowSelectionIntentReset(rowId);
613
+ }, onCheckedChanged: (event) => {
614
+ if (!rowId || rowSelectionIntentRef.current !== rowId) {
615
+ return;
616
+ }
617
+ try {
618
+ setRowSelection(row, event.detail);
619
+ }
620
+ finally {
621
+ rowSelectionIntentRef.current = null;
622
+ }
623
+ } }));
624
+ }, [getSelectionRowId, isRowSelected, setRowSelection]);
625
+ const renderExpandToggle = useCallback((row, rowIndex) => {
626
+ const isExpanded = row.getIsExpanded();
627
+ const canExpand = row.getCanExpand();
628
+ const expandedContentId = `datatable-expanded-content-${rowIndex}`;
629
+ if (!canExpand) {
630
+ return null;
631
+ }
632
+ return (React.createElement(NvIconbutton, { "data-testid": `datatable-row-${rowIndex}-expand-toggle`, "aria-expanded": isExpanded ? 'true' : 'false', "aria-controls": expandedContentId, "aria-label": isExpanded
633
+ ? `Collapse row ${rowIndex + 1}`
634
+ : `Expand row ${rowIndex + 1}`, onClick: (e) => {
635
+ e.stopPropagation();
636
+ row.toggleExpanded();
637
+ }, name: isExpanded ? 'chevron-down' : 'chevron-right', size: "sm", emphasis: "lower" }));
638
+ }, []);
498
639
  return (React.createElement(React.Fragment, null,
499
640
  filteringAPI && renderFiltering && renderFiltering(filteringAPI),
500
641
  React.createElement(NvTable, { ...htmlProps },
501
- React.createElement("table", null,
642
+ React.createElement("table", { "data-row-click-enabled": rowClickEnabled ? 'true' : 'false' },
502
643
  React.createElement("thead", { "data-sticky-top": stickyHeader ? 'true' : undefined }, table.getHeaderGroups().map((headerGroup) => (React.createElement("tr", { key: headerGroup.id }, headerGroup.headers.map((header) => {
503
644
  const isSelectionHeader = header.id === '__selection__';
645
+ const isExpandHeader = header.id === '__expand__';
646
+ const isCombinedHeader = header.id === '__selection_expand__';
647
+ const isSpecialHeader = isSelectionHeader || isExpandHeader || isCombinedHeader;
504
648
  const canSort = header.column.getCanSort();
505
649
  const sortDirection = header.column.getIsSorted();
650
+ const renderSelectAllCheckbox = selectionMode === 'multiple' && selectAllEnabled ? (React.createElement("div", { className: "flex items-center justify-center" },
651
+ React.createElement(NvFieldcheckbox, { "data-testid": "datatable-select-all-checkbox", checked: selectAllState === 'checked', indeterminate: selectAllState === 'indeterminate', label: "Select all rows", hideLabel: true, onPointerDownCapture: () => {
652
+ if (selectAllIntentResetTimerRef.current) {
653
+ clearTimeout(selectAllIntentResetTimerRef.current);
654
+ }
655
+ selectAllIntentRef.current = true;
656
+ }, onKeyDownCapture: markSelectAllIntentFromKey, onBlurCapture: () => {
657
+ scheduleSelectAllIntentReset();
658
+ }, onCheckedChanged: (event) => {
659
+ if (!selectAllIntentRef.current) {
660
+ return;
661
+ }
662
+ try {
663
+ handleSelectAllChanged(event.detail);
664
+ }
665
+ finally {
666
+ selectAllIntentRef.current = false;
667
+ }
668
+ } }))) : null;
506
669
  return (React.createElement("th", { key: header.id, "data-testid": isSelectionHeader
507
670
  ? 'datatable-header-selection'
508
- : `datatable-header-${header.id}`, style: {
671
+ : isExpandHeader
672
+ ? 'datatable-header-expand'
673
+ : isCombinedHeader
674
+ ? 'datatable-header-selection-expand'
675
+ : `datatable-header-${header.id}`, style: {
509
676
  width: header.column.columnDef.size + 'px',
510
- }, "data-no-resize": isSelectionHeader
677
+ }, "data-no-resize": isSpecialHeader
511
678
  ? true
512
679
  : header.column.columnDef.enableResizing
513
680
  ? null
514
- : true }, isSelectionHeader ? (selectionMode === 'multiple' && selectAllEnabled ? (React.createElement("div", { className: "flex items-center justify-center" },
515
- React.createElement(NvFieldcheckbox, { "data-testid": "datatable-select-all-checkbox", checked: selectAllState === 'checked', indeterminate: selectAllState === 'indeterminate', label: "Select all rows", hideLabel: true, onCheckedChanged: (event) => {
516
- const checked = resolveCheckedChange(event);
517
- if (typeof checked === 'boolean') {
518
- handleSelectAllChanged(checked);
519
- }
520
- } }))) : null) : (React.createElement(NvTableheader, { sortable: canSort ? true : undefined, sortDirection: sortDirection || (canSort ? 'none' : undefined), onSortDirectionChanged: canSort
681
+ : true }, isSelectionHeader ? (renderSelectAllCheckbox) : isCombinedHeader ? (renderSelectAllCheckbox) : isExpandHeader ? null : (React.createElement(NvTableheader, { sortable: canSort ? true : undefined, sortDirection: sortDirection || (canSort ? 'none' : undefined), onSortDirectionChanged: canSort
521
682
  ? header.column.getToggleSortingHandler()
522
683
  : undefined }, header.isPlaceholder
523
684
  ? null
@@ -525,14 +686,59 @@ function NvDatatable({ mode = 'client', columns, rows, pagination, sorting, filt
525
686
  }))))),
526
687
  React.createElement("tbody", null, tableRows.map((row, index) => {
527
688
  const isLastRow = isInfiniteScroll && index === tableRows.length - 1;
528
- return (React.createElement("tr", { key: row.id, "data-testid": `datatable-row-${index}`, ref: isLastRow ? lastRowRef : undefined }, row.getVisibleCells().map((cell) => (React.createElement("td", { key: cell.id, "data-testid": cell.column.id === '__selection__'
529
- ? 'datatable-cell-selection'
530
- : `datatable-cell-${cell.column.id}` }, cell.column.id === '__selection__' ? (React.createElement(NvFieldcheckbox, { "data-testid": `datatable-row-${index}-checkbox`, checked: isRowSelected(row.original), label: `Select row ${index + 1}`, hideLabel: true, onCheckedChanged: (event) => {
531
- const checked = resolveCheckedChange(event);
532
- if (typeof checked === 'boolean') {
533
- setRowSelection(row.original, checked);
534
- }
535
- } })) : (flexRender(cell.column.columnDef.cell, cell.getContext())))))));
689
+ const stableRowKey = getSelectionRowId(row.original) || row.id;
690
+ const expandedContent = hasExpandable
691
+ ? expandable.renderExpandedContent({
692
+ row: row.original,
693
+ rowIndex: index,
694
+ })
695
+ : null;
696
+ const hasContent = expandedContent != null;
697
+ const showExpanded = shouldShowExpandedContent({
698
+ hasContent,
699
+ isCollapsible,
700
+ isExpanded: isCollapsible ? row.getIsExpanded() : false,
701
+ });
702
+ const expandedContentId = `datatable-expanded-content-${index}`;
703
+ const totalColumnCount = row.getVisibleCells().length;
704
+ return (React.createElement(React.Fragment, { key: stableRowKey },
705
+ React.createElement("tr", { "data-testid": `datatable-row-${index}`, "data-selected": isRowSelected(row.original) ? '' : undefined, ref: isLastRow ? lastRowRef : undefined, className: rowClickEnabled ? 'nv-datatable-row-clickable' : undefined, tabIndex: rowClickEnabled ? 0 : undefined, onClick: rowClickEnabled
706
+ ? () => onRowClick?.({
707
+ row: row.original,
708
+ rowIndex: index,
709
+ })
710
+ : undefined, onKeyDown: rowClickEnabled
711
+ ? (event) => {
712
+ if (event.currentTarget !== event.target) {
713
+ return;
714
+ }
715
+ if (event.key === 'Enter' || event.key === ' ') {
716
+ event.preventDefault();
717
+ onRowClick?.({
718
+ row: row.original,
719
+ rowIndex: index,
720
+ });
721
+ }
722
+ }
723
+ : undefined }, row.getVisibleCells().map((cell) => {
724
+ const isCombinedCell = cell.column.id === '__selection_expand__';
725
+ const isSelectionCell = cell.column.id === '__selection__';
726
+ const isExpandCell = cell.column.id === '__expand__';
727
+ return (React.createElement("td", { key: cell.id, "data-testid": isSelectionCell
728
+ ? 'datatable-cell-selection'
729
+ : isExpandCell
730
+ ? 'datatable-cell-expand'
731
+ : isCombinedCell
732
+ ? 'datatable-cell-selection-expand'
733
+ : `datatable-cell-${cell.column.id}`, onClick: isSelectionCell || isExpandCell || isCombinedCell
734
+ ? (event) => event.stopPropagation()
735
+ : undefined }, isSelectionCell ? (renderRowSelectionCheckbox(row.original, index)) : isExpandCell ? (renderExpandToggle(row, index)) : isCombinedCell ? (React.createElement("div", { className: "flex items-center justify-center gap-1" },
736
+ renderRowSelectionCheckbox(row.original, index),
737
+ renderExpandToggle(row, index))) : (flexRender(cell.column.columnDef.cell, cell.getContext()))));
738
+ })),
739
+ showExpanded && (React.createElement("tr", { className: "nv-datatable-expanded-row", "data-testid": `datatable-expanded-row-${index}`, id: expandedContentId, role: "region", "aria-label": `Expanded content for row ${index + 1}` },
740
+ React.createElement("td", { colSpan: totalColumnCount },
741
+ React.createElement("div", { className: "nv-datatable-expanded-content" }, expandedContent))))));
536
742
  })))),
537
743
  paginationAPI &&
538
744
  (renderPagination
@@ -0,0 +1,8 @@
1
+ export function shouldShowExpandedContent(params) {
2
+ const { hasContent, isCollapsible, isExpanded } = params;
3
+ if (!hasContent)
4
+ return false;
5
+ if (!isCollapsible)
6
+ return true;
7
+ return isExpanded;
8
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { shouldShowExpandedContent } from './expandState';
3
+ describe('expandState', () => {
4
+ describe('shouldShowExpandedContent', () => {
5
+ it('returns false when there is no content', () => {
6
+ expect(shouldShowExpandedContent({
7
+ hasContent: false,
8
+ isCollapsible: false,
9
+ isExpanded: false,
10
+ })).toBe(false);
11
+ });
12
+ it('returns false when there is no content even if expanded', () => {
13
+ expect(shouldShowExpandedContent({
14
+ hasContent: false,
15
+ isCollapsible: true,
16
+ isExpanded: true,
17
+ })).toBe(false);
18
+ });
19
+ it('returns true when has content and not collapsible', () => {
20
+ expect(shouldShowExpandedContent({
21
+ hasContent: true,
22
+ isCollapsible: false,
23
+ isExpanded: false,
24
+ })).toBe(true);
25
+ });
26
+ it('returns false when collapsible and not expanded', () => {
27
+ expect(shouldShowExpandedContent({
28
+ hasContent: true,
29
+ isCollapsible: true,
30
+ isExpanded: false,
31
+ })).toBe(false);
32
+ });
33
+ it('returns true when collapsible and expanded', () => {
34
+ expect(shouldShowExpandedContent({
35
+ hasContent: true,
36
+ isCollapsible: true,
37
+ isExpanded: true,
38
+ })).toBe(true);
39
+ });
40
+ });
41
+ });
@@ -0,0 +1,9 @@
1
+ export function resolvePaginationRowCount(opts) {
2
+ if (opts.mode === 'server' && !opts.isInfinite) {
3
+ return opts.totalRowCount || opts.rowsLength;
4
+ }
5
+ if (opts.mode === 'client' && opts.hasFiltering) {
6
+ return opts.filteredRowsLength;
7
+ }
8
+ return opts.rowsLength;
9
+ }