@pallasoft/ps-table-components 1.27.0 → 1.28.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 (32) hide show
  1. package/dist/components/ps-data-table.d.ts +3 -1
  2. package/dist/components/ps-data-table.js +18 -44
  3. package/dist/components/ps-table-pagination.js +2 -2
  4. package/dist/components/ps-table-toolbar.js +1 -1
  5. package/dist/constants/ps-table-pagination-config.d.ts +3 -0
  6. package/dist/constants/ps-table-pagination-config.js +6 -0
  7. package/dist/esm/components/ps-data-table.js +19 -45
  8. package/dist/esm/components/ps-table-pagination.js +2 -2
  9. package/dist/esm/components/ps-table-toolbar.js +1 -1
  10. package/dist/esm/constants/ps-table-pagination-config.js +3 -0
  11. package/dist/esm/hooks/use-ps-sticky-head.js +82 -0
  12. package/dist/esm/index.js +5 -0
  13. package/dist/esm/interfaces/ps-scroll-timeline-options.js +1 -0
  14. package/dist/esm/interfaces/ps-scroll-timeline-window.js +1 -0
  15. package/dist/esm/interfaces/ps-sticky-head-refs.js +1 -0
  16. package/dist/esm/interfaces/ps-table-sticky-head-keyframe.js +1 -0
  17. package/dist/esm/services/ps-table-sticky-head-service.js +17 -0
  18. package/dist/hooks/use-ps-sticky-head.d.ts +2 -0
  19. package/dist/hooks/use-ps-sticky-head.js +86 -0
  20. package/dist/index.d.ts +5 -0
  21. package/dist/index.js +5 -0
  22. package/dist/interfaces/ps-scroll-timeline-options.d.ts +4 -0
  23. package/dist/interfaces/ps-scroll-timeline-options.js +2 -0
  24. package/dist/interfaces/ps-scroll-timeline-window.d.ts +4 -0
  25. package/dist/interfaces/ps-scroll-timeline-window.js +2 -0
  26. package/dist/interfaces/ps-sticky-head-refs.d.ts +9 -0
  27. package/dist/interfaces/ps-sticky-head-refs.js +2 -0
  28. package/dist/interfaces/ps-table-sticky-head-keyframe.d.ts +4 -0
  29. package/dist/interfaces/ps-table-sticky-head-keyframe.js +2 -0
  30. package/dist/services/ps-table-sticky-head-service.d.ts +5 -0
  31. package/dist/services/ps-table-sticky-head-service.js +21 -0
  32. package/package.json +1 -1
@@ -42,8 +42,10 @@ export interface PsDataTableProps<RowData> {
42
42
  searchingLabel?: string;
43
43
  searchPlaceholder?: string;
44
44
  selectAllMatching?: PsTableSelectAllMatching;
45
+ stickyClassName?: string;
46
+ stickyLeading?: ReactNode;
45
47
  stickyTop?: string;
46
48
  toolbarActions?: ReactNode;
47
49
  totalRows?: number;
48
50
  }
49
- export declare const PsDataTable: <RowData>({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density, expandedRowIds, mobileLayout, getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading, isPending, isRefreshing, layout, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition, pinFirstColumn, query, renderExpandedRow, reorder, rows, searchingLabel, searchPlaceholder, selectAllMatching, stickyTop, toolbarActions, totalRows, }: PsDataTableProps<RowData>) => import("react").JSX.Element;
51
+ export declare const PsDataTable: <RowData>({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density, expandedRowIds, mobileLayout, getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading, isPending, isRefreshing, layout, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition, pinFirstColumn, query, renderExpandedRow, reorder, rows, searchingLabel, searchPlaceholder, selectAllMatching, stickyClassName, stickyLeading, stickyTop, toolbarActions, totalRows, }: PsDataTableProps<RowData>) => import("react").JSX.Element;
@@ -42,11 +42,13 @@ const ps_input_components_1 = require("@pallasoft/ps-input-components");
42
42
  const ps_motion_components_1 = require("@pallasoft/ps-motion-components");
43
43
  const react_virtual_1 = require("@tanstack/react-virtual");
44
44
  const ps_table_control_classes_1 = require("../constants/ps-table-control-classes");
45
+ const ps_table_pagination_config_1 = require("../constants/ps-table-pagination-config");
45
46
  const ps_table_pagination_position_1 = require("../constants/ps-table-pagination-position");
46
47
  const table_breakpoint_1 = require("../constants/table-breakpoint");
47
48
  const table_config_1 = require("../constants/table-config");
48
49
  const use_ps_incremental_count_1 = require("../hooks/use-ps-incremental-count");
49
50
  const use_ps_scroll_anchor_1 = require("../hooks/use-ps-scroll-anchor");
51
+ const use_ps_sticky_head_1 = require("../hooks/use-ps-sticky-head");
50
52
  const use_ps_table_overflow_1 = require("../hooks/use-ps-table-overflow");
51
53
  const use_ps_table_stream_status_1 = require("../hooks/use-ps-table-stream-status");
52
54
  const ps_table_cell_tooltip_service_1 = require("../services/ps-table-cell-tooltip-service");
@@ -86,7 +88,7 @@ const alignClassNames = {
86
88
  left: 'text-left',
87
89
  right: 'text-right tabular-nums',
88
90
  };
