@pallasoft/ps-table-components 1.27.1 → 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.
@@ -43,8 +43,9 @@ export interface PsDataTableProps<RowData> {
43
43
  searchPlaceholder?: string;
44
44
  selectAllMatching?: PsTableSelectAllMatching;
45
45
  stickyClassName?: string;
46
+ stickyLeading?: ReactNode;
46
47
  stickyTop?: string;
47
48
  toolbarActions?: ReactNode;
48
49
  totalRows?: number;
49
50
  }
50
- 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, 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;
@@ -48,6 +48,7 @@ const table_breakpoint_1 = require("../constants/table-breakpoint");
48
48
  const table_config_1 = require("../constants/table-config");
49
49
  const use_ps_incremental_count_1 = require("../hooks/use-ps-incremental-count");
50
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");
51
52
  const use_ps_table_overflow_1 = require("../hooks/use-ps-table-overflow");
52
53
  const use_ps_table_stream_status_1 = require("../hooks/use-ps-table-stream-status");
53
54
  const ps_table_cell_tooltip_service_1 = require("../services/ps-table-cell-tooltip-service");
@@ -87,7 +88,7 @@ const alignClassNames = {
87
88
  left: 'text-left',
88
89
  right: 'text-right tabular-nums',
89
90
  };
90
- 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, 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, }) => {
91
92
  const containerRef = (0, react_1.useRef)(null);
92
93
  const [uncontrolledPage, setUncontrolledPage] = (0, react_1.useState)(1);
93
94
  const [uncontrolledQuery, setUncontrolledQuery] = (0, react_1.useState)(() => ps_table_query_service_1.PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -98,47 +99,13 @@ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPag
98
99
  const [seenClearKey, setSeenClearKey] = (0, react_1.useState)(clearSelectionKey);
99
100
  const stickyRegionRef = (0, react_1.useRef)(null);
100
101
  const stickySentinelRef = (0, react_1.useRef)(null);
101
- const [stickyHeight, setStickyHeight] = (0, react_1.useState)(0);
102
- const [isStuck, setIsStuck] = (0, react_1.useState)(false);
102
+ const stickyBackdropRef = (0, react_1.useRef)(null);
103
+ const stickyHeadRef = (0, react_1.useRef)(null);
103
104
  const currentQuery = query ? query : uncontrolledQuery;
104
105
  if (seenClearKey !== clearSelectionKey) {
105
106
  setSeenClearKey(clearSelectionKey);
106
107
  setSelectedIds([]);
107
108
  }
108
- (0, react_1.useLayoutEffect)(() => {
109
- const region = stickyRegionRef.current;
110
- if (stickyTop === undefined || region === null)
111
- return;
112
- const measure = () => setStickyHeight(region.offsetHeight);
113
- const observer = new ResizeObserver(measure);
114
- measure();
115
- observer.observe(region);
116
- return () => observer.disconnect();
117
- }, [stickyTop]);
118
- (0, react_1.useEffect)(() => {
119
- const region = stickyRegionRef.current;
120
- const sentinel = stickySentinelRef.current;
121
- if (stickyTop === undefined || region === null || sentinel === null)
122
- return;
123
- let frame = 0;
124
- const check = () => {
125
- frame = 0;
126
- setIsStuck(sentinel.getBoundingClientRect().top < Number.parseFloat(getComputedStyle(region).top));
127
- };
128
- const schedule = () => {
129
- if (frame === 0)
130
- frame = window.requestAnimationFrame(check);
131
- };
132
- check();
133
- window.addEventListener('scroll', schedule, { capture: true, passive: true });
134
- window.addEventListener('resize', schedule);
135
- return () => {
136
- window.removeEventListener('scroll', schedule, { capture: true });
137
- window.removeEventListener('resize', schedule);
138
- if (frame !== 0)
139
- window.cancelAnimationFrame(frame);
140
- };
141
- }, [stickyTop]);
142
109
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
143
110
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
144
111
  const fittedWidths = (0, react_1.useMemo)(() => new Map(columns
@@ -161,6 +128,14 @@ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPag
161
128
  const pins = ps_table_pin_service_1.PsTablePinService.leading(pinFirstColumn, Boolean(reorder), isSelectable);
162
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));
163
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
+ });
164
139
  const deferredSearch = (0, react_1.useDeferredValue)(search);
165
140
  const queriedRows = (0, react_1.useMemo)(() => isServerPaginated
166
141
  ? rows
@@ -421,7 +396,7 @@ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPag
421
396
  updateQuery({ search: next });
422
397
  resetPage();
423
398
  }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, banner] }));
424
- 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 px-1 pb-2 pt-1 transition-shadow', stickyClassName === undefined ? 'bg-canvas' : stickyClassName, 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 &&
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 &&
425
400
  paginationPosition !== ps_table_pagination_position_1.PsTablePaginationPosition.bottom &&
426
401
  rowCount > ps_table_pagination_config_1.PsTablePaginationConfig.topMinRows
427
402
  ? renderPagination('border-b border-t-0')
@@ -432,13 +407,9 @@ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPag
432
407
  ? grouping.renderGroupSummary(group.rows)
