@pallasoft/ps-table-components 1.22.1 → 1.23.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 (79) hide show
  1. package/dist/components/ps-data-table.d.ts +2 -1
  2. package/dist/components/ps-data-table.js +47 -11
  3. package/dist/components/ps-table-bulk-bar.js +3 -2
  4. package/dist/components/ps-table-column-picker.js +2 -1
  5. package/dist/components/ps-table-export-button.js +1 -1
  6. package/dist/components/ps-table-group-header.js +1 -1
  7. package/dist/components/ps-table-pagination.js +2 -2
  8. package/dist/components/ps-table-reorder-frame.js +15 -2
  9. package/dist/components/ps-table-reorder-handle.js +1 -1
  10. package/dist/components/ps-table-sort-header.js +2 -2
  11. package/dist/components/ps-table-sortable-row.d.ts +5 -1
  12. package/dist/components/ps-table-sortable-row.js +12 -4
  13. package/dist/components/ps-table-toolbar.js +2 -2
  14. package/dist/constants/ps-table-control-classes.d.ts +9 -0
  15. package/dist/constants/ps-table-control-classes.js +12 -0
  16. package/dist/constants/ps-table-drop-classes.d.ts +2 -0
  17. package/dist/constants/ps-table-drop-classes.js +8 -0
  18. package/dist/constants/ps-table-drop-placement.d.ts +6 -0
  19. package/dist/constants/ps-table-drop-placement.js +8 -0
  20. package/dist/constants/ps-table-pin-classes.d.ts +16 -0
  21. package/dist/constants/ps-table-pin-classes.js +19 -0
  22. package/dist/constants/table-breakpoint.d.ts +7 -0
  23. package/dist/constants/table-breakpoint.js +13 -0
  24. package/dist/esm/components/ps-data-table.js +48 -12
  25. package/dist/esm/components/ps-table-bulk-bar.js +3 -2
  26. package/dist/esm/components/ps-table-column-picker.js +2 -1
  27. package/dist/esm/components/ps-table-export-button.js +1 -1
  28. package/dist/esm/components/ps-table-group-header.js +1 -1
  29. package/dist/esm/components/ps-table-pagination.js +2 -2
  30. package/dist/esm/components/ps-table-reorder-frame.js +15 -2
  31. package/dist/esm/components/ps-table-reorder-handle.js +1 -1
  32. package/dist/esm/components/ps-table-sort-header.js +2 -2
  33. package/dist/esm/components/ps-table-sortable-row.js +12 -4
  34. package/dist/esm/components/ps-table-toolbar.js +2 -2
  35. package/dist/esm/constants/ps-table-control-classes.js +9 -0
  36. package/dist/esm/constants/ps-table-drop-classes.js +5 -0
  37. package/dist/esm/constants/ps-table-drop-placement.js +5 -0
  38. package/dist/esm/constants/ps-table-pin-classes.js +16 -0
  39. package/dist/esm/constants/table-breakpoint.js +10 -0
  40. package/dist/esm/hooks/use-ps-table-overflow.js +19 -0
  41. package/dist/esm/hooks/use-ps-table-reorder.js +89 -15
  42. package/dist/esm/index.js +10 -0
  43. package/dist/esm/interfaces/table-drop.js +1 -0
  44. package/dist/esm/interfaces/table-nesting-announcements.js +1 -0
  45. package/dist/esm/interfaces/table-overflow-edges.js +1 -0
  46. package/dist/esm/interfaces/table-pin-classes.js +1 -0
  47. package/dist/esm/interfaces/table-reorder-nesting-binding.js +1 -0
  48. package/dist/esm/interfaces/table-reorder-nesting.js +1 -0
  49. package/dist/esm/services/ps-table-overflow-service.js +11 -0
  50. package/dist/esm/services/ps-table-pin-service.js +22 -0
  51. package/dist/esm/services/ps-table-reorder-service.js +38 -1
  52. package/dist/hooks/use-ps-table-overflow.d.ts +6 -0
  53. package/dist/hooks/use-ps-table-overflow.js +23 -0
  54. package/dist/hooks/use-ps-table-reorder.d.ts +6 -2
  55. package/dist/hooks/use-ps-table-reorder.js +87 -13
  56. package/dist/index.d.ts +10 -0
  57. package/dist/index.js +10 -0
  58. package/dist/interfaces/table-column.d.ts +3 -0
  59. package/dist/interfaces/table-drop.d.ts +5 -0
  60. package/dist/interfaces/table-drop.js +2 -0
  61. package/dist/interfaces/table-nesting-announcements.d.ts +5 -0
  62. package/dist/interfaces/table-nesting-announcements.js +2 -0
  63. package/dist/interfaces/table-overflow-edges.d.ts +4 -0
  64. package/dist/interfaces/table-overflow-edges.js +2 -0
  65. package/dist/interfaces/table-pin-classes.d.ts +5 -0
  66. package/dist/interfaces/table-pin-classes.js +2 -0
  67. package/dist/interfaces/table-reorder-nesting-binding.d.ts +8 -0
  68. package/dist/interfaces/table-reorder-nesting-binding.js +2 -0
  69. package/dist/interfaces/table-reorder-nesting.d.ts +8 -0
  70. package/dist/interfaces/table-reorder-nesting.js +2 -0
  71. package/dist/interfaces/table-reorder-row-props.d.ts +3 -0
  72. package/dist/interfaces/table-reorder.d.ts +4 -2
  73. package/dist/services/ps-table-overflow-service.d.ts +6 -0
  74. package/dist/services/ps-table-overflow-service.js +15 -0
  75. package/dist/services/ps-table-pin-service.d.ts +5 -0
  76. package/dist/services/ps-table-pin-service.js +26 -0
  77. package/dist/services/ps-table-reorder-service.d.ts +11 -0
  78. package/dist/services/ps-table-reorder-service.js +38 -1
  79. package/package.json +2 -2