89
- const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, isRefreshing = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = ps_table_pagination_position_1.PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = table_config_1.PsTableConfig.searchingLabel, searchPlaceholder = table_config_1.PsTableConfig.searchPlaceholder, selectAllMatching, stickyTop, toolbarActions, totalRows, }) => {
91
+ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, isRefreshing = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = ps_table_pagination_position_1.PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = table_config_1.PsTableConfig.searchingLabel, searchPlaceholder = table_config_1.PsTableConfig.searchPlaceholder, selectAllMatching, stickyClassName, stickyLeading, stickyTop, toolbarActions, totalRows, }) => {
90
92
  const containerRef = (0, react_1.useRef)(null);
91
93
  const [uncontrolledPage, setUncontrolledPage] = (0, react_1.useState)(1);
92
94
  const [uncontrolledQuery, setUncontrolledQuery] = (0, react_1.useState)(() => ps_table_query_service_1.PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -97,47 +99,13 @@ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPag
97
99
  const [seenClearKey, setSeenClearKey] = (0, react_1.useState)(clearSelectionKey);
98
100
  const stickyRegionRef = (0, react_1.useRef)(null);
99
101
  const stickySentinelRef = (0, react_1.useRef)(null);
100
- const [stickyHeight, setStickyHeight] = (0, react_1.useState)(0);
101
- const [isStuck, setIsStuck] = (0, react_1.useState)(false);
102
+ const stickyBackdropRef = (0, react_1.useRef)(null);
103
+ const stickyHeadRef = (0, react_1.useRef)(null);
102
104
  const currentQuery = query ? query : uncontrolledQuery;
103
105
  if (seenClearKey !== clearSelectionKey) {
104
106
  setSeenClearKey(clearSelectionKey);
105
107
  setSelectedIds([]);
106
108
  }
107
- (0, react_1.useLayoutEffect)(() => {
108
- const region = stickyRegionRef.current;
109
- if (stickyTop === undefined || region === null)
110
- return;
111
- const measure = () => setStickyHeight(region.offsetHeight);
112
- const observer = new ResizeObserver(measure);
113
- measure();
114
- observer.observe(region);
115
- return () => observer.disconnect();
116
- }, [stickyTop]);
117
- (0, react_1.useEffect)(() => {
118
- const region = stickyRegionRef.current;
119
- const sentinel = stickySentinelRef.current;
120
- if (stickyTop === undefined || region === null || sentinel === null)
121
- return;
122
- let frame = 0;
123
- const check = () => {
124
- frame = 0;
125
- setIsStuck(sentinel.getBoundingClientRect().top < Number.parseFloat(getComputedStyle(region).top));
126
- };
127
- const schedule = () => {
128
- if (frame === 0)
129
- frame = window.requestAnimationFrame(check);
130
- };
131
- check();
132
- window.addEventListener('scroll', schedule, { passive: true });
133
- window.addEventListener('resize', schedule);
134
- return () => {
135
- window.removeEventListener('scroll', schedule);
136
- window.removeEventListener('resize', schedule);
137
- if (frame !== 0)
138
- window.cancelAnimationFrame(frame);
139
- };
140
- }, [stickyTop]);
141
109
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
142
110
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
143
111
  const fittedWidths = (0, react_1.useMemo)(() => new Map(columns
@@ -160,6 +128,14 @@ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPag
160
128
  const pins = ps_table_pin_service_1.PsTablePinService.leading(pinFirstColumn, Boolean(reorder), isSelectable);
161
129
  const placementOf = (column, columnIndex) => ps_design_system_1.PallasoftClassNameService.merge(column.hideBelow === undefined ? '' : table_breakpoint_1.PsTableHideBelowClasses[column.hideBelow], columnIndex === 0 ? pins.first : '', ps_table_pin_service_1.PsTablePinService.end(column.pinEnd === true));
162
130
  const overflow = (0, use_ps_table_overflow_1.usePsTableOverflow)(`${visibleColumns.map((column) => column.id).join(',')}:${rows.length}`);
131
+ const isStuck = (0, use_ps_sticky_head_1.usePsStickyHead)({
132
+ backdropRef: stickyBackdropRef,
133
+ headRef: stickyHeadRef,
134
+ layoutKey: `${visibleColumns.map((column) => column.id).join(',')}:${rows.length}:${isLoading}`,
135
+ regionRef: stickyRegionRef,
136
+ sentinelRef: stickySentinelRef,
137
+ stickyTop,
138
+ });
163
139
  const deferredSearch = (0, react_1.useDeferredValue)(search);
164
140
  const queriedRows = (0, react_1.useMemo)(() => isServerPaginated
165
141
  ? rows
@@ -420,7 +396,9 @@ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPag
420
396
  updateQuery({ search: next });
421
397
  resetPage();
422
398
  }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, banner] }));