433
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}`));
434
409
  })
435
- : 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
436
- ? 'ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain'
437
- : '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
438
411
  ? ''
439
- : 'sticky z-docked bg-surface [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), style: stickyTop === undefined
440
- ? undefined
441
- : { 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) => {
442
413
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
443
414
  const isSorted = sort !== null && sort.columnId === column.id;
444
415
  return ((0, jsx_runtime_1.jsx)("th", { "aria-sort": isSorted
@@ -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;
@@ -1,5 +1,5 @@
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';
@@ -12,6 +12,7 @@ import { PsTableHideBelowClasses } from '../constants/table-breakpoint';
12
12
  import { PsTableConfig } from '../constants/table-config';
13
13
  import { usePsIncrementalCount } from '../hooks/use-ps-incremental-count';
14
14
  import { usePsScrollAnchor } from '../hooks/use-ps-scroll-anchor';
15
+ import { usePsStickyHead } from '../hooks/use-ps-sticky-head';
15
16
  import { usePsTableOverflow } from '../hooks/use-ps-table-overflow';
16
17
  import { usePsTableStreamStatus } from '../hooks/use-ps-table-stream-status';
17
18
  import { PsTableCellTooltipService } from '../services/ps-table-cell-tooltip-service';
@@ -51,7 +52,7 @@ const alignClassNames = {
51
52
  left: 'text-left',
52
53
  right: 'text-right tabular-nums',
53
54
  };
54
- 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, 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, }) => {
55
56
  const containerRef = useRef(null);
56
57
  const [uncontrolledPage, setUncontrolledPage] = useState(1);
57
58
  const [uncontrolledQuery, setUncontrolledQuery] = useState(() => PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -62,47 +63,13 @@ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cu
62
63
  const [seenClearKey, setSeenClearKey] = useState(clearSelectionKey);
63
64
  const stickyRegionRef = useRef(null);
64
65
  const stickySentinelRef = useRef(null);
65
- const [stickyHeight, setStickyHeight] = useState(0);
66
- const [isStuck, setIsStuck] = useState(false);
66
+ const stickyBackdropRef = useRef(null);
67
+ const stickyHeadRef = useRef(null);
67
68
  const currentQuery = query ? query : uncontrolledQuery;
68
69
  if (seenClearKey !== clearSelectionKey) {
69
70
  setSeenClearKey(clearSelectionKey);
70
71
  setSelectedIds([]);
71
72
  }
72
- useLayoutEffect(() => {
73
- const region = stickyRegionRef.current;
74
- if (stickyTop === undefined || region === null)
75
- return;
76
- const measure = () => setStickyHeight(region.offsetHeight);
77
- const observer = new ResizeObserver(measure);
78
- measure();
79
- observer.observe(region);
80
- return () => observer.disconnect();
81
- }, [stickyTop]);
82
- useEffect(() => {
83
- const region = stickyRegionRef.current;
84
- const sentinel = stickySentinelRef.current;
85
- if (stickyTop === undefined || region === null || sentinel === null)
86
- return;
87
- let frame = 0;
88
- const check = () => {
89
- frame = 0;
90
- setIsStuck(sentinel.getBoundingClientRect().top < Number.parseFloat(getComputedStyle(region).top));
91
- };
92
- const schedule = () => {
93
- if (frame === 0)
94
- frame = window.requestAnimationFrame(check);
95
- };
96
- check();
97
- window.addEventListener('scroll', schedule, { capture: true, passive: true });
98
- window.addEventListener('resize', schedule);
99
- return () => {
100
- window.removeEventListener('scroll', schedule, { capture: true });
101
- window.removeEventListener('resize', schedule);
102
- if (frame !== 0)
103
- window.cancelAnimationFrame(frame);
104
- };
105
- }, [stickyTop]);
106
73
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
107
74
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
108
75
  const fittedWidths = useMemo(() => new Map(columns
@@ -125,6 +92,14 @@ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cu
125
92
  const pins = PsTablePinService.leading(pinFirstColumn, Boolean(reorder), isSelectable);
126
93
  const placementOf = (column, columnIndex) => PallasoftClassNameService.merge(column.hideBelow === undefined ? '' : PsTableHideBelowClasses[column.hideBelow], columnIndex === 0 ? pins.first : '', PsTablePinService.end(column.pinEnd === true));
127
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
+ });
128
103
  const deferredSearch = useDeferredValue(search);
129
104
  const queriedRows = useMemo(() => isServerPaginated
130
105
  ? rows
@@ -385,7 +360,7 @@ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cu
385
360
  updateQuery({ search: next });
386
361
  resetPage();
387
362
  }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, banner] }));
388
- 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 px-1 pb-2 pt-1 transition-shadow', stickyClassName === undefined ? 'bg-canvas' : stickyClassName, 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 &&
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 &&
389
364
  paginationPosition !== PsTablePaginationPosition.bottom &&
390
365
  rowCount > PsTablePaginationConfig.topMinRows
391
366
  ? renderPagination('border-b border-t-0')
@@ -396,13 +371,9 @@ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cu
396
371
  ? grouping.renderGroupSummary(group.rows)
397
372
  : undefined }), _jsx(PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
398
373
  })
399
- : 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
400
- ? 'ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain'
401
- : '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
402
375
  ? ''
403
- : 'sticky z-docked bg-surface [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), style: stickyTop === undefined
404
- ? undefined
405
- : { 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) => {
406
377
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
407
378
  const isSorted = sort !== null && sort.columnId === column.id;
408
379
  return (_jsx("th", { "aria-sort": isSorted
@@ -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,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';
@@ -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';
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);
@@ -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.1",
3
+ "version": "1.28.0",
4
4
  "private": false,
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/esm/index.js",