@@ -1,17 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Fragment, Suspense, lazy, useDeferredValue, useEffect, useMemo, useRef, useState, } from 'react';
2
+ import { Fragment, Suspense, lazy, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { PsDragGrip, PsSkeletonTable } from '@pallasoft/ps-core-components';
4
4
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
5
5
  import { PsCheckbox } from '@pallasoft/ps-input-components';
6
6
  import { PsCollapse } from '@pallasoft/ps-motion-components';
7
7
  import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual';
8
+ import { PsTableControlClasses } from '../constants/ps-table-control-classes';
9
+ import { PsTableHideBelowClasses } from '../constants/table-breakpoint';
8
10
  import { PsTableConfig } from '../constants/table-config';
9
11
  import { usePsIncrementalCount } from '../hooks/use-ps-incremental-count';
10
12
  import { usePsScrollAnchor } from '../hooks/use-ps-scroll-anchor';
13
+ import { usePsTableOverflow } from '../hooks/use-ps-table-overflow';
11
14
  import { usePsTableStreamStatus } from '../hooks/use-ps-table-stream-status';
12
15
  import { PsTableCellTooltipService } from '../services/ps-table-cell-tooltip-service';
13
16
  import { PsTableColumnSizeService } from '../services/ps-table-column-size-service';
14
17
  import { PsTableGroupService } from '../services/ps-table-group-service';
18
+ import { PsTablePinService } from '../services/ps-table-pin-service';
15
19
  import { PsTableQueryService } from '../services/ps-table-query-service';
16
20
  import { PsTableReorderService } from '../services/ps-table-reorder-service';
17
21
  import { PsTableScrollService } from '../services/ps-table-scroll-service';
@@ -44,7 +48,7 @@ const alignClassNames = {
44
48
  left: 'text-left',
45
49
  right: 'text-right tabular-nums',
46
50
  };
47
- export const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, query, renderExpandedRow, reorder, rows, searchingLabel = PsTableConfig.searchingLabel, searchPlaceholder = PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
51
+ export const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = PsTableConfig.searchingLabel, searchPlaceholder = PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
48
52
  const containerRef = useRef(null);
49
53
  const [uncontrolledPage, setUncontrolledPage] = useState(1);
50
54
  const [uncontrolledQuery, setUncontrolledQuery] = useState(() => PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -69,6 +73,9 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
69
73
  const hasToolbar = isSearchable || hasFilters || Boolean(toolbarActions);
70
74
  const isSelectable = Boolean(getBulkActions);
71
75
  const columnCount = visibleColumns.length + (isSelectable ? 1 : 0) + (reorder ? 1 : 0);
76
+ const pins = PsTablePinService.leading(pinFirstColumn, Boolean(reorder), isSelectable);
77
+ const placementOf = (column, columnIndex) => PallasoftClassNameService.merge(column.hideBelow === undefined ? '' : PsTableHideBelowClasses[column.hideBelow], columnIndex === 0 ? pins.first : '', PsTablePinService.end(column.pinEnd === true));
78
+ const overflow = usePsTableOverflow(`${visibleColumns.map((column) => column.id).join(',')}:${rows.length}`);
72
79
  const deferredSearch = useDeferredValue(search);
73
80
  const queriedRows = useMemo(() => isServerPaginated
74
81
  ? rows
@@ -138,23 +145,48 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
138
145
  safePage,
139
146
  toggledGroupKeys,
140
147
  ]);
148
+ const reorderKey = useCallback((row) => reorder && reorder.groupKey ? reorder.groupKey(row) : PsTableReorderService.treeKey, [reorder]);
149
+ const nestingBinding = useMemo(() => {
150
+ if (reorder === undefined || reorder.nesting === undefined)
151
+ return undefined;
152
+ const { nesting } = reorder;
153
+ const find = (id) => visibleRows.find((candidate) => getRowId(candidate) === id);
154
+ return {
155
+ announcements: nesting.announcements,
156
+ canDrag: (id) => {
157
+ const row = find(id);
158
+ return row !== undefined && nesting.canDrag(row);
159
+ },
160
+ canDrop: (activeId, targetId, placement) => {
161
+ const active = find(activeId);
162
+ const target = find(targetId);
163
+ return (active !== undefined && target !== undefined && nesting.canDrop(active, target, placement));
164
+ },
165
+ onMove: (activeId, targetId, placement) => {
166
+ const active = find(activeId);
167
+ const target = find(targetId);
168
+ if (active !== undefined && target !== undefined)
169
+ nesting.onMove(active, target, placement);
170
+ },
171
+ };
172
+ }, [getRowId, reorder, visibleRows]);
141
173
  const reorderItems = useMemo(() => reorder
142
174
  ? visibleRows.map((row) => {
143
175
  const rowId = getRowId(row);
144
176
  const label = getRowLabel ? getRowLabel(row) : rowId;
145
177
  return {
146
- groupKey: reorder.groupKey(row),
178
+ groupKey: reorderKey(row),
147
179
  handleLabel: reorder.getHandleLabel(label),
148
180
  id: rowId,
149
181
  label,
150
182
  };
151
183
  })
152
- : [], [getRowId, getRowLabel, reorder, visibleRows]);
184
+ : [], [getRowId, getRowLabel, reorder, reorderKey, visibleRows]);
153
185
  const renderOverlayRow = (rowId) => {
154
186
  const row = visibleRows.find((candidate) => getRowId(candidate) === rowId);
155
187
  return row === undefined ? null : (_jsx("table", { className: 'w-full table-fixed border-collapse bg-surface text-left', children: _jsx("tbody", { children: renderRow(PsTableReorderService.plain, row) }) }));
156
188
  };