423
- return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [stickyTop === undefined ? (topContent) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: '-mb-3 h-0', ref: stickySentinelRef }), (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('sticky z-sticky -mx-1 flex flex-col gap-3 bg-canvas px-1 pb-2 pt-1 transition-shadow', isStuck ? 'shadow-[0_8px_8px_-8px_rgb(var(--ps-color-ink-rgb,15_23_42)/0.18)]' : ''), ref: stickyRegionRef, style: { top: stickyTop }, children: topContent })] })), (0, jsx_runtime_1.jsxs)("div", { "aria-busy": isRefreshing, className: ps_design_system_1.PallasoftClassNameService.merge('min-w-0 rounded-[1.25rem] border border-line bg-surface', stickyTop === undefined ? 'overflow-hidden' : 'overflow-clip'), children: [showsPagination && paginationPosition !== ps_table_pagination_position_1.PsTablePaginationPosition.bottom
399
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [stickyTop === undefined ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [stickyLeading, topContent] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: '-mb-3 h-0', ref: stickySentinelRef }), (0, jsx_runtime_1.jsxs)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('sticky z-sticky flex flex-col gap-3', stickyClassName), "data-stuck": isStuck, ref: stickyRegionRef, style: { top: stickyTop }, children: [(0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: ps_design_system_1.PallasoftClassNameService.merge('ps-glass-strong pointer-events-none absolute -inset-x-3 -bottom-3 rounded-b-ps-card border-t-0 transition-opacity duration-150', isStuck ? 'opacity-100' : 'opacity-0'), ref: stickyBackdropRef, style: { top: `calc(-1 * ${stickyTop})` } }), (0, jsx_runtime_1.jsxs)("div", { className: 'relative flex flex-col gap-3', children: [stickyLeading, topContent] })] })] })), (0, jsx_runtime_1.jsxs)("div", { "aria-busy": isRefreshing, className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [showsPagination &&
400
+ paginationPosition !== ps_table_pagination_position_1.PsTablePaginationPosition.bottom &&
401
+ rowCount > ps_table_pagination_config_1.PsTablePaginationConfig.topMinRows
424
402
  ? renderPagination('border-b border-t-0')
425
403
  : null, mobileLayout === 'cards' ? ((0, jsx_runtime_1.jsxs)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('transition-opacity duration-150 sm:hidden', isRefreshing ? 'opacity-60' : ''), children: [reorder && reorder.cardsReason ? ((0, jsx_runtime_1.jsx)("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
426
404
  ? groups.map((group, groupIndex) => {
@@ -429,13 +407,9 @@ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPag
429
407
  ? grouping.renderGroupSummary(group.rows)
430
408
  : undefined }), (0, jsx_runtime_1.jsx)(ps_motion_components_1.PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
431
409
  })
432
- : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? ((0, jsx_runtime_1.jsx)(ps_table_searching_state_1.PsTableSearchingState, { label: searchingLabel, skeletonRows: table_config_1.PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsxs)("div", { className: 'px-4 py-10 text-center', children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm font-semibold text-ink', children: table_config_1.PsTableConfig.noMatchesTitle }), (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge(stickyTop === undefined
433
- ? 'ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain'
434
- : 'overflow-x-clip', 'transition-opacity duration-150', isRefreshing ? 'opacity-60' : '', 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) => ((0, jsx_runtime_1.jsxs)("table", { className: ps_design_system_1.PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? (0, jsx_runtime_1.jsx)("caption", { className: 'sr-only', children: caption }) : null, (0, jsx_runtime_1.jsx)("thead", { className: ps_design_system_1.PallasoftClassNameService.merge('bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', stickyTop === undefined
410
+ : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? ((0, jsx_runtime_1.jsx)(ps_table_searching_state_1.PsTableSearchingState, { label: searchingLabel, skeletonRows: table_config_1.PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsxs)("div", { className: 'px-4 py-10 text-center', children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm font-semibold text-ink', children: table_config_1.PsTableConfig.noMatchesTitle }), (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain', 'transition-opacity duration-150', isRefreshing ? 'opacity-60' : '', 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) => ((0, jsx_runtime_1.jsxs)("table", { className: ps_design_system_1.PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? (0, jsx_runtime_1.jsx)("caption", { className: 'sr-only', children: caption }) : null, (0, jsx_runtime_1.jsx)("thead", { className: ps_design_system_1.PallasoftClassNameService.merge('bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', stickyTop === undefined
435
411
  ? ''
436
- : 'sticky z-docked bg-surface [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), style: stickyTop === undefined
437
- ? undefined
438
- : { top: `calc(${stickyTop} + ${stickyHeight}px)` }, children: (0, jsx_runtime_1.jsxs)("tr", { children: [reorder ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? ((0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? ((0, jsx_runtime_1.jsx)("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsDragGrip, {}) })) : null })) : null, isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: (0, jsx_runtime_1.jsx)(ps_input_components_1.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) => {
412
+ : 'relative z-docked bg-surface will-change-transform [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), ref: stickyHeadRef, children: (0, jsx_runtime_1.jsxs)("tr", { children: [reorder ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? ((0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? ((0, jsx_runtime_1.jsx)("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsDragGrip, {}) })) : null })) : null, isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: (0, jsx_runtime_1.jsx)(ps_input_components_1.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) => {
439
413
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
440
414
  const isSorted = sort !== null && sort.columnId === column.id;
441
415
  return ((0, jsx_runtime_1.jsx)("th", { "aria-sort": isSorted
@@ -11,11 +11,11 @@ const PsTablePagination = ({ className, cursor, isPartial = false, loadedPageCou
11
11
  const pageNumber = cursor === undefined ? Math.min(page, pageCount) : cursor.pageNumber;
12
12
  const cursorFirst = cursor === undefined ? firstRow : (cursor.pageNumber - 1) * pageSize + 1;
13
13
  const cursorLast = cursor === undefined ? lastRow : Math.min(cursor.pageNumber * pageSize, totalRows);
14
- return ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": 'Pagination', className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-3 border-t border-line px-4 py-3', className), children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm text-ink/55', children: totalRows === 0
14
+ return ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": 'Pagination', className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-x-6 gap-y-3 border-t border-line px-5 py-3', className), children: [(0, jsx_runtime_1.jsx)("p", { className: 'shrink-0 whitespace-nowrap text-sm text-ink/55', children: totalRows === 0
15
15
  ? 'No rows'
16
16
  : cursor === undefined
17
17
  ? `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}`
18
- : `${cursorFirst}–${cursorLast} of ${approximately}${totalRows.toLocaleString()}` }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-2', children: [cursor !== undefined && cursor.pageSizeOptions !== undefined && cursor.onPageSizeChange ? ((0, jsx_runtime_1.jsxs)("label", { className: 'flex items-center gap-2 text-sm text-ink/65', children: [(0, jsx_runtime_1.jsx)("span", { className: 'hidden sm:inline', children: "Rows per page" }), (0, jsx_runtime_1.jsx)("select", { "aria-label": 'Rows per page', className: 'ps-input-control ps-focus-ring ps-control ps-control-padding rounded-ps-control text-sm', onChange: (event) => cursor.onPageSizeChange?.(Number(event.target.value)), value: pageSize, children: cursor.pageSizeOptions.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option, children: option }, option))) })] })) : null, (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: cursor === undefined ? page <= 1 : !cursor.hasPrevious, onClick: () => (cursor === undefined ? onPageChange(page - 1) : cursor.onPrevious()), type: 'button', children: "Previous" }), (0, jsx_runtime_1.jsxs)("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", pageNumber, " of ", approximately, pageCount] }), (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: cursor === undefined
18
+ : `${cursorFirst}–${cursorLast} of ${approximately}${totalRows.toLocaleString()}` }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex shrink-0 items-center gap-3', children: [cursor !== undefined && cursor.pageSizeOptions !== undefined && cursor.onPageSizeChange ? ((0, jsx_runtime_1.jsxs)("label", { className: 'flex shrink-0 items-center gap-2 whitespace-nowrap text-sm text-ink/65', children: [(0, jsx_runtime_1.jsx)("span", { className: 'hidden sm:inline', children: "Rows per page" }), (0, jsx_runtime_1.jsx)("select", { "aria-label": 'Rows per page', className: 'ps-input-control ps-focus-ring ps-control ps-control-padding w-auto shrink-0 rounded-ps-control text-sm', onChange: (event) => cursor.onPageSizeChange?.(Number(event.target.value)), value: pageSize, children: cursor.pageSizeOptions.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option, children: option }, option))) })] })) : null, (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: cursor === undefined ? page <= 1 : !cursor.hasPrevious, onClick: () => (cursor === undefined ? onPageChange(page - 1) : cursor.onPrevious()), type: 'button', children: "Previous" }), (0, jsx_runtime_1.jsxs)("span", { className: 'hidden whitespace-nowrap text-sm text-ink/65 sm:inline', children: ["Page ", pageNumber, " of ", approximately, pageCount] }), (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: cursor === undefined
19
19
  ? page >=
20
20
  (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount))
21
21
  : !cursor.hasNext, onClick: () => (cursor === undefined ? onPageChange(page + 1) : cursor.onNext()), onFocus: cursor === undefined ? undefined : cursor.onNextIntent, onPointerEnter: cursor === undefined ? undefined : cursor.onNextIntent, type: 'button', children: "Next" })] })] }));
@@ -13,6 +13,6 @@ const PsTableToolbar = ({ actions, columns, filters, isSearchable, onClearFilter
13
13
  const filterableColumns = columns.filter((column) => column.filter);
14
14
  const zIndex = (0, ps_core_components_1.usePsLayerZIndex)(ps_design_system_1.PallasoftZIndex.Dropdown);
15
15
  const activeCount = filterableColumns.filter((column) => ps_table_query_service_1.PsTableQueryService.isActive(filters[column.id])).length;
16
- return ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-wrap items-center gap-2', children: [isSearchable ? ((0, jsx_runtime_1.jsxs)("label", { className: 'relative basis-full sm:min-w-[12rem] sm:flex-1 sm:basis-auto sm:max-w-xs', children: [(0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: searchPlaceholder }), (0, jsx_runtime_1.jsx)(ps_icons_1.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: ps_icons_1.PsIconKey.Search }), (0, jsx_runtime_1.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, (0, jsx_runtime_1.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 ? ((0, jsx_runtime_1.jsxs)(react_aria_components_1.DialogTrigger, { children: [(0, jsx_runtime_1.jsxs)(react_aria_components_1.Button, { className: ps_table_control_classes_1.PsTableControlClasses.toolbarButton, children: [(0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: ps_icons_1.PsIconKey.Filter }), (0, jsx_runtime_1.jsx)("span", { className: 'sr-only sm:not-sr-only', children: "Filters" }), activeCount > 0 ? ((0, jsx_runtime_1.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] }), (0, jsx_runtime_1.jsx)(react_aria_components_1.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: (0, jsx_runtime_1.jsx)(react_aria_components_1.Dialog, { "aria-label": 'Filters', className: 'flex flex-col gap-4 p-4 outline-none', children: (0, jsx_runtime_1.jsxs)(ps_core_components_1.PsLayer, { zIndex: zIndex, children: [filterableColumns.map((column) => ((0, jsx_runtime_1.jsx)(ps_table_filter_field_1.PsTableFilterField, { kind: column.filter ? column.filter.kind : 'text', label: column.label, onChange: (value) => onFilterChange(column.id, value), options: ps_table_query_service_1.PsTableQueryService.options(rows, column), value: filters[column.id] ? filters[column.id] : {} }, column.id))), (0, jsx_runtime_1.jsx)("div", { className: 'flex justify-end border-t border-line pt-3', children: (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'text-ink/65 hover:text-ink disabled:opacity-40'), disabled: activeCount === 0, onClick: onClearFilters, type: 'button', children: "Clear all" }) })] }) }) })] })) : null, activeCount > 0 ? ((0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'text-ink/65 hover:text-ink'), onClick: onClearFilters, type: 'button', children: "Clear filters" })) : null, actions ? (0, jsx_runtime_1.jsx)("div", { className: 'ml-auto flex shrink-0 items-center gap-2', children: actions }) : null] })] }));
16
+ return ((0, jsx_runtime_1.jsxs)("div", { className: 'flex flex-wrap items-center gap-2', children: [isSearchable ? ((0, jsx_runtime_1.jsxs)("label", { className: 'relative basis-full sm:min-w-[12rem] sm:flex-1 sm:basis-auto sm:max-w-xs', children: [(0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: searchPlaceholder }), (0, jsx_runtime_1.jsx)(ps_icons_1.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: ps_icons_1.PsIconKey.Search }), (0, jsx_runtime_1.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, (0, jsx_runtime_1.jsxs)("div", { className: 'flex max-w-full shrink-0 grow flex-wrap items-center gap-2', children: [filterableColumns.length > 0 ? ((0, jsx_runtime_1.jsxs)(react_aria_components_1.DialogTrigger, { children: [(0, jsx_runtime_1.jsxs)(react_aria_components_1.Button, { className: ps_table_control_classes_1.PsTableControlClasses.toolbarButton, children: [(0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'block h-4 w-4', iconKey: ps_icons_1.PsIconKey.Filter }), (0, jsx_runtime_1.jsx)("span", { className: 'sr-only sm:not-sr-only', children: "Filters" }), activeCount > 0 ? ((0, jsx_runtime_1.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] }), (0, jsx_runtime_1.jsx)(react_aria_components_1.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: (0, jsx_runtime_1.jsx)(react_aria_components_1.Dialog, { "aria-label": 'Filters', className: 'flex flex-col gap-4 p-4 outline-none', children: (0, jsx_runtime_1.jsxs)(ps_core_components_1.PsLayer, { zIndex: zIndex, children: [filterableColumns.map((column) => ((0, jsx_runtime_1.jsx)(ps_table_filter_field_1.PsTableFilterField, { kind: column.filter ? column.filter.kind : 'text', label: column.label, onChange: (value) => onFilterChange(column.id, value), options: ps_table_query_service_1.PsTableQueryService.options(rows, column), value: filters[column.id] ? filters[column.id] : {} }, column.id))), (0, jsx_runtime_1.jsx)("div", { className: 'flex justify-end border-t border-line pt-3', children: (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'text-ink/65 hover:text-ink disabled:opacity-40'), disabled: activeCount === 0, onClick: onClearFilters, type: 'button', children: "Clear all" }) })] }) }) })] })) : null, activeCount > 0 ? ((0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'text-ink/65 hover:text-ink'), onClick: onClearFilters, type: 'button', children: "Clear filters" })) : null, actions ? (0, jsx_runtime_1.jsx)("div", { className: 'ml-auto flex shrink-0 items-center gap-2', children: actions }) : null] })] }));
17
17
  };
18
18
  exports.PsTableToolbar = PsTableToolbar;
@@ -0,0 +1,3 @@
1
+ export declare const PsTablePaginationConfig: {
2
+ readonly topMinRows: 20;
3
+ };
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsTablePaginationConfig = void 0;
4
+ exports.PsTablePaginationConfig = {
5
+ topMinRows: 20,
6
+ };
@@ -1,16 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Fragment, Suspense, lazy, useCallback, useDeferredValue, useEffect, useLayoutEffect, 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
8
  import { PsTableControlClasses } from '../constants/ps-table-control-classes';
9
+ import { PsTablePaginationConfig } from '../constants/ps-table-pagination-config';
9
10
  import { PsTablePaginationPosition } from '../constants/ps-table-pagination-position';
10
11
  import { PsTableHideBelowClasses } from '../constants/table-breakpoint';
11
12
  import { PsTableConfig } from '../constants/table-config';
12
13
  import { usePsIncrementalCount } from '../hooks/use-ps-incremental-count';
13
14
  import { usePsScrollAnchor } from '../hooks/use-ps-scroll-anchor';
15
+ import { usePsStickyHead } from '../hooks/use-ps-sticky-head';
14
16
  import { usePsTableOverflow } from '../hooks/use-ps-table-overflow';
15
17
  import { usePsTableStreamStatus } from '../hooks/use-ps-table-stream-status';
16
18
  import { PsTableCellTooltipService } from '../services/ps-table-cell-tooltip-service';
@@ -50,7 +52,7 @@ const alignClassNames = {
50
52
  left: 'text-left',
51
53
  right: 'text-right tabular-nums',
52
54
  };
53
- export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, isRefreshing = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = PsTableConfig.searchingLabel, searchPlaceholder = PsTableConfig.searchPlaceholder, selectAllMatching, stickyTop, toolbarActions, totalRows, }) => {
55
+ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, isRefreshing = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = PsTableConfig.searchingLabel, searchPlaceholder = PsTableConfig.searchPlaceholder, selectAllMatching, stickyClassName, stickyLeading, stickyTop, toolbarActions, totalRows, }) => {
54
56
  const containerRef = useRef(null);
55
57
  const [uncontrolledPage, setUncontrolledPage] = useState(1);
56
58
  const [uncontrolledQuery, setUncontrolledQuery] = useState(() => PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -61,47 +63,13 @@ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cu
61
63
  const [seenClearKey, setSeenClearKey] = useState(clearSelectionKey);
62
64
  const stickyRegionRef = useRef(null);
63
65
  const stickySentinelRef = useRef(null);
64
- const [stickyHeight, setStickyHeight] = useState(0);
65
- const [isStuck, setIsStuck] = useState(false);
66
+ const stickyBackdropRef = useRef(null);
67
+ const stickyHeadRef = useRef(null);
66
68
  const currentQuery = query ? query : uncontrolledQuery;
67
69
  if (seenClearKey !== clearSelectionKey) {
68
70
  setSeenClearKey(clearSelectionKey);
69
71
  setSelectedIds([]);
70
72
  }
71
- useLayoutEffect(() => {
72
- const region = stickyRegionRef.current;
73
- if (stickyTop === undefined || region === null)
74
- return;
75
- const measure = () => setStickyHeight(region.offsetHeight);
76
- const observer = new ResizeObserver(measure);
77
- measure();
78
- observer.observe(region);
79
- return () => observer.disconnect();
80
- }, [stickyTop]);
81
- useEffect(() => {
82
- const region = stickyRegionRef.current;
83
- const sentinel = stickySentinelRef.current;
84
- if (stickyTop === undefined || region === null || sentinel === null)
85
- return;
86
- let frame = 0;
87
- const check = () => {
88
- frame = 0;
89
- setIsStuck(sentinel.getBoundingClientRect().top < Number.parseFloat(getComputedStyle(region).top));
90
- };
91
- const schedule = () => {
92
- if (frame === 0)
93
- frame = window.requestAnimationFrame(check);
94
- };
95
- check();
96
- window.addEventListener('scroll', schedule, { passive: true });
97
- window.addEventListener('resize', schedule);
98
- return () => {
99
- window.removeEventListener('scroll', schedule);
100
- window.removeEventListener('resize', schedule);
101
- if (frame !== 0)
102
- window.cancelAnimationFrame(frame);
103
- };
104
- }, [stickyTop]);
105
73
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
106
74
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
107
75
  const fittedWidths = useMemo(() => new Map(columns
@@ -124,6 +92,14 @@ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cu
124
92
  const pins = PsTablePinService.leading(pinFirstColumn, Boolean(reorder), isSelectable);
125
93
  const placementOf = (column, columnIndex) => PallasoftClassNameService.merge(column.hideBelow === undefined ? '' : PsTableHideBelowClasses[column.hideBelow], columnIndex === 0 ? pins.first : '', PsTablePinService.end(column.pinEnd === true));
126
94
  const overflow = usePsTableOverflow(`${visibleColumns.map((column) => column.id).join(',')}:${rows.length}`);
95
+ const isStuck = usePsStickyHead({
96
+ backdropRef: stickyBackdropRef,
97
+ headRef: stickyHeadRef,
98
+ layoutKey: `${visibleColumns.map((column) => column.id).join(',')}:${rows.length}:${isLoading}`,
99
+ regionRef: stickyRegionRef,
100
+ sentinelRef: stickySentinelRef,
101
+ stickyTop,
102
+ });
127
103
  const deferredSearch = useDeferredValue(search);
128
104
  const queriedRows = useMemo(() => isServerPaginated
129
105
  ? rows
@@ -384,7 +360,9 @@ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cu
384
360
  updateQuery({ search: next });
385
361
  resetPage();
386
362
  }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, banner] }));
387
- return (_jsxs("div", { ref: containerRef, className: PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [stickyTop === undefined ? (topContent) : (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": 'true', className: '-mb-3 h-0', ref: stickySentinelRef }), _jsx("div", { className: PallasoftClassNameService.merge('sticky z-sticky -mx-1 flex flex-col gap-3 bg-canvas px-1 pb-2 pt-1 transition-shadow', isStuck ? 'shadow-[0_8px_8px_-8px_rgb(var(--ps-color-ink-rgb,15_23_42)/0.18)]' : ''), ref: stickyRegionRef, style: { top: stickyTop }, children: topContent })] })), _jsxs("div", { "aria-busy": isRefreshing, className: PallasoftClassNameService.merge('min-w-0 rounded-[1.25rem] border border-line bg-surface', stickyTop === undefined ? 'overflow-hidden' : 'overflow-clip'), children: [showsPagination && paginationPosition !== PsTablePaginationPosition.bottom
363
+ return (_jsxs("div", { ref: containerRef, className: PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [stickyTop === undefined ? (_jsxs(_Fragment, { children: [stickyLeading, topContent] })) : (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": 'true', className: '-mb-3 h-0', ref: stickySentinelRef }), _jsxs("div", { className: PallasoftClassNameService.merge('sticky z-sticky flex flex-col gap-3', stickyClassName), "data-stuck": isStuck, ref: stickyRegionRef, style: { top: stickyTop }, children: [_jsx("div", { "aria-hidden": 'true', className: PallasoftClassNameService.merge('ps-glass-strong pointer-events-none absolute -inset-x-3 -bottom-3 rounded-b-ps-card border-t-0 transition-opacity duration-150', isStuck ? 'opacity-100' : 'opacity-0'), ref: stickyBackdropRef, style: { top: `calc(-1 * ${stickyTop})` } }), _jsxs("div", { className: 'relative flex flex-col gap-3', children: [stickyLeading, topContent] })] })] })), _jsxs("div", { "aria-busy": isRefreshing, className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [showsPagination &&
364
+ paginationPosition !== PsTablePaginationPosition.bottom &&
365
+ rowCount > PsTablePaginationConfig.topMinRows
388
366
  ? renderPagination('border-b border-t-0')
389
367
  : null, mobileLayout === 'cards' ? (_jsxs("div", { className: PallasoftClassNameService.merge('transition-opacity duration-150 sm:hidden', isRefreshing ? 'opacity-60' : ''), children: [reorder && reorder.cardsReason ? (_jsx("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
390
368
  ? groups.map((group, groupIndex) => {
@@ -393,13 +371,9 @@ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cu
393
371
  ? grouping.renderGroupSummary(group.rows)
394
372
  : undefined }), _jsx(PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
395
373
  })
396
- : 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(stickyTop === undefined
397
- ? 'ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain'
398
- : 'overflow-x-clip', 'transition-opacity duration-150', isRefreshing ? 'opacity-60' : '', 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: PallasoftClassNameService.merge('bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', stickyTop === undefined
374
+ : 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', 'transition-opacity duration-150', isRefreshing ? 'opacity-60' : '', 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: PallasoftClassNameService.merge('bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', stickyTop === undefined
399
375
  ? ''
400
- : 'sticky z-docked bg-surface [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), style: stickyTop === undefined
401
- ? undefined
402
- : { top: `calc(${stickyTop} + ${stickyHeight}px)` }, 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) => {
376
+ : 'relative z-docked bg-surface will-change-transform [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), ref: stickyHeadRef, 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) => {
403
377
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
404
378
  const isSorted = sort !== null && sort.columnId === column.id;
405
379
  return (_jsx("th", { "aria-sort": isSorted
@@ -8,11 +8,11 @@ export const PsTablePagination = ({ className, cursor, isPartial = false, loaded
8
8
  const pageNumber = cursor === undefined ? Math.min(page, pageCount) : cursor.pageNumber;
9
9
  const cursorFirst = cursor === undefined ? firstRow : (cursor.pageNumber - 1) * pageSize + 1;
10
10
  const cursorLast = cursor === undefined ? lastRow : Math.min(cursor.pageNumber * pageSize, totalRows);
11
- 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
11
+ return (_jsxs("nav", { "aria-label": 'Pagination', className: PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-x-6 gap-y-3 border-t border-line px-5 py-3', className), children: [_jsx("p", { className: 'shrink-0 whitespace-nowrap text-sm text-ink/55', children: totalRows === 0
12
12
  ? 'No rows'
13
13
  : cursor === undefined
14
14
  ? `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}`
15
- : `${cursorFirst}–${cursorLast} of ${approximately}${totalRows.toLocaleString()}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [cursor !== undefined && cursor.pageSizeOptions !== undefined && cursor.onPageSizeChange ? (_jsxs("label", { className: 'flex items-center gap-2 text-sm text-ink/65', children: [_jsx("span", { className: 'hidden sm:inline', children: "Rows per page" }), _jsx("select", { "aria-label": 'Rows per page', className: 'ps-input-control ps-focus-ring ps-control ps-control-padding rounded-ps-control text-sm', onChange: (event) => cursor.onPageSizeChange?.(Number(event.target.value)), value: pageSize, children: cursor.pageSizeOptions.map((option) => (_jsx("option", { value: option, children: option }, option))) })] })) : null, _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: cursor === undefined ? page <= 1 : !cursor.hasPrevious, onClick: () => (cursor === undefined ? onPageChange(page - 1) : cursor.onPrevious()), type: 'button', children: "Previous" }), _jsxs("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", pageNumber, " of ", approximately, pageCount] }), _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: cursor === undefined
15
+ : `${cursorFirst}–${cursorLast} of ${approximately}${totalRows.toLocaleString()}` }), _jsxs("div", { className: 'flex shrink-0 items-center gap-3', children: [cursor !== undefined && cursor.pageSizeOptions !== undefined && cursor.onPageSizeChange ? (_jsxs("label", { className: 'flex shrink-0 items-center gap-2 whitespace-nowrap text-sm text-ink/65', children: [_jsx("span", { className: 'hidden sm:inline', children: "Rows per page" }), _jsx("select", { "aria-label": 'Rows per page', className: 'ps-input-control ps-focus-ring ps-control ps-control-padding w-auto shrink-0 rounded-ps-control text-sm', onChange: (event) => cursor.onPageSizeChange?.(Number(event.target.value)), value: pageSize, children: cursor.pageSizeOptions.map((option) => (_jsx("option", { value: option, children: option }, option))) })] })) : null, _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: cursor === undefined ? page <= 1 : !cursor.hasPrevious, onClick: () => (cursor === undefined ? onPageChange(page - 1) : cursor.onPrevious()), type: 'button', children: "Previous" }), _jsxs("span", { className: 'hidden whitespace-nowrap text-sm text-ink/65 sm:inline', children: ["Page ", pageNumber, " of ", approximately, pageCount] }), _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: cursor === undefined
16
16
  ? page >=
17
17
  (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount))
18
18
  : !cursor.hasNext, onClick: () => (cursor === undefined ? onPageChange(page + 1) : cursor.onNext()), onFocus: cursor === undefined ? undefined : cursor.onNextIntent, onPointerEnter: cursor === undefined ? undefined : cursor.onNextIntent, type: 'button', children: "Next" })] })] }));
@@ -10,5 +10,5 @@ export const PsTableToolbar = ({ actions, columns, filters, isSearchable, onClea
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: 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] })] }));
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: 'flex max-w-full shrink-0 grow flex-wrap items-center gap-2', 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,3 @@
1
+ export const PsTablePaginationConfig = {
2
+ topMinRows: 20,
3
+ };
@@ -0,0 +1,82 @@
1
+ import { useLayoutEffect, useState } from 'react';
2
+ import { PsTableScrollService } from '../services/ps-table-scroll-service';
3
+ import { PsTableStickyHeadService } from '../services/ps-table-sticky-head-service';
4
+ export const usePsStickyHead = ({ backdropRef, headRef, layoutKey, regionRef, sentinelRef, stickyTop, }) => {
5
+ const [isStuck, setIsStuck] = useState(false);
6
+ useLayoutEffect(() => {
7
+ const region = regionRef.current;
8
+ const sentinel = sentinelRef.current;
9
+ const backdrop = backdropRef.current;
10
+ const head = headRef.current;
11
+ const table = head === null ? null : head.parentElement;
12
+ if (stickyTop === undefined ||
13
+ region === null ||
14
+ sentinel === null ||
15
+ backdrop === null ||
16
+ head === null ||
17
+ table === null)
18
+ return;
19
+ const scroller = PsTableScrollService.parent(region);
20
+ const Timeline = window.ScrollTimeline;
21
+ const isDriven = scroller !== null && Timeline !== undefined;
22
+ let animation = null;
23
+ let frame = 0;
24
+ const maxShift = () => Math.max(table.offsetHeight - head.offsetHeight, 0);
25
+ const drive = () => {
26
+ if (scroller === null || Timeline === undefined)
27
+ return;
28
+ const scrollerTop = scroller.getBoundingClientRect().top;
29
+ const regionTop = region.getBoundingClientRect().top;
30
+ const pinnedBottom = scrollerTop +
31
+ Number.parseFloat(getComputedStyle(region).top) +
32
+ backdrop.getBoundingClientRect().bottom -
33
+ regionTop;
34
+ const pinScroll = table.getBoundingClientRect().top + scroller.scrollTop - pinnedBottom;
35
+ const keyframes = PsTableStickyHeadService.keyframes(pinScroll, maxShift(), scroller.scrollHeight - scroller.clientHeight);
36
+ if (animation !== null)
37
+ animation.cancel();
38
+ animation =
39
+ keyframes.length === 0
40
+ ? null
41
+ : head.animate(keyframes.map((keyframe) => ({
42
+ offset: keyframe.offset,
43
+ transform: `translateY(${keyframe.shift}px)`,
44
+ })), { fill: 'both', timeline: new Timeline({ axis: 'block', source: scroller }) });
45
+ };
46
+ const follow = () => (head.style.transform = `translateY(${PsTableStickyHeadService.offset(backdrop.getBoundingClientRect().bottom, table.getBoundingClientRect().top, table.offsetHeight, head.offsetHeight)}px)`);
47
+ const check = () => {
48
+ frame = 0;
49
+ setIsStuck(sentinel.getBoundingClientRect().top < region.getBoundingClientRect().top - 1);
50
+ if (!isDriven)
51
+ follow();
52
+ };
53
+ const schedule = () => {
54
+ if (frame === 0)
55
+ frame = window.requestAnimationFrame(check);
56
+ };
57
+ const measure = () => {
58
+ if (isDriven)
59
+ drive();
60
+ schedule();
61
+ };
62
+ const observer = new ResizeObserver(measure);
63
+ measure();
64
+ observer.observe(region);
65
+ observer.observe(table);
66
+ if (scroller !== null)
67
+ Array.from(scroller.children).forEach((child) => observer.observe(child));
68
+ window.addEventListener('scroll', schedule, { capture: true, passive: true });
69
+ window.addEventListener('resize', measure);
70
+ return () => {
71
+ observer.disconnect();
72
+ if (animation !== null)
73
+ animation.cancel();
74
+ head.style.transform = '';
75
+ window.removeEventListener('scroll', schedule, { capture: true });
76
+ window.removeEventListener('resize', measure);
77
+ if (frame !== 0)
78
+ window.cancelAnimationFrame(frame);
79
+ };
80
+ }, [backdropRef, headRef, layoutKey, regionRef, sentinelRef, stickyTop]);
81
+ return isStuck;
82
+ };
package/dist/esm/index.js CHANGED
@@ -29,9 +29,13 @@ export * from './services/ps-table-group-service';
29
29
  export * from './services/ps-table-query-service';
30
30
  export * from './services/ps-table-search-service';
31
31
  export * from './services/ps-table-scroll-service';
32
+ export * from './services/ps-table-sticky-head-service';
32
33
  export * from './services/ps-table-pin-service';
33
34
  export * from './services/ps-table-overflow-service';
34
35
  export * from './hooks/use-ps-table-overflow';
36
+ export * from './hooks/use-ps-sticky-head';
37
+ export * from './interfaces/ps-sticky-head-refs';
38
+ export * from './interfaces/ps-table-sticky-head-keyframe';
35
39
  export * from './interfaces/table-pin-classes';
36
40
  export * from './interfaces/table-overflow-edges';
37
41
  export * from './services/ps-table-stream-service';
@@ -47,6 +51,7 @@ export * from './interfaces/table-reorder-nesting';
47
51
  export * from './interfaces/table-nesting-announcements';
48
52
  export * from './constants/ps-table-drop-placement';
49
53
  export * from './services/ps-table-reorder-service';
54
+ export * from './constants/ps-table-pagination-config';
50
55
  export * from './constants/ps-table-pagination-position';
51
56
  export * from './interfaces/table-drag-size';
52
57
  export * from './components/ps-select-all-matching-banner';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ export class PsTableStickyHeadService {
2
+ static offset(pinnedBottom, tableTop, tableHeight, headHeight) {
3
+ return Math.min(Math.max(pinnedBottom - tableTop, 0), Math.max(tableHeight - headHeight, 0));
4
+ }
5
+ static keyframes(pinScroll, maxShift, scrollRange) {
6
+ if (scrollRange <= 0)
7
+ return [];
8
+ const shiftAt = (scroll) => Math.min(Math.max(scroll - pinScroll, 0), maxShift);
9
+ const stops = [0, pinScroll, pinScroll + maxShift, scrollRange].filter((scroll) => scroll >= 0 && scroll <= scrollRange);
10
+ return stops
11
+ .filter((scroll, index) => stops.indexOf(scroll) === index)
12
+ .map((scroll) => ({
13
+ offset: scroll / scrollRange,
14
+ shift: shiftAt(scroll),
15
+ }));
16
+ }
17
+ }
@@ -0,0 +1,2 @@
1
+ import type { PsStickyHeadRefs } from '../interfaces/ps-sticky-head-refs';
2
+ export declare const usePsStickyHead: ({ backdropRef, headRef, layoutKey, regionRef, sentinelRef, stickyTop, }: PsStickyHeadRefs) => boolean;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usePsStickyHead = void 0;
4
+ const react_1 = require("react");
5
+ const ps_table_scroll_service_1 = require("../services/ps-table-scroll-service");
6
+ const ps_table_sticky_head_service_1 = require("../services/ps-table-sticky-head-service");
7
+ const usePsStickyHead = ({ backdropRef, headRef, layoutKey, regionRef, sentinelRef, stickyTop, }) => {
8
+ const [isStuck, setIsStuck] = (0, react_1.useState)(false);
9
+ (0, react_1.useLayoutEffect)(() => {
10
+ const region = regionRef.current;
11
+ const sentinel = sentinelRef.current;
12
+ const backdrop = backdropRef.current;
13
+ const head = headRef.current;
14
+ const table = head === null ? null : head.parentElement;
15
+ if (stickyTop === undefined ||
16
+ region === null ||
17
+ sentinel === null ||
18
+ backdrop === null ||
19
+ head === null ||
20
+ table === null)
21
+ return;
22
+ const scroller = ps_table_scroll_service_1.PsTableScrollService.parent(region);
23
+ const Timeline = window.ScrollTimeline;
24
+ const isDriven = scroller !== null && Timeline !== undefined;
25
+ let animation = null;
26
+ let frame = 0;
27
+ const maxShift = () => Math.max(table.offsetHeight - head.offsetHeight, 0);
28
+ const drive = () => {
29
+ if (scroller === null || Timeline === undefined)
30
+ return;
31
+ const scrollerTop = scroller.getBoundingClientRect().top;
32
+ const regionTop = region.getBoundingClientRect().top;
33
+ const pinnedBottom = scrollerTop +
34
+ Number.parseFloat(getComputedStyle(region).top) +
35
+ backdrop.getBoundingClientRect().bottom -
36
+ regionTop;
37
+ const pinScroll = table.getBoundingClientRect().top + scroller.scrollTop - pinnedBottom;
38
+ const keyframes = ps_table_sticky_head_service_1.PsTableStickyHeadService.keyframes(pinScroll, maxShift(), scroller.scrollHeight - scroller.clientHeight);
39
+ if (animation !== null)
40
+ animation.cancel();
41
+ animation =
42
+ keyframes.length === 0
43
+ ? null
44
+ : head.animate(keyframes.map((keyframe) => ({
45
+ offset: keyframe.offset,
46
+ transform: `translateY(${keyframe.shift}px)`,
47
+ })), { fill: 'both', timeline: new Timeline({ axis: 'block', source: scroller }) });
48
+ };
49
+ const follow = () => (head.style.transform = `translateY(${ps_table_sticky_head_service_1.PsTableStickyHeadService.offset(backdrop.getBoundingClientRect().bottom, table.getBoundingClientRect().top, table.offsetHeight, head.offsetHeight)}px)`);
50
+ const check = () => {
51
+ frame = 0;
52
+ setIsStuck(sentinel.getBoundingClientRect().top < region.getBoundingClientRect().top - 1);
53
+ if (!isDriven)
54
+ follow();
55
+ };
56
+ const schedule = () => {
57
+ if (frame === 0)
58
+ frame = window.requestAnimationFrame(check);
59
+ };
60
+ const measure = () => {
61
+ if (isDriven)
62
+ drive();
63
+ schedule();
64
+ };
65
+ const observer = new ResizeObserver(measure);
66
+ measure();
67
+ observer.observe(region);
68
+ observer.observe(table);
69
+ if (scroller !== null)
70
+ Array.from(scroller.children).forEach((child) => observer.observe(child));
71
+ window.addEventListener('scroll', schedule, { capture: true, passive: true });
72
+ window.addEventListener('resize', measure);
73
+ return () => {
74
+ observer.disconnect();
75
+ if (animation !== null)
76
+ animation.cancel();
77
+ head.style.transform = '';
78
+ window.removeEventListener('scroll', schedule, { capture: true });
79
+ window.removeEventListener('resize', measure);
80
+ if (frame !== 0)
81
+ window.cancelAnimationFrame(frame);
82
+ };
83
+ }, [backdropRef, headRef, layoutKey, regionRef, sentinelRef, stickyTop]);
84
+ return isStuck;
85
+ };
86
+ exports.usePsStickyHead = usePsStickyHead;
package/dist/index.d.ts CHANGED
@@ -29,9 +29,13 @@ export * from './services/ps-table-group-service';
29
29
  export * from './services/ps-table-query-service';
30
30
  export * from './services/ps-table-search-service';
31
31
  export * from './services/ps-table-scroll-service';
32
+ export * from './services/ps-table-sticky-head-service';
32
33
  export * from './services/ps-table-pin-service';
33
34
  export * from './services/ps-table-overflow-service';
34
35
  export * from './hooks/use-ps-table-overflow';
36
+ export * from './hooks/use-ps-sticky-head';
37
+ export * from './interfaces/ps-sticky-head-refs';
38
+ export * from './interfaces/ps-table-sticky-head-keyframe';
35
39
  export * from './interfaces/table-pin-classes';
36
40
  export * from './interfaces/table-overflow-edges';
37
41
  export * from './services/ps-table-stream-service';
@@ -47,6 +51,7 @@ export * from './interfaces/table-reorder-nesting';
47
51
  export * from './interfaces/table-nesting-announcements';
48
52
  export * from './constants/ps-table-drop-placement';
49
53
  export * from './services/ps-table-reorder-service';
54
+ export * from './constants/ps-table-pagination-config';
50
55
  export * from './constants/ps-table-pagination-position';
51
56
  export * from './interfaces/table-drag-size';
52
57
  export * from './components/ps-select-all-matching-banner';
package/dist/index.js CHANGED
@@ -45,9 +45,13 @@ __exportStar(require("./services/ps-table-group-service"), exports);
45
45
  __exportStar(require("./services/ps-table-query-service"), exports);
46
46
  __exportStar(require("./services/ps-table-search-service"), exports);
47
47
  __exportStar(require("./services/ps-table-scroll-service"), exports);
48
+ __exportStar(require("./services/ps-table-sticky-head-service"), exports);
48
49
  __exportStar(require("./services/ps-table-pin-service"), exports);
49
50
  __exportStar(require("./services/ps-table-overflow-service"), exports);
50
51
  __exportStar(require("./hooks/use-ps-table-overflow"), exports);
52
+ __exportStar(require("./hooks/use-ps-sticky-head"), exports);
53
+ __exportStar(require("./interfaces/ps-sticky-head-refs"), exports);
54
+ __exportStar(require("./interfaces/ps-table-sticky-head-keyframe"), exports);
51
55
  __exportStar(require("./interfaces/table-pin-classes"), exports);
52
56
  __exportStar(require("./interfaces/table-overflow-edges"), exports);
53
57
  __exportStar(require("./services/ps-table-stream-service"), exports);
@@ -63,6 +67,7 @@ __exportStar(require("./interfaces/table-reorder-nesting"), exports);
63
67
  __exportStar(require("./interfaces/table-nesting-announcements"), exports);
64
68
  __exportStar(require("./constants/ps-table-drop-placement"), exports);
65
69
  __exportStar(require("./services/ps-table-reorder-service"), exports);
70
+ __exportStar(require("./constants/ps-table-pagination-config"), exports);
66
71
  __exportStar(require("./constants/ps-table-pagination-position"), exports);
67
72
  __exportStar(require("./interfaces/table-drag-size"), exports);
68
73
  __exportStar(require("./components/ps-select-all-matching-banner"), exports);
@@ -0,0 +1,4 @@
1
+ export interface PsScrollTimelineOptions {
2
+ axis: 'block';
3
+ source: Element;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ import type { PsScrollTimelineOptions } from './ps-scroll-timeline-options';
2
+ export interface PsScrollTimelineWindow {
3
+ ScrollTimeline?: new (options: PsScrollTimelineOptions) => AnimationTimeline;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ import type { RefObject } from 'react';
2
+ export interface PsStickyHeadRefs {
3
+ backdropRef: RefObject<HTMLDivElement | null>;
4
+ headRef: RefObject<HTMLTableSectionElement | null>;
5
+ layoutKey: string;
6
+ regionRef: RefObject<HTMLDivElement | null>;
7
+ sentinelRef: RefObject<HTMLDivElement | null>;
8
+ stickyTop?: string;
9
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ export interface PsTableStickyHeadKeyframe {
2
+ offset: number;
3
+ shift: number;
4
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ import type { PsTableStickyHeadKeyframe } from '../interfaces/ps-table-sticky-head-keyframe';
2
+ export declare class PsTableStickyHeadService {
3
+ static offset(pinnedBottom: number, tableTop: number, tableHeight: number, headHeight: number): number;
4
+ static keyframes(pinScroll: number, maxShift: number, scrollRange: number): PsTableStickyHeadKeyframe[];
5
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsTableStickyHeadService = void 0;
4
+ class PsTableStickyHeadService {
5
+ static offset(pinnedBottom, tableTop, tableHeight, headHeight) {
6
+ return Math.min(Math.max(pinnedBottom - tableTop, 0), Math.max(tableHeight - headHeight, 0));
7
+ }
8
+ static keyframes(pinScroll, maxShift, scrollRange) {
9
+ if (scrollRange <= 0)
10
+ return [];
11
+ const shiftAt = (scroll) => Math.min(Math.max(scroll - pinScroll, 0), maxShift);
12
+ const stops = [0, pinScroll, pinScroll + maxShift, scrollRange].filter((scroll) => scroll >= 0 && scroll <= scrollRange);
13
+ return stops
14
+ .filter((scroll, index) => stops.indexOf(scroll) === index)
15
+ .map((scroll) => ({
16
+ offset: scroll / scrollRange,
17
+ shift: shiftAt(scroll),
18
+ }));
19
+ }
20
+ }
21
+ exports.PsTableStickyHeadService = PsTableStickyHeadService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-table-components",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "private": false,
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/esm/index.js",