157
- const renderReorder = (children) => reorder === undefined ? (children(PsTableReorderService.plain)) : (_jsx(Suspense, { fallback: children(PsTableReorderService.plain), children: _jsx(LazyReorderFrame, { announcements: reorder.announcements, enabled: reorder.enabled, instructions: reorder.instructions, items: reorderItems, onReorder: reorder.onReorder, renderOverlay: renderOverlayRow, children: children }) }));
189
+ const renderReorder = (children) => reorder === undefined ? (children(PsTableReorderService.plain)) : (_jsx(Suspense, { fallback: children(PsTableReorderService.plain), children: _jsx(LazyReorderFrame, { announcements: reorder.announcements, enabled: reorder.enabled, instructions: reorder.instructions, items: reorderItems, nesting: nestingBinding, onReorder: reorder.onReorder, renderOverlay: renderOverlayRow, children: children }) }));
158
190
  const changePage = (next) => {
159
191
  if (onPageChange) {
160
192
  onPageChange(next);
@@ -238,7 +270,11 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
238
270
  const rowLabel = getRowLabel ? getRowLabel(row) : rowId;
239
271
  const Row = api.Row;
240
272
  const rowExtras = api.isActive
241
- ? { ...api.rowProps(rowId), measureRef: virtualItem ? virtualizer.measureElement : undefined }
273
+ ? {
274
+ ...api.rowProps(rowId),
275
+ handleCellClassName: pins.reorder,
276
+ measureRef: virtualItem ? virtualizer.measureElement : undefined,
277
+ }
242
278
  : { ref: virtualItem ? virtualizer.measureElement : undefined };
243
279
  return (_jsxs(Fragment, { children: [_jsxs(Row, { ...rowExtras, "data-index": virtualItem ? virtualItem.index : undefined, "aria-expanded": renderExpandedRow ? isExpanded : undefined, "aria-label": onRowClick ? rowLabel : undefined, className: PallasoftClassNameService.merge('align-middle transition-colors', motion ? motion.className : '', onRowClick ? 'ps-focus-ring cursor-pointer hover:bg-mist/60' : 'hover:bg-mist/40', isExpanded ? 'bg-mist/40' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
244
280
  ? (event) => {
@@ -251,13 +287,13 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
251
287
  }
252
288
  : undefined, style: motion && motion.delayMs !== undefined
253
289
  ? { animationDelay: `${motion.delayMs}ms` }
254
- : undefined, tabIndex: onRowClick ? 0 : undefined, children: [reorder && !api.isActive ? _jsx("td", { className: 'w-9 pl-3 pr-0' }) : null, isSelectable ? (_jsx("td", { className: 'w-10 pl-4 pr-0', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(PsCheckbox, { isSelected: selectedIdSet.has(rowId), label: `Select ${rowLabel}`, onChange: () => toggleRow(rowId) }) })) : null, visibleColumns.map((column) => (_jsx("td", { className: PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined ? column.align : 'left'], column.wrap ? '' : 'whitespace-nowrap', (layout === 'fixed' && !column.wrap) ||
290
+ : undefined, tabIndex: onRowClick ? 0 : undefined, children: [reorder && !api.isActive ? (_jsx("td", { className: PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder) })) : null, isSelectable ? (_jsx("td", { className: PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(PsCheckbox, { isSelected: selectedIdSet.has(rowId), label: `Select ${rowLabel}`, onChange: () => toggleRow(rowId) }) })) : null, visibleColumns.map((column, columnIndex) => (_jsx("td", { className: PallasoftClassNameService.merge(densityClassNames[density].cell, placementOf(column, columnIndex), alignClassNames[column.align !== null && column.align !== undefined ? column.align : 'left'], column.wrap ? '' : 'whitespace-nowrap', (layout === 'fixed' && !column.wrap) ||
255
291
  PsTableColumnSizeService.maxWidthOf(column) !== undefined
256
292
  ? 'truncate'
257
293
  : '', 'text-sm text-ink'), style: columnStyle(column), title: PsTableCellTooltipService.forCell(column, row), children: column.render(row) }, column.id)))] }), renderExpandedRow ? (_jsx("tr", { "aria-hidden": !isExpanded, className: isExpanded ? 'border-t-0' : 'border-0', children: _jsx("td", { className: 'p-0', colSpan: columnCount, children: _jsx(PsCollapse, { isOpen: isExpanded, children: _jsx("div", { className: 'w-0 min-w-full border-l-2 border-primary bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) }) }) })) : null] }, rowId));
258
294
  };
259
295
  const renderEntries = (api, entries) => reorder
260
- ? PsTableReorderService.runs(entries, (entry) => reorder.groupKey(entry.row), (entry) => getRowId(entry.row)).map((run) => api.run(run.id, run.items.map((entry) => getRowId(entry.row)), run.items.map((entry) => renderRow(api, entry.row, entry.motion, entry.virtualItem))))
296
+ ? PsTableReorderService.runs(entries, (entry) => reorderKey(entry.row), (entry) => getRowId(entry.row)).map((run) => api.run(run.id, run.items.map((entry) => getRowId(entry.row)), run.items.map((entry) => renderRow(api, entry.row, entry.motion, entry.virtualItem))))
261
297
  : entries.map((entry) => renderRow(api, entry.row, entry.motion, entry.virtualItem));
262
298
  if (isLoading)
263
299
  return (_jsx(PsSkeletonTable, { className: className, columns: visibleColumns.length, hasToolbar: hasToolbar, rows: PsTableConfig.skeletonRows }));
@@ -276,15 +312,15 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
276
312
  ? grouping.renderGroupSummary(group.rows)
277
313
  : undefined }), _jsx(PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
278
314
  })
279
- : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? (_jsx(PsTableSearchingState, { label: searchingLabel, skeletonRows: PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? (_jsxs("div", { className: 'px-4 py-10 text-center', children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("button", { className: 'ps-focus-ring mt-2 rounded-full px-3 py-1.5 text-sm font-semibold text-primary', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? (_jsx("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, _jsx("div", { className: PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto overflow-y-hidden', mobileLayout === 'cards' ? 'hidden sm:block' : ''), children: renderReorder((api) => (_jsxs("table", { className: PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { className: 'bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', children: _jsxs("tr", { children: [reorder ? (_jsx("th", { className: 'w-9 pl-3 pr-0', scope: 'col', children: reorder.enabled ? (_jsx("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? (_jsx("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: _jsx(PsDragGrip, {}) })) : null })) : null, isSelectable ? (_jsx("th", { className: 'w-10 pl-4 pr-0', scope: 'col', children: _jsx(PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column) => {
315
+ : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? (_jsx(PsTableSearchingState, { label: searchingLabel, skeletonRows: PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? (_jsxs("div", { className: 'px-4 py-10 text-center', children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? (_jsx("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, _jsx("div", { className: PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => (_jsxs("table", { className: PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { className: 'bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', children: _jsxs("tr", { children: [reorder ? (_jsx("th", { className: PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? (_jsx("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? (_jsx("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: _jsx(PsDragGrip, {}) })) : null })) : null, isSelectable ? (_jsx("th", { className: PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: _jsx(PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
280
316
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
281
317
  const isSorted = sort !== null && sort.columnId === column.id;
282
318
  return (_jsx("th", { "aria-sort": isSorted
283
319
  ? sort.direction
284
320
  : column.sortValue && isQueryable
285
321
  ? 'none'
286
- : undefined, className: PallasoftClassNameService.merge(densityClassNames[density].head, alignClassNames[align], 'whitespace-nowrap'), scope: 'col', style: columnStyle(column), children: column.sortValue && isQueryable ? (_jsx(PsTableSortHeader, { align: align, columnId: column.id, label: column.label, onSort: updateSort, sort: sort })) : (column.label) }, column.id));
287
- })] }) }), _jsxs("tbody", { className: 'divide-y divide-line', ref: bodyRef, children: [visibleRows.length === 0 && isSearching ? (_jsx("tr", { children: _jsx("td", { className: 'p-0', colSpan: columnCount, children: _jsx(PsTableSearchingState, { label: searchingLabel, skeletonRows: PsTableConfig.searchingSkeletonRows }) }) })) : null, visibleRows.length === 0 && isQueried && !isSearching ? (_jsx("tr", { children: _jsxs("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("p", { className: 'mt-1 text-sm text-ink/55', children: PsTableConfig.noMatchesDescription }), _jsx("button", { className: 'ps-focus-ring mt-3 rounded-full px-3 py-1.5 text-sm font-semibold text-primary hover:underline', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, grouping
322
+ : undefined, className: PallasoftClassNameService.merge(densityClassNames[density].head, alignClassNames[align], placementOf(column, columnIndex), 'whitespace-nowrap'), scope: 'col', style: columnStyle(column), children: column.sortValue && isQueryable ? (_jsx(PsTableSortHeader, { align: align, columnId: column.id, label: column.label, onSort: updateSort, sort: sort })) : (column.label) }, column.id));
323
+ })] }) }), _jsxs("tbody", { className: 'divide-y divide-line', ref: bodyRef, children: [visibleRows.length === 0 && isSearching ? (_jsx("tr", { children: _jsx("td", { className: 'p-0', colSpan: columnCount, children: _jsx(PsTableSearchingState, { label: searchingLabel, skeletonRows: PsTableConfig.searchingSkeletonRows }) }) })) : null, visibleRows.length === 0 && isQueried && !isSearching ? (_jsx("tr", { children: _jsxs("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("p", { className: 'mt-1 text-sm text-ink/55', children: PsTableConfig.noMatchesDescription }), _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'mt-3 text-primary hover:underline'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, grouping
288
324
  ? groups.map((group, groupIndex) => {
289
325
  const isExpanded = isGroupExpanded(group.key, groupIndex);
290
326
  return (_jsxs(Fragment, { children: [_jsx("tr", { children: _jsx("td", { className: 'p-0', colSpan: columnCount, children: _jsx(PsTableGroupHeader, { count: group.rows.length, isExpanded: isExpanded, label: grouping.getGroupLabel(group.key, group.rows), onToggle: () => toggleGroup(group.key, isExpanded), summary: grouping.renderGroupSummary
@@ -305,7 +341,7 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
305
341
  }))),
306
342
  _jsx("tr", { "aria-hidden": 'true', children: _jsx("td", { className: 'p-0', colSpan: columnCount, style: { height: virtualBottom } }) }, 'virtual-bottom'),
307
343
  ]
308
- : renderEntries(api, visibleRows.map((row) => ({ row })))] }), hasFooter ? (_jsx("tfoot", { className: 'border-t-2 border-line bg-mist/50 text-sm font-semibold text-ink', children: _jsxs("tr", { children: [reorder ? _jsx("td", {}) : null, isSelectable ? _jsx("td", {}) : null, visibleColumns.map((column) => (_jsx("td", { className: PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined
344
+ : renderEntries(api, visibleRows.map((row) => ({ row })))] }), hasFooter ? (_jsx("tfoot", { className: 'border-t-2 border-line bg-mist/50 text-sm font-semibold text-ink', children: _jsxs("tr", { children: [reorder ? _jsx("td", { className: pins.reorder }) : null, isSelectable ? _jsx("td", { className: pins.select }) : null, visibleColumns.map((column, columnIndex) => (_jsx("td", { className: PallasoftClassNameService.merge(densityClassNames[density].cell, placementOf(column, columnIndex), alignClassNames[column.align !== null && column.align !== undefined
309
345
  ? column.align
310
346
  : 'left'], 'whitespace-nowrap'), children: column.footer }, column.id)))] }) })) : null] }))) }), visibleRows.length > 0 && isSearching ? (_jsx(PsTableSearchingState, { className: 'border-t border-line py-3', label: searchingLabel })) : null, pageSize && (!grouping || isServerPaginated) ? (_jsx(PsTablePagination, { isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null] })] }));
311
347
  };
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
3
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsTableControlClasses } from '../constants/ps-table-control-classes';
4
5
  export const PsTableBulkBar = ({ actions, onClear, selectedCount }) => {
5
6
  const safeActions = actions.filter((action) => !action.isDanger);
6
7
  const dangerActions = actions.filter((action) => action.isDanger);
7
- const renderAction = (action) => (_jsx("button", { className: PallasoftClassNameService.merge('ps-focus-ring inline-flex min-h-9 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-sm font-semibold transition-colors disabled:opacity-40', action.isDanger ? 'text-danger hover:bg-danger/10' : 'text-ink hover:bg-ink/5'), disabled: action.isDisabled, onClick: action.onSelect, type: 'button', children: action.label }, action.id));
8
- return (_jsxs("div", { "aria-live": 'polite', className: 'flex min-h-12 items-center gap-2 rounded-2xl border border-primary/30 bg-primary/10 py-1.5 pl-2 pr-1.5', children: [_jsxs("span", { className: 'inline-flex min-h-9 shrink-0 items-center rounded-full bg-primary px-3 text-sm font-semibold tabular-nums text-white', children: [selectedCount, " selected"] }), _jsxs("div", { className: 'ps-scrollbar-hidden flex min-w-0 flex-1 items-center gap-1 overflow-x-auto', children: [safeActions.map(renderAction), dangerActions.length > 0 && safeActions.length > 0 ? (_jsx("span", { "aria-hidden": 'true', className: 'mx-1 h-5 w-px shrink-0 bg-line' })) : null, dangerActions.map(renderAction)] }), _jsx("button", { "aria-label": 'Clear selection', className: 'ps-focus-ring inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-ink/60 transition-colors hover:bg-ink/5 hover:text-ink', onClick: onClear, title: 'Clear selection', type: 'button', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Close }) })] }));
8
+ const renderAction = (action) => (_jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.bulkAction, action.isDanger ? 'text-danger hover:bg-danger/10' : 'text-ink hover:bg-ink/5'), disabled: action.isDisabled, onClick: action.onSelect, type: 'button', children: action.label }, action.id));
9
+ return (_jsxs("div", { "aria-live": 'polite', className: PsTableControlClasses.bulkBar, children: [_jsxs("span", { className: PsTableControlClasses.bulkCount, children: [selectedCount, " selected"] }), _jsxs("div", { className: 'ps-scrollbar-hidden flex min-w-0 flex-1 items-center gap-1 overflow-x-auto', children: [safeActions.map(renderAction), dangerActions.length > 0 && safeActions.length > 0 ? (_jsx("span", { "aria-hidden": 'true', className: 'mx-1 h-5 w-px shrink-0 bg-line' })) : null, dangerActions.map(renderAction)] }), _jsx("button", { "aria-label": 'Clear selection', className: PsTableControlClasses.iconButton, onClick: onClear, title: 'Clear selection', type: 'button', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Close }) })] }));
9
10
  };
@@ -4,10 +4,11 @@ import { PallasoftZIndex } from '@pallasoft/ps-design-system';
4
4
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
5
5
  import { PsCheckbox } from '@pallasoft/ps-input-components';
6
6
  import { Button, Dialog, DialogTrigger, Popover } from 'react-aria-components';
7
+ import { PsTableControlClasses } from '../constants/ps-table-control-classes';
7
8
  export const PsTableColumnPicker = ({ columns, hiddenColumnIds, onChange, }) => {
8
9
  const zIndex = usePsLayerZIndex(PallasoftZIndex.Dropdown);
9
10
  const visibleCount = columns.filter((column) => !hiddenColumnIds.includes(column.id)).length;
10
- return (_jsxs(DialogTrigger, { children: [_jsxs(Button, { className: 'ps-input-control ps-control-trigger ps-focus-ring ps-control ps-control-padding inline-flex min-w-11 items-center justify-center gap-2 rounded-ps-control text-sm font-semibold text-ink outline-none transition-colors hover:border-primary/40', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4 sm:hidden', iconKey: PsIconKey.Columns }), _jsx("span", { className: 'sr-only sm:not-sr-only', children: "Columns" }), _jsx(PsIcon, { "aria-hidden": 'true', className: 'hidden h-4 w-4 sm:block', iconKey: PsIconKey.Chevron })] }), _jsx(Popover, { className: 'ps-floating-panel w-60 outline-none', offset: 6, placement: 'bottom end', style: { zIndex }, children: _jsx(Dialog, { "aria-label": 'Columns', className: 'flex flex-col gap-0.5 p-1.5 outline-none', children: _jsx(PsLayer, { zIndex: zIndex, children: columns.map((column) => {
11
+ return (_jsxs(DialogTrigger, { children: [_jsxs(Button, { className: PsTableControlClasses.toolbarButton, children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4 sm:hidden', iconKey: PsIconKey.Columns }), _jsx("span", { className: 'sr-only sm:not-sr-only', children: "Columns" }), _jsx(PsIcon, { "aria-hidden": 'true', className: 'hidden h-4 w-4 sm:block', iconKey: PsIconKey.Chevron })] }), _jsx(Popover, { className: 'ps-floating-panel w-60 outline-none', offset: 6, placement: 'bottom end', style: { zIndex }, children: _jsx(Dialog, { "aria-label": 'Columns', className: 'flex flex-col gap-0.5 p-1.5 outline-none', children: _jsx(PsLayer, { zIndex: zIndex, children: columns.map((column) => {
11
12
  const isVisible = !hiddenColumnIds.includes(column.id);
12
13
  return (_jsx(PsCheckbox, { className: 'ps-floating-option w-full', isDisabled: isVisible && visibleCount <= 1, isSelected: isVisible, label: column.label, onChange: () => onChange(isVisible
13
14
  ? [...hiddenColumnIds, column.id]
@@ -1,3 +1,3 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
3
- export const PsTableExportButton = ({ label, onExport }) => (_jsxs("button", { className: 'ps-focus-ring ps-liquid ps-liquid-control inline-flex min-h-10 min-w-10 items-center justify-center gap-2 rounded-full px-3 text-sm font-semibold text-ink transition-colors hover:border-primary/40 sm:px-4', onClick: onExport, type: 'button', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: PsIconKey.Download }), _jsx("span", { className: 'sr-only sm:not-sr-only', children: label })] }));
3
+ export const PsTableExportButton = ({ label, onExport }) => (_jsxs("button", { className: 'ps-focus-ring ps-liquid ps-liquid-control ps-control ps-control-padding inline-flex min-w-ps-control items-center justify-center gap-2 rounded-ps-control border border-ps-control text-sm font-semibold text-ink transition-colors hover:border-primary/40', onClick: onExport, type: 'button', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: PsIconKey.Download }), _jsx("span", { className: 'sr-only sm:not-sr-only', children: label })] }));
@@ -1,4 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
3
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
- export const PsTableGroupHeader = ({ count, isExpanded, label, onToggle, summary, }) => (_jsxs("button", { "aria-expanded": isExpanded, className: 'ps-focus-ring flex w-full flex-wrap items-center gap-x-3 gap-y-2 bg-mist/60 px-4 py-3 text-left transition-colors hover:bg-mist', onClick: onToggle, type: 'button', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: PallasoftClassNameService.merge('h-4 w-4 shrink-0 text-ink/50 transition-transform duration-200', isExpanded ? '' : '-rotate-90'), iconKey: PsIconKey.Chevron }), _jsx("span", { className: 'text-sm font-semibold text-ink', children: label }), _jsx("span", { className: 'rounded-full bg-surface px-2 py-0.5 text-xs font-semibold tabular-nums text-ink/60', children: count }), summary ? (_jsx("span", { className: 'ml-auto flex flex-wrap items-center justify-end gap-2', children: summary })) : null] }));
4
+ export const PsTableGroupHeader = ({ count, isExpanded, label, onToggle, summary, }) => (_jsxs("button", { "aria-expanded": isExpanded, className: 'ps-focus-ring flex w-full flex-wrap items-center gap-x-3 gap-y-2 bg-mist/60 px-4 py-3 text-left transition-colors hover:bg-mist', onClick: onToggle, type: 'button', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: PallasoftClassNameService.merge('h-4 w-4 shrink-0 text-ink/50 transition-transform duration-200', isExpanded ? '' : '-rotate-90'), iconKey: PsIconKey.Chevron }), _jsx("span", { className: 'text-sm font-semibold text-ink', children: label }), _jsx("span", { className: 'rounded-ps-control bg-surface px-2 py-0.5 text-xs font-semibold tabular-nums text-ink/60', children: count }), summary ? (_jsx("span", { className: 'ml-auto flex flex-wrap items-center justify-end gap-2', children: summary })) : null] }));
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsTableControlClasses } from '../constants/ps-table-control-classes';
3
4
  export const PsTablePagination = ({ className, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
4
5
  const firstRow = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
5
6
  const lastRow = Math.min(page * pageSize, totalRows);
6
- const buttonClassName = 'ps-focus-ring inline-flex min-h-9 items-center rounded-full border border-line px-3.5 text-sm font-semibold text-ink transition-colors disabled:cursor-not-allowed disabled:opacity-40 enabled:hover:border-primary/40';
7
7
  return (_jsxs("nav", { "aria-label": 'Pagination', className: PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-3 border-t border-line px-4 py-3', className), children: [_jsx("p", { className: 'text-sm text-ink/55', children: totalRows === 0
8
8
  ? 'No rows'
9
- : `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx("button", { className: buttonClassName, disabled: page <= 1, onClick: () => onPageChange(page - 1), type: 'button', children: "Previous" }), _jsxs("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", Math.min(page, pageCount), " of ", pageCount] }), _jsx("button", { className: buttonClassName, disabled: page >=
9
+ : `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx("button", { className: PsTableControlClasses.paginationButton, disabled: page <= 1, onClick: () => onPageChange(page - 1), type: 'button', children: "Previous" }), _jsxs("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", Math.min(page, pageCount), " of ", pageCount] }), _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: page >=
10
10
  (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount)), onClick: () => onPageChange(page + 1), type: 'button', children: "Next" })] })] }));
11
11
  };
@@ -3,24 +3,37 @@ import { DndContext } from '@dnd-kit/core';
3
3
  import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
4
4
  import { PsDragClasses, PsDragListLiftFrames, PsDragOverlay } from '@pallasoft/ps-core-components';
5
5
  import { usePsTableReorder } from '../hooks/use-ps-table-reorder';
6
+ import { PsTableReorderService } from '../services/ps-table-reorder-service';
6
7
  import { PsTableSortableRow } from './ps-table-sortable-row';
7
8
  export const PsTableReorderFrame = ({ children, renderOverlay, ...input }) => {
8
- const { activeId, activeRunId, contextProps, placements } = usePsTableReorder(input);
9
+ const { activeId, activeRunId, contextProps, drop, placements } = usePsTableReorder(input);
9
10
  const itemById = new Map(input.items.map((item) => [item.id, item]));
11
+ const { nesting } = input;
10
12
  const api = {
11
13
  isActive: true,
12
14
  Row: PsTableSortableRow,
13
15
  rowProps: (rowId) => {
14
16
  const item = itemById.get(rowId);
15
17
  const placement = placements.get(rowId);
18
+ if (nesting !== undefined)
19
+ return {
20
+ dropPlacement: drop !== null && drop.overId === rowId ? drop.placement : null,
21
+ handleLabel: item === undefined ? '' : item.handleLabel,
22
+ hasHandle: item !== undefined && input.enabled && nesting.canDrag(rowId),
23
+ isDisabled: !input.enabled,
24
+ isDropTarget: item !== undefined,
25
+ rowId,
26
+ };
16
27
  return {
28
+ dropPlacement: null,
17
29
  handleLabel: item === undefined ? '' : item.handleLabel,
18
30
  hasHandle: item !== undefined && input.enabled && placement !== undefined && placement.size > 1,
19
31
  isDisabled: activeRunId !== null && (placement === undefined || placement.runId !== activeRunId),
32
+ isDropTarget: false,
20
33
  rowId,
21
34
  };
22
35
  },
23
- run: (runId, ids, runChildren) => (_jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: runChildren }, runId)),
36
+ run: (runId, ids, runChildren) => (_jsx(SortableContext, { items: ids, strategy: nesting === undefined ? verticalListSortingStrategy : PsTableReorderService.still, children: runChildren }, runId)),
24
37
  };
25
38
  return (_jsxs(DndContext, { ...contextProps, children: [children(api), _jsx(PsDragOverlay, { className: PsDragClasses.lifted, frames: PsDragListLiftFrames, children: activeId === null ? null : renderOverlay(activeId) })] }));
26
39
  };
@@ -1,4 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { PsDragGrip } from '@pallasoft/ps-core-components';
3
3
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
4
- export const PsTableReorderHandle = ({ activatorRef, className, label, ...buttonProps }) => (_jsx("button", { ...buttonProps, "aria-label": label, className: PallasoftClassNameService.merge('ps-focus-ring flex h-7 w-6 shrink-0 cursor-grab touch-none items-center justify-center rounded-md text-ink/40 transition-colors hover:bg-mist hover:text-ink active:cursor-grabbing', className), onClick: (event) => event.stopPropagation(), ref: activatorRef, title: label, type: 'button', children: _jsx(PsDragGrip, {}) }));
4
+ export const PsTableReorderHandle = ({ activatorRef, className, label, ...buttonProps }) => (_jsx("button", { ...buttonProps, "aria-label": label, className: PallasoftClassNameService.merge('ps-focus-ring ps-touch-target relative flex h-7 w-6 shrink-0 cursor-grab touch-none items-center justify-center rounded-ps-control text-ink/40 transition-colors hover:bg-mist hover:text-ink active:cursor-grabbing', className), onClick: (event) => event.stopPropagation(), ref: activatorRef, title: label, type: 'button', children: _jsx(PsDragGrip, {}) }));
@@ -8,7 +8,7 @@ export const PsTableSortHeader = ({ align, columnId, label, onSort, sort, }) =>
8
8
  : sort.direction === 'ascending'
9
9
  ? PsIconKey.SortAscending
10
10
  : PsIconKey.SortDescending;
11
- return (_jsxs("button", { className: PallasoftClassNameService.merge('ps-focus-ring group inline-flex items-center gap-1 rounded-md uppercase tracking-[0.18em] transition-colors hover:text-ink', align === 'right' ? 'flex-row-reverse' : '', isSorted ? 'text-ink' : ''), onClick: () => onSort(columnId), type: 'button', children: [_jsx("span", { children: label }), _jsx(PsIcon, { "aria-hidden": 'true', className: PallasoftClassNameService.merge('block h-3.5 w-3.5 shrink-0', isSorted
11
+ return (_jsxs("button", { className: PallasoftClassNameService.merge('ps-focus-ring group inline-flex items-center gap-1 rounded-ps-control uppercase tracking-[0.18em] transition-colors hover:text-ink', align === 'right' ? 'flex-row-reverse' : '', isSorted ? 'text-ink' : ''), onClick: () => onSort(columnId), type: 'button', children: [_jsx("span", { children: label }), _jsx(PsIcon, { "aria-hidden": 'true', className: PallasoftClassNameService.merge('block h-3.5 w-3.5 shrink-0', isSorted
12
12
  ? 'text-primary'
13
- : 'opacity-0 group-hover:opacity-60 group-focus-visible:opacity-60'), iconKey: iconKey })] }));
13
+ : 'opacity-0 group-hover:opacity-60 group-focus-visible:opacity-60 [@media(hover:none)]:opacity-40'), iconKey: iconKey })] }));
14
14
  };
@@ -2,10 +2,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useSortable } from '@dnd-kit/sortable';
3
3
  import { CSS } from '@dnd-kit/utilities';
4
4
  import { PsDragConfig } from '@pallasoft/ps-core-components';
5
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
6
+ import { PsTableDropClasses } from '../constants/ps-table-drop-classes';
5
7
  import { PsTableReorderHandle } from './ps-table-reorder-handle';
6
- export const PsTableSortableRow = ({ children, className, handleLabel, hasHandle, isDisabled, measureRef, rowId, style, ...rowProps }) => {
7
- const { attributes, isDragging, listeners, setActivatorNodeRef, setNodeRef, transform, transition, } = useSortable({ disabled: isDisabled || !hasHandle, id: rowId });
8
- return (_jsxs("tr", { ...rowProps, className: className, ref: (element) => {
8
+ export const PsTableSortableRow = ({ children, className, dropPlacement = null, handleCellClassName, handleLabel, hasHandle, isDisabled, isDropTarget = false, measureRef, rowId, style, ...rowProps }) => {
9
+ const { attributes, isDragging, listeners, setActivatorNodeRef, setNodeRef, transform, transition, } = useSortable({
10
+ disabled: {
11
+ draggable: isDisabled || !hasHandle,
12
+ droppable: isDisabled || (!hasHandle && !isDropTarget),
13
+ },
14
+ id: rowId,
15
+ });
16
+ return (_jsxs("tr", { ...rowProps, className: PallasoftClassNameService.merge(className, dropPlacement === null ? '' : PsTableDropClasses[dropPlacement]), ref: (element) => {
9
17
  setNodeRef(element);
10
18
  if (measureRef)
11
19
  measureRef(element);
@@ -14,5 +22,5 @@ export const PsTableSortableRow = ({ children, className, handleLabel, hasHandle
14
22
  opacity: isDragging ? PsDragConfig.placeholderOpacity : style?.opacity,
15
23
  transform: CSS.Translate.toString(transform),
16
24
  transition,
17
- }, children: [_jsx("td", { className: 'w-9 pl-3 pr-0', onClick: (event) => event.stopPropagation(), children: hasHandle ? (_jsx(PsTableReorderHandle, { ...attributes, ...listeners, activatorRef: setActivatorNodeRef, label: handleLabel })) : null }), children] }));
25
+ }, children: [_jsx("td", { className: PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', handleCellClassName), onClick: (event) => event.stopPropagation(), children: hasHandle ? (_jsx(PsTableReorderHandle, { ...attributes, ...listeners, activatorRef: setActivatorNodeRef, label: handleLabel })) : null }), children] }));
18
26
  };
@@ -3,12 +3,12 @@ import { PsLayer, usePsLayerZIndex } from '@pallasoft/ps-core-components';
3
3
  import { PallasoftClassNameService, PallasoftZIndex } from '@pallasoft/ps-design-system';
4
4
  import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
5
5
  import { Button, Dialog, DialogTrigger, Popover } from 'react-aria-components';
6
+ import { PsTableControlClasses } from '../constants/ps-table-control-classes';
6
7
  import { PsTableQueryService } from '../services/ps-table-query-service';
7
8
  import { PsTableFilterField } from './ps-table-filter-field';
8
- const toolbarButtonClassName = 'ps-input-control ps-control-trigger ps-focus-ring ps-control ps-control-padding inline-flex min-w-11 items-center justify-center gap-2 rounded-ps-control text-sm font-semibold text-ink outline-none transition-colors hover:border-primary/40';
9
9
  export const PsTableToolbar = ({ actions, columns, filters, isSearchable, onClearFilters, onFilterChange, onSearchChange, rows, search, searchPlaceholder, }) => {
10
10
  const filterableColumns = columns.filter((column) => column.filter);
11
11
  const zIndex = usePsLayerZIndex(PallasoftZIndex.Dropdown);
12
12
  const activeCount = filterableColumns.filter((column) => PsTableQueryService.isActive(filters[column.id])).length;
13
- return (_jsxs("div", { className: 'flex flex-wrap items-center gap-2', children: [isSearchable ? (_jsxs("label", { className: 'relative basis-full sm:min-w-[12rem] sm:flex-1 sm:basis-auto sm:max-w-xs', children: [_jsx("span", { className: 'sr-only', children: searchPlaceholder }), _jsx(PsIcon, { "aria-hidden": 'true', className: 'pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-ink/45', iconKey: PsIconKey.Search }), _jsx("input", { className: 'ps-input-control ps-focus-ring ps-control rounded-ps-control pl-10 text-sm', onChange: (event) => onSearchChange(event.target.value), placeholder: searchPlaceholder, type: 'search', value: search })] })) : null, _jsxs("div", { className: 'ps-scrollbar-hidden flex w-full min-w-0 flex-1 items-center gap-2 overflow-x-auto sm:w-auto', children: [filterableColumns.length > 0 ? (_jsxs(DialogTrigger, { children: [_jsxs(Button, { className: toolbarButtonClassName, children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: PsIconKey.Filter }), _jsx("span", { className: 'sr-only sm:not-sr-only', children: "Filters" }), activeCount > 0 ? (_jsx("span", { className: 'inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-semibold text-[color:var(--ps-color-primary-contrast)]', children: activeCount })) : null] }), _jsx(Popover, { className: 'ps-floating-panel ps-scrollbar-hidden max-h-[min(34rem,80vh)] w-[min(22rem,calc(100vw-2rem))] overflow-y-auto outline-none', offset: 6, placement: 'bottom start', style: { zIndex }, children: _jsx(Dialog, { "aria-label": 'Filters', className: 'flex flex-col gap-4 p-4 outline-none', children: _jsxs(PsLayer, { zIndex: zIndex, children: [filterableColumns.map((column) => (_jsx(PsTableFilterField, { kind: column.filter ? column.filter.kind : 'text', label: column.label, onChange: (value) => onFilterChange(column.id, value), options: PsTableQueryService.options(rows, column), value: filters[column.id] ? filters[column.id] : {} }, column.id))), _jsx("div", { className: 'flex justify-end border-t border-line pt-3', children: _jsx("button", { className: 'ps-focus-ring rounded-lg px-2 py-1 text-sm font-semibold text-ink/65 hover:text-ink disabled:opacity-40', disabled: activeCount === 0, onClick: onClearFilters, type: 'button', children: "Clear all" }) })] }) }) })] })) : null, activeCount > 0 ? (_jsx("button", { className: PallasoftClassNameService.merge('ps-focus-ring rounded-full px-3 py-2 text-sm font-semibold text-ink/65 hover:text-ink'), onClick: onClearFilters, type: 'button', children: "Clear filters" })) : null, actions ? _jsx("div", { className: 'ml-auto flex shrink-0 items-center gap-2', children: actions }) : null] })] }));
13
+ return (_jsxs("div", { className: 'flex flex-wrap items-center gap-2', children: [isSearchable ? (_jsxs("label", { className: 'relative basis-full sm:min-w-[12rem] sm:flex-1 sm:basis-auto sm:max-w-xs', children: [_jsx("span", { className: 'sr-only', children: searchPlaceholder }), _jsx(PsIcon, { "aria-hidden": 'true', className: 'pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-ink/45', iconKey: PsIconKey.Search }), _jsx("input", { className: 'ps-input-control ps-focus-ring ps-control rounded-ps-control pl-10 text-sm', onChange: (event) => onSearchChange(event.target.value), placeholder: searchPlaceholder, type: 'search', value: search })] })) : null, _jsxs("div", { className: 'ps-scrollbar-hidden flex w-full min-w-0 flex-1 items-center gap-2 overflow-x-auto sm:w-auto', children: [filterableColumns.length > 0 ? (_jsxs(DialogTrigger, { children: [_jsxs(Button, { className: PsTableControlClasses.toolbarButton, children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: PsIconKey.Filter }), _jsx("span", { className: 'sr-only sm:not-sr-only', children: "Filters" }), activeCount > 0 ? (_jsx("span", { className: 'inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-semibold text-[color:var(--ps-color-primary-contrast)]', children: activeCount })) : null] }), _jsx(Popover, { className: 'ps-floating-panel ps-scrollbar-hidden max-h-[min(34rem,80vh)] w-[min(22rem,calc(100vw-2rem))] overflow-y-auto outline-none', offset: 6, placement: 'bottom start', style: { zIndex }, children: _jsx(Dialog, { "aria-label": 'Filters', className: 'flex flex-col gap-4 p-4 outline-none', children: _jsxs(PsLayer, { zIndex: zIndex, children: [filterableColumns.map((column) => (_jsx(PsTableFilterField, { kind: column.filter ? column.filter.kind : 'text', label: column.label, onChange: (value) => onFilterChange(column.id, value), options: PsTableQueryService.options(rows, column), value: filters[column.id] ? filters[column.id] : {} }, column.id))), _jsx("div", { className: 'flex justify-end border-t border-line pt-3', children: _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'text-ink/65 hover:text-ink disabled:opacity-40'), disabled: activeCount === 0, onClick: onClearFilters, type: 'button', children: "Clear all" }) })] }) }) })] })) : null, activeCount > 0 ? (_jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'text-ink/65 hover:text-ink'), onClick: onClearFilters, type: 'button', children: "Clear filters" })) : null, actions ? _jsx("div", { className: 'ml-auto flex shrink-0 items-center gap-2', children: actions }) : null] })] }));
14
14
  };
@@ -0,0 +1,9 @@
1
+ export const PsTableControlClasses = {
2
+ bulkAction: 'ps-focus-ring ps-control-compact ps-control-padding-compact inline-flex shrink-0 items-center whitespace-nowrap rounded-ps-control text-sm font-semibold transition-colors disabled:opacity-40',
3
+ bulkBar: 'ps-control-large flex items-center gap-2 rounded-ps-control border border-primary/30 bg-primary/10 py-1 pl-1 pr-1',
4
+ bulkCount: 'ps-control-compact ps-control-padding-compact inline-flex shrink-0 items-center rounded-ps-control bg-primary text-sm font-semibold tabular-nums text-white',
5
+ iconButton: 'ps-focus-ring ps-control-square ps-control-compact inline-flex shrink-0 items-center justify-center rounded-ps-control text-ink/60 transition-colors hover:bg-ink/5 hover:text-ink',
6
+ paginationButton: 'ps-focus-ring ps-control-compact ps-control-padding-compact inline-flex items-center rounded-ps-control border border-ps-control border-line text-sm font-semibold text-ink transition-colors disabled:cursor-not-allowed disabled:opacity-40 enabled:hover:border-primary/40',
7
+ textButton: 'ps-focus-ring ps-control-compact ps-control-padding-compact inline-flex items-center rounded-ps-control text-sm font-semibold',
8
+ toolbarButton: 'ps-input-control ps-control-trigger ps-focus-ring ps-control ps-control-padding inline-flex min-w-ps-control items-center justify-center gap-2 rounded-ps-control text-sm font-semibold text-ink outline-none transition-colors hover:border-primary/40',
9
+ };
@@ -0,0 +1,5 @@
1
+ export const PsTableDropClasses = {
2
+ after: '[&>td]:shadow-[inset_0_-2px_0_0_rgb(var(--ps-color-primary-rgb))]',
3
+ before: '[&>td]:shadow-[inset_0_2px_0_0_rgb(var(--ps-color-primary-rgb))]',
4
+ into: 'bg-primary/10 [&>td]:shadow-[inset_0_0_0_2px_rgb(var(--ps-color-primary-rgb)/0.5)]',
5
+ };
@@ -0,0 +1,5 @@
1
+ export const PsTableDropPlacement = {
2
+ after: 'after',
3
+ before: 'before',
4
+ into: 'into',
5
+ };
@@ -0,0 +1,16 @@
1
+ export const PsTablePinClasses = {
2
+ cell: 'ps-table-pin',
3
+ edge: 'ps-table-pin-start-edge',
4
+ end: 'ps-table-pin ps-table-pin-end right-0',
5
+ first: {
6
+ both: 'left-[4.75rem]',
7
+ none: 'left-0',
8
+ reorder: 'left-9',
9
+ select: 'left-10',
10
+ },
11
+ reorder: 'left-0',
12
+ select: {
13
+ alone: 'left-0',
14
+ withReorder: 'left-9',
15
+ },
16
+ };
@@ -0,0 +1,10 @@
1
+ export const PsTableBreakpoint = {
2
+ md: 'md',
3
+ lg: 'lg',
4
+ xl: 'xl',
5
+ };
6
+ export const PsTableHideBelowClasses = {
7
+ md: 'hidden md:table-cell',
8
+ lg: 'hidden lg:table-cell',
9
+ xl: 'hidden xl:table-cell',
10
+ };
@@ -0,0 +1,19 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { PsTableOverflowService } from '../services/ps-table-overflow-service';
3
+ export const usePsTableOverflow = (layoutKey) => {
4
+ const ref = useRef(null);
5
+ const [edges, setEdges] = useState(PsTableOverflowService.none);
6
+ const update = useCallback(() => {
7
+ const element = ref.current;
8
+ if (element === null)
9
+ return;
10
+ const next = PsTableOverflowService.edges(element.scrollLeft, element.clientWidth, element.scrollWidth);
11
+ setEdges((current) => (PsTableOverflowService.isSame(current, next) ? current : next));
12
+ }, []);
13
+ useEffect(() => {
14
+ update();
15
+ window.addEventListener('resize', update);
16
+ return () => window.removeEventListener('resize', update);
17
+ }, [layoutKey, update]);
18
+ return { edges, ref, update };
19
+ };
@@ -1,9 +1,16 @@
1
- import { useMemo, useState } from 'react';
2
- import { closestCenter, } from '@dnd-kit/core';
1
+ import { useMemo, useRef, useState } from 'react';
2
+ import { closestCenter, pointerWithin, } from '@dnd-kit/core';
3
3
  import { usePsDragSensors } from '@pallasoft/ps-core-components';
4
+ import { PsTableDropPlacement } from '../constants/ps-table-drop-placement';
4
5
  import { PsTableReorderService } from '../services/ps-table-reorder-service';
5
- export const usePsTableReorder = ({ announcements: texts, enabled, instructions, items, onReorder, }) => {
6
+ const pointerThenCenter = (args) => {
7
+ const within = pointerWithin(args);
8
+ return within.length > 0 ? within : closestCenter(args);
9
+ };
10
+ export const usePsTableReorder = ({ announcements: texts, enabled, instructions, items, nesting, onReorder, }) => {
6
11
  const [activeId, setActiveId] = useState(null);
12
+ const [drop, setDrop] = useState(null);
13
+ const dropRef = useRef(null);
7
14
  const sensors = usePsDragSensors();
8
15
  const runs = useMemo(() => enabled
9
16
  ? PsTableReorderService.runs(items, (item) => item.groupKey, (item) => item.id)
@@ -11,6 +18,7 @@ export const usePsTableReorder = ({ announcements: texts, enabled, instructions,
11
18
  const placements = useMemo(() => PsTableReorderService.placements(runs, (item) => item.id), [runs]);
12
19
  const activePlacement = activeId === null ? undefined : placements.get(activeId);
13
20
  const itemById = useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
21
+ const indexById = useMemo(() => new Map(items.map((item, index) => [item.id, index])), [items]);
14
22
  const labelOf = (id) => {
15
23
  const item = itemById.get(id);
16
24
  return item === undefined ? id : item.label;
@@ -23,23 +31,83 @@ export const usePsTableReorder = ({ announcements: texts, enabled, instructions,
23
31
  const placement = placements.get(id);
24
32
  return placement === undefined ? 0 : placement.size;
25
33
  };
34
+ const edgeFor = (activeKey, overId) => {
35
+ const activeIndex = indexById.get(activeKey);
36
+ const overIndex = indexById.get(overId);
37
+ return activeIndex !== undefined && overIndex !== undefined && activeIndex < overIndex
38
+ ? PsTableDropPlacement.after
39
+ : PsTableDropPlacement.before;
40
+ };
41
+ const resolveDrop = (event) => {
42
+ if (nesting === undefined || event.over === null || event.over.id === event.active.id)
43
+ return null;
44
+ const activeKey = String(event.active.id);
45
+ const overId = String(event.over.id);
46
+ const placement = PsTableReorderService.dropPlacement(PsTableReorderService.pointerY(event.activatorEvent, event.delta.y), event.over.rect, edgeFor(activeKey, overId), (candidate) => nesting.canDrop(activeKey, overId, candidate));
47
+ return placement === null ? null : { overId, placement };
48
+ };
49
+ const announcedDrop = (activeKey, overId) => {
50
+ if (nesting === undefined)
51
+ return null;
52
+ const known = dropRef.current;
53
+ if (known !== null && known.overId === overId)
54
+ return known;
55
+ const placement = edgeFor(activeKey, overId);
56
+ return nesting.canDrop(activeKey, overId, placement) ? { overId, placement } : null;
57
+ };
58
+ const describeDrop = (active, target) => nesting === undefined
59
+ ? undefined
60
+ : nesting.announcements[target.placement](labelOf(active), labelOf(target.overId));
26
61
  const announcements = {
27
62
  onDragCancel: ({ active }) => texts.cancelled(labelOf(String(active.id))),
28
- onDragEnd: ({ active, over }) => over === null
29
- ? texts.cancelled(labelOf(String(active.id)))
30
- : texts.dropped(labelOf(String(active.id)), positionOf(String(over.id)), countOf(String(over.id))),
31
- onDragOver: ({ active, over }) => over === null
32
- ? undefined
33
- : texts.moved(labelOf(String(active.id)), positionOf(String(over.id)), countOf(String(over.id))),
63
+ onDragEnd: ({ active, over }) => {
64
+ if (nesting !== undefined) {
65
+ const target = over === null ? null : announcedDrop(String(active.id), String(over.id));
66
+ return target === null
67
+ ? texts.cancelled(labelOf(String(active.id)))
68
+ : describeDrop(String(active.id), target);
69
+ }
70
+ return over === null
71
+ ? texts.cancelled(labelOf(String(active.id)))
72
+ : texts.dropped(labelOf(String(active.id)), positionOf(String(over.id)), countOf(String(over.id)));
73
+ },
74
+ onDragOver: ({ active, over }) => {
75
+ if (over === null)
76
+ return undefined;
77
+ if (nesting !== undefined) {
78
+ const target = announcedDrop(String(active.id), String(over.id));
79
+ return target === null ? undefined : describeDrop(String(active.id), target);
80
+ }
81
+ return texts.moved(labelOf(String(active.id)), positionOf(String(over.id)), countOf(String(over.id)));
82
+ },
34
83
  onDragStart: ({ active }) => texts.lifted(labelOf(String(active.id)), positionOf(String(active.id)), countOf(String(active.id))),
35
84
  };
36
- const handleDragEnd = ({ active, over }) => {
85
+ const handleDragMove = (event) => {
86
+ const next = resolveDrop(event);
87
+ dropRef.current = next;
88
+ setDrop((current) => current !== null &&
89
+ next !== null &&
90
+ current.overId === next.overId &&
91
+ current.placement === next.placement
92
+ ? current
93
+ : next);
94
+ };
95
+ const handleDragEnd = (event) => {
96
+ const { active, over } = event;
97
+ const target = resolveDrop(event);
37
98
  setActiveId(null);
38
- if (over === null || active.id === over.id)
99
+ setDrop(null);
100
+ dropRef.current = null;
101
+ if (nesting !== undefined) {
102
+ if (target !== null)
103
+ nesting.onMove(String(active.id), target.overId, target.placement);
104
+ return;
105
+ }
106
+ if (over === null || active.id === over.id || onReorder === undefined)
39
107
  return;
40
108
  const placement = placements.get(String(active.id));
41
- const target = placements.get(String(over.id));
42
- if (placement === undefined || target === undefined || placement.runId !== target.runId)
109
+ const sibling = placements.get(String(over.id));
110
+ if (placement === undefined || sibling === undefined || placement.runId !== sibling.runId)
43
111
  return;
44
112
  const run = runs.find((candidate) => candidate.id === placement.runId);
45
113
  if (run === undefined)
@@ -51,13 +119,19 @@ export const usePsTableReorder = ({ announcements: texts, enabled, instructions,
51
119
  activeRunId: activePlacement === undefined ? null : activePlacement.runId,
52
120
  contextProps: {
53
121
  accessibility: { announcements, screenReaderInstructions: { draggable: instructions } },
54
- collisionDetection: closestCenter,
122
+ collisionDetection: nesting === undefined ? closestCenter : pointerThenCenter,
55
123
  modifiers: [PsTableReorderService.lockToVerticalAxis],
56
- onDragCancel: () => setActiveId(null),
124
+ onDragCancel: () => {
125
+ setActiveId(null);
126
+ setDrop(null);
127
+ dropRef.current = null;
128
+ },
57
129
  onDragEnd: handleDragEnd,
130
+ onDragMove: nesting === undefined ? undefined : handleDragMove,
58
131
  onDragStart: ({ active }) => setActiveId(String(active.id)),
59
132
  sensors,
60
133
  },
134
+ drop,
61
135
  placements,
62
136
  };
63
137
  };