pixelize-design-library 2.4.2-beta.47 → 2.4.2-beta.48

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 (33) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/COMPONENT-GOTCHAS.md +39 -6
  3. package/dist/Components/Common/chipTriggerSize.d.ts +10 -0
  4. package/dist/Components/Common/chipTriggerSize.js +15 -0
  5. package/dist/Components/SearchSelect/SearchSelect.js +48 -7
  6. package/dist/Components/SearchSelect/SearchSelectProps.d.ts +5 -1
  7. package/dist/Components/Select/SelectNative.d.ts +2 -0
  8. package/dist/Components/Select/SelectPopup.js +97 -36
  9. package/dist/Components/Select/SelectProps.d.ts +4 -0
  10. package/dist/Components/SelectV2/SelectV2.d.ts +2 -9
  11. package/dist/Components/SelectV2/SelectV2.js +2 -10
  12. package/dist/Components/Table/Table.js +6 -7
  13. package/dist/Components/Table/TableProps.d.ts +9 -1
  14. package/dist/Components/Table/components/Pagination.js +8 -5
  15. package/dist/Components/Table/components/TableBody.d.ts +2 -1
  16. package/dist/Components/Table/components/TableBody.js +82 -15
  17. package/dist/Components/Table/components/TableHeader.d.ts +1 -1
  18. package/dist/Components/Table/components/TableHeader.js +8 -9
  19. package/dist/Components/Table/components/rowMetrics.d.ts +0 -1
  20. package/dist/Components/Table/components/rowMetrics.js +1 -2
  21. package/dist/Components/Table/hooks/useTable.d.ts +5 -1
  22. package/dist/Components/Table/hooks/useTable.js +43 -7
  23. package/dist/esm/Components/Common/chipTriggerSize.js +11 -0
  24. package/dist/esm/Components/SearchSelect/SearchSelect.js +48 -7
  25. package/dist/esm/Components/Select/SelectPopup.js +97 -36
  26. package/dist/esm/Components/SelectV2/SelectV2.js +2 -9
  27. package/dist/esm/Components/Table/Table.js +6 -7
  28. package/dist/esm/Components/Table/components/Pagination.js +8 -5
  29. package/dist/esm/Components/Table/components/TableBody.js +81 -16
  30. package/dist/esm/Components/Table/components/TableHeader.js +8 -9
  31. package/dist/esm/Components/Table/components/rowMetrics.js +0 -1
  32. package/dist/esm/Components/Table/hooks/useTable.js +44 -8
  33. package/package.json +1 -1
@@ -11,17 +11,37 @@ import TableGroupRow from "./TableGroupRow.js";
11
11
  import { useTableVariant } from "../variants/TableVariantContext.js";
12
12
  import { resolvePillInk } from "../variants/pillInk.js";
13
13
  import EmptyState from "../../EmptyState/EmptyState.js";
14
- import { CHECKBOX_SLOT_OFFSET, DENSITY_ROWS, EMPTY_STATE_HEIGHT, GROUP_HEADER_HEIGHT } from "./rowMetrics.js";
14
+ import { DENSITY_ROWS, EMPTY_STATE_HEIGHT, GROUP_HEADER_HEIGHT } from "./rowMetrics.js";
15
15
  import { transitionFor } from "../../../Theme/tokens/primitives/motion.js";
16
16
  import { reducedMotionSx } from "../../../Theme/chakra/motion.js";
17
17
  const getRowId = (row) => row.id;
18
18
  const getRowContent = (row) => typeof row.content === "function"
19
19
  ? row.content
20
20
  : undefined;
21
+ // Smallest index i in [0, total] where offsets[i+1] > target; offsets is sorted.
22
+ export const findFirstVisibleIndex = (offsets, total, target) => {
23
+ let lo = 0;
24
+ let hi = total;
25
+ while (lo < hi) {
26
+ const mid = (lo + hi) >>> 1;
27
+ if (offsets[mid + 1] <= target) {
28
+ lo = mid + 1;
29
+ }
30
+ else {
31
+ hi = mid;
32
+ }
33
+ }
34
+ return lo;
35
+ };
21
36
  // Monday-style status cell: the whole cell is filled with the status color and
22
37
  // the label is centered in whichever of white/black clears AA against that fill —
23
38
  // the fill can be a consumer hex we can't know in advance is light or dark.
24
- const StatusCell = ({ value, onSolid }) => (_jsx(Box, { display: "flex", alignItems: "center", justifyContent: "center", w: "100%", h: "100%", px: "0.5rem", fontSize: "0.75rem", fontWeight: 600, color: onSolid, textAlign: "center", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", children: value }));
39
+ const BLEED_JUSTIFY = {
40
+ start: "flex-start",
41
+ center: "center",
42
+ end: "flex-end",
43
+ };
44
+ const StatusCell = ({ value, onSolid, align, }) => (_jsx(Box, { display: "flex", alignItems: "center", justifyContent: BLEED_JUSTIFY[align !== null && align !== void 0 ? align : "center"], w: "100%", h: "100%", px: "0.5rem", fontSize: "0.75rem", fontWeight: 600, color: onSolid, textAlign: align !== null && align !== void 0 ? align : "center", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", children: value }));
25
45
  // Studio's status cell: a contained chip on the tone's soft rung. Same tone pair
26
46
  // the group header already uses, so an override hex behaves identically in both.
27
47
  const StatusPill = ({ value, soft, ink, radius, }) => (_jsx(Box, { display: "inline-flex", alignItems: "center", maxW: "100%", px: "0.5rem", py: "0.125rem", borderRadius: radius, bg: soft, color: ink, fontSize: "0.75rem", fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", children: value }));
@@ -110,13 +130,13 @@ const TableRow = React.memo(({ row, rowIndex, isChecked, isExpanded, isContent,
110
130
  const bleedBg = statusColor === null || statusColor === void 0 ? void 0 : statusColor.soft;
111
131
  const bleedInk = statusColor === null || statusColor === void 0 ? void 0 : statusColor.text;
112
132
  const canHover = !isBleed && !((cellStyles === null || cellStyles === void 0 ? void 0 : cellStyles[header.id]) || rowStyle);
113
- return (_jsx(Td, { maxWidth: 500, minWidth: 120, height: `${rowHeight}px`, py: isBleed ? 0 : cellPy, px: isBleed ? 0 : undefined, onClick: () => handleCellClick(header), fontSize: 14, fontWeight: 400, position: isFrozen ? "sticky" : undefined, left: isFrozen ? leftOffsets[headerIndex] : undefined, zIndex: isFrozen ? 1 : 0, background: isBleed ? bleedBg : isChecked ? (isFrozen ? selectedFrozenBg : selectedBg) : isFrozen ? frozenCellBg : restBg, textAlign: isBleed ? undefined : header.align, textOverflow: "ellipsis", boxShadow: isFirstDataCell ? leadSx : undefined, borderBottom: noBorders
133
+ return (_jsx(Td, { maxWidth: 500, minWidth: 120, height: `${rowHeight}px`, py: isBleed ? 0 : cellPy, px: isBleed ? 0 : undefined, onClick: () => handleCellClick(header), fontSize: 14, fontWeight: 400, position: isFrozen ? "sticky" : undefined, left: isFrozen ? `${leftOffsets[headerIndex]}px` : undefined, zIndex: isFrozen ? 1 : 0, background: isBleed ? bleedBg : isChecked ? (isFrozen ? selectedFrozenBg : selectedBg) : isFrozen ? frozenCellBg : restBg, textAlign: isBleed ? undefined : header.align, textOverflow: "ellipsis", boxShadow: isFirstDataCell ? leadSx : undefined, borderBottom: noBorders
114
134
  ? "none"
115
135
  : `0.063rem solid ${isFrozen || isChecked
116
136
  ? variant.row.frozenRuleColor
117
137
  : variant.row.ruleColor}`, className: `columns ${isFrozen ? "sticky-columns" : "scrollable-columns"}`, boxSizing: "border-box", color: isBleed ? bleedInk : variant.cells.ink, "data-hoverable": canHover ? "true" : "false", _hover: variant.row.hoverScope === "row" || !canHover
118
138
  ? EMPTY_HOVER
119
- : hoverStyle, children: isBleed ? (_jsx(StatusCell, { value: normalizeTableCellValue(row[header.id]), onSolid: bleedInk })) : isStatus && statusColor ? (_jsx(StatusPill, { value: normalizeTableCellValue(row[header.id]), soft: statusColor.soft, ink: resolvePillInk(statusColor.text, statusColor.soft, theme.colors.background[50]), radius: variant.cells.pillRadius })) : (_jsx(Box, { display: "block", overflow: "hidden", whiteSpace: "normal", overflowWrap: "break-word", children: normalizeTableCellValue(header.node ? header.node(row) : row[header.id]) })) }, headerIndex + 1));
139
+ : hoverStyle, children: isBleed ? (_jsx(StatusCell, { value: normalizeTableCellValue(row[header.id]), onSolid: bleedInk, align: header.align })) : isStatus && statusColor ? (_jsx(StatusPill, { value: normalizeTableCellValue(row[header.id]), soft: statusColor.soft, ink: resolvePillInk(statusColor.text, statusColor.soft, theme.colors.background[50]), radius: variant.cells.pillRadius })) : (_jsx(Box, { display: "block", overflow: "hidden", whiteSpace: "normal", overflowWrap: "break-word", children: normalizeTableCellValue(header.node ? header.node(row) : row[header.id]) })) }, headerIndex + 1));
120
140
  }), isLink && (_jsx(Td, { w: 2, p: 0, fontSize: 14, background: isChecked ? (isActionFreeze ? selectedFrozenBg : selectedBg) : isActionFreeze ? frozenCellBg : restBg, color: freezedTextColor, position: isActionFreeze ? "sticky" : "relative", borderBottom: borderStyle, boxSizing: "border-box", right: 0, zIndex: 1, className: `columns sticky-columns${isActionFreeze ? "-right" : ""}`, children: (row.onLink || row.onEdit || row.onDelete) ? (_jsx(TableActions, { row: row })) : null }))] }), rowContent && isExpanded && (_jsx(Tr, { ref: expandedRowRef, children: _jsx(Td, { colSpan: columns.length +
121
141
  (isCheckbox ? 1 : 0) +
122
142
  (isContent ? 1 : 0) +
@@ -131,7 +151,7 @@ const TableEmpty = ({ emptyState }) => {
131
151
  return _jsx(_Fragment, { children: emptyState });
132
152
  return (_jsx(EmptyState, { size: "sm", minH: `${EMPTY_STATE_HEIGHT}px`, title: structured === null || structured === void 0 ? void 0 : structured.title, description: structured === null || structured === void 0 ? void 0 : structured.description, icon: icon }));
133
153
  };
134
- const TableBody = ({ data, isCheckbox, columns, startRow, columnWidths, freezedBgColor, freezedTextColor, noBorders, handleCheckbox, selections, isLoading, onRowClick, isContent, isLink, isActionFreeze, loadingSkeletonRows, scrollContainerRef, density, stripe = false, accentColors, groups, onAddItem, emptyState, }) => {
154
+ const TableBody = ({ data, isCheckbox, columns, startRow, rowsPerPage, columnWidths, slotWidth, freezedBgColor, freezedTextColor, noBorders, handleCheckbox, selections, isLoading, onRowClick, isContent, isLink, isActionFreeze, loadingSkeletonRows, scrollContainerRef, density, stripe = false, accentColors, groups, onAddItem, emptyState, }) => {
135
155
  var _a;
136
156
  const theme = useCustomTheme();
137
157
  const variant = useTableVariant();
@@ -180,7 +200,15 @@ const TableBody = ({ data, isCheckbox, columns, startRow, columnWidths, freezedB
180
200
  const el = scrollContainerRef === null || scrollContainerRef === void 0 ? void 0 : scrollContainerRef.current;
181
201
  if (!el || !(shouldVirtualize || groupVirtualize))
182
202
  return;
183
- const onScroll = () => setScrollTop(el.scrollTop);
203
+ let rafId = null;
204
+ const onScroll = () => {
205
+ if (rafId != null)
206
+ return;
207
+ rafId = requestAnimationFrame(() => {
208
+ rafId = null;
209
+ setScrollTop(el.scrollTop);
210
+ });
211
+ };
184
212
  const measure = () => setViewportH(el.clientHeight);
185
213
  measure();
186
214
  setScrollTop(el.scrollTop);
@@ -189,23 +217,30 @@ const TableBody = ({ data, isCheckbox, columns, startRow, columnWidths, freezedB
189
217
  return () => {
190
218
  el.removeEventListener("scroll", onScroll);
191
219
  window.removeEventListener("resize", measure);
220
+ if (rafId != null)
221
+ cancelAnimationFrame(rafId);
192
222
  };
193
223
  }, [scrollContainerRef, shouldVirtualize, groupVirtualize, data.length, groupRowCount]);
194
- // New page: jump back to the top so the window recomputes from row 0.
224
+ // Reset only on a real page change, never merely from the virtualization
225
+ // threshold flipping or rows being appended (infinite scroll/load-more).
226
+ const sliceKey = `${startRow}|${rowsPerPage}`;
227
+ const prevSliceKeyRef = useRef(sliceKey);
195
228
  useEffect(() => {
196
- if (!shouldVirtualize)
229
+ const changed = prevSliceKeyRef.current !== sliceKey;
230
+ prevSliceKeyRef.current = sliceKey;
231
+ if (!changed)
197
232
  return;
198
233
  const el = scrollContainerRef === null || scrollContainerRef === void 0 ? void 0 : scrollContainerRef.current;
199
234
  if (el)
200
235
  el.scrollTop = 0;
201
236
  setScrollTop(0);
202
- }, [startRow, shouldVirtualize, scrollContainerRef]);
237
+ }, [sliceKey, scrollContainerRef]);
203
238
  const total = data.length;
204
239
  // Prefix-sum offsets using measured heights (exact for rendered rows) and
205
240
  // estimates for not-yet-measured rows (ROW_HEIGHT, + ESTIMATED_EXPAND if expanded).
206
241
  // Independent of scrollTop, so scrolling reuses it instead of rebuilding it.
207
242
  const offsets = useMemo(() => {
208
- if (!shouldVirtualize)
243
+ if (!shouldVirtualize || groups)
209
244
  return null;
210
245
  const acc = new Array(total + 1);
211
246
  acc[0] = 0;
@@ -218,7 +253,7 @@ const TableBody = ({ data, isCheckbox, columns, startRow, columnWidths, freezedB
218
253
  acc[i + 1] = acc[i] + h;
219
254
  }
220
255
  return acc;
221
- }, [shouldVirtualize, total, data, measuredHeights, expandedRows, startRow, ROW_HEIGHT, ESTIMATED_EXPAND]);
256
+ }, [shouldVirtualize, groups, total, data, measuredHeights, expandedRows, startRow, ROW_HEIGHT, ESTIMATED_EXPAND]);
222
257
  let firstIndex = 0;
223
258
  let lastIndex = total;
224
259
  let topPad = 0;
@@ -228,9 +263,7 @@ const TableBody = ({ data, isCheckbox, columns, startRow, columnWidths, freezedB
228
263
  const totalH = offsets[total];
229
264
  const top = scrollTop - OVERSCAN * ROW_HEIGHT;
230
265
  const bottom = scrollTop + vh + OVERSCAN * ROW_HEIGHT;
231
- firstIndex = 0;
232
- while (firstIndex < total && offsets[firstIndex + 1] <= top)
233
- firstIndex++;
266
+ firstIndex = findFirstVisibleIndex(offsets, total, top);
234
267
  lastIndex = firstIndex;
235
268
  while (lastIndex < total && offsets[lastIndex] < bottom)
236
269
  lastIndex++;
@@ -265,9 +298,9 @@ const TableBody = ({ data, isCheckbox, columns, startRow, columnWidths, freezedB
265
298
  (isContent ? 1 : 0) +
266
299
  (isLink ? 1 : 0), [columns, isCheckbox, isContent, isLink]);
267
300
  const leftOffsets = useMemo(() => {
268
- const checkboxOffset = isCheckbox ? CHECKBOX_SLOT_OFFSET : 0;
301
+ const checkboxOffset = isCheckbox ? slotWidth : 0;
269
302
  return columnWidths.map((_, idx) => columnWidths.slice(0, idx).reduce((sum, w) => sum + w, 0) + checkboxOffset);
270
- }, [columnWidths, isCheckbox]);
303
+ }, [columnWidths, isCheckbox, slotWidth]);
271
304
  const borderStyle = useMemo(() => noBorders ? "none" : `0.063rem solid ${variant.row.ruleColor}`, [noBorders, variant.row.ruleColor]);
272
305
  // Selected-row tint applied per-cell (NOT a blanket `& td` override) so status
273
306
  // cells keep their solid color + white text instead of white-on-tint.
@@ -304,6 +337,38 @@ const TableBody = ({ data, isCheckbox, columns, startRow, columnWidths, freezedB
304
337
  }
305
338
  return { heights, offsets, totalH: acc };
306
339
  }, [groupItems, groupVirtualize, rowHeight]);
340
+ // Clamp scrollTop back when a shorter data set leaves it past the new content height.
341
+ useEffect(() => {
342
+ const totalH = shouldVirtualize && offsets ? offsets[total] : groupVirtualize && groupMetrics ? groupMetrics.totalH : null;
343
+ if (totalH == null)
344
+ return;
345
+ const maxScrollTop = Math.max(0, totalH - viewportH);
346
+ if (scrollTop > maxScrollTop) {
347
+ const el = scrollContainerRef === null || scrollContainerRef === void 0 ? void 0 : scrollContainerRef.current;
348
+ if (el)
349
+ el.scrollTop = maxScrollTop;
350
+ setScrollTop(maxScrollTop);
351
+ }
352
+ }, [shouldVirtualize, offsets, total, groupVirtualize, groupMetrics, viewportH, scrollTop, scrollContainerRef]);
353
+ // Prune measured heights for rows no longer in data so the map stays bounded.
354
+ useEffect(() => {
355
+ setMeasuredHeights((prev) => {
356
+ if (prev.size === 0)
357
+ return prev;
358
+ const ids = new Set(data.map(getRowId));
359
+ let changed = false;
360
+ const next = new Map();
361
+ prev.forEach((h, id) => {
362
+ if (ids.has(id)) {
363
+ next.set(id, h);
364
+ }
365
+ else {
366
+ changed = true;
367
+ }
368
+ });
369
+ return changed ? next : prev;
370
+ });
371
+ }, [data]);
307
372
  if (isLoading && data.length === 0) {
308
373
  return (_jsx(TableBodyLoading, { noOfColumns: totalVisibleColumns, borderColor: theme.colors.boxborder[200], rowsCount: typeof loadingSkeletonRows === "number" ? loadingSkeletonRows : 4, rowHeight: rowHeight }));
309
374
  }
@@ -8,24 +8,23 @@ import TableFilters from "../filters/TableFilters.js";
8
8
  import { ChevronDown, ChevronsUpDown, ChevronUp } from "lucide-react";
9
9
  import CheckBox from "../../Checkbox/Checkbox.js";
10
10
  import { useTableVariant } from "../variants/TableVariantContext.js";
11
- import { CHECKBOX_SLOT_OFFSET } from "./rowMetrics.js";
12
11
  import { transitionFor } from "../../../Theme/tokens/primitives/motion.js";
13
12
  import { reducedMotionSx } from "../../../Theme/chakra/motion.js";
14
13
  import { focusVisibleRing } from "../../../Theme/chakra/focusRing.styles.js";
15
14
  const LABEL_JUSTIFY = { start: "space-between", center: "center", end: "flex-start" };
16
- const TableHeader = ({ columns, isCheckbox, handleSort, headerRefs, columnWidths, columnsSort, freezedTextColor, noBorders, handleCheckbox, checked, isLoading, isContent, isLink, isActionFreeze, setColumnsSearch, columnsSearch, isSelecting, }) => {
15
+ const TableHeader = ({ columns, isCheckbox, handleSort, headerRefs, columnWidths, slotRef, slotWidth, columnsSort, freezedTextColor, noBorders, handleCheckbox, checked, isLoading, isContent, isLink, isActionFreeze, setColumnsSearch, columnsSearch, isSelecting, }) => {
17
16
  const [openFilterId, setOpenFilterId] = useState(null);
18
- return (_jsxs(Tr, { pr: 0, "aria-rowindex": 1, children: [isContent && _jsx(ContentSpacerCell, { noBorders: noBorders }), isCheckbox && (_jsx(SelectAllCell, { freezedTextColor: freezedTextColor, noBorders: noBorders, isSelecting: isSelecting, isLoading: isLoading, checked: checked, handleCheckbox: handleCheckbox })), columns.map((header, index) => {
17
+ return (_jsxs(Tr, { pr: 0, "aria-rowindex": 1, children: [isContent && _jsx(ContentSpacerCell, { noBorders: noBorders }), isCheckbox && (_jsx(SelectAllCell, { slotRef: slotRef, freezedTextColor: freezedTextColor, noBorders: noBorders, isSelecting: isSelecting, isLoading: isLoading, checked: checked, handleCheckbox: handleCheckbox })), columns.map((header, index) => {
19
18
  if (header === null || header === void 0 ? void 0 : header.isHidden)
20
19
  return null;
21
- return (_jsx(ColumnHeader, { header: header, index: index, columnWidths: columnWidths, handleSort: handleSort, isCheckbox: isCheckbox, noBorders: noBorders, columnsSort: columnsSort, headerRefs: headerRefs, columnsSearch: columnsSearch, setColumnsSearch: setColumnsSearch, openFilterId: openFilterId, setOpenFilterId: setOpenFilterId }, header.label));
20
+ return (_jsx(ColumnHeader, { header: header, index: index, columnWidths: columnWidths, slotWidth: slotWidth, handleSort: handleSort, isCheckbox: isCheckbox, noBorders: noBorders, columnsSort: columnsSort, headerRefs: headerRefs, columnsSearch: columnsSearch, setColumnsSearch: setColumnsSearch, openFilterId: openFilterId, setOpenFilterId: setOpenFilterId }, header.label));
22
21
  }), isLink && _jsx(ViewSpacerCell, { noBorders: noBorders, isActionFreeze: isActionFreeze })] }));
23
22
  };
24
23
  export default memo(TableHeader);
25
- const SelectAllCell = memo(({ freezedTextColor, noBorders, isSelecting, isLoading, checked, handleCheckbox, }) => {
24
+ const SelectAllCell = memo(({ slotRef, freezedTextColor, noBorders, isSelecting, isLoading, checked, handleCheckbox, }) => {
26
25
  const theme = useCustomTheme();
27
26
  const variant = useTableVariant();
28
- return (_jsx(Th, { scope: "col", w: variant.slot.width, minW: variant.slot.width, px: variant.slot.centered ? 0 : undefined, textAlign: variant.slot.centered ? "center" : undefined, fontSize: 14, fontWeight: 600, color: freezedTextColor, textTransform: "capitalize", backgroundColor: variant.header.bg, position: "sticky", className: `columns sticky-columns`, left: "0px", zIndex: 9, borderBottom: noBorders ? "none" : `0.063rem solid ${variant.header.ruleColor}`, children: _jsx(Box, { display: "flex", alignItems: "center", justifyContent: "center", children: isSelecting ? (_jsx(Spinner, { size: "sm", color: theme.colors.accentText })) : (_jsx(CheckBox, { "aria-label": "Select all rows", variant: "outline", onChange: () => handleCheckbox(0), isChecked: isLoading ? false : checked === true, isIndeterminate: checked === "indeterminate" })) }) }));
27
+ return (_jsx(Th, { ref: slotRef, scope: "col", w: variant.slot.width, minW: variant.slot.width, px: variant.slot.centered ? 0 : undefined, textAlign: variant.slot.centered ? "center" : undefined, fontSize: 14, fontWeight: 600, color: freezedTextColor, textTransform: "capitalize", backgroundColor: variant.header.bg, position: "sticky", className: `columns sticky-columns`, left: "0px", zIndex: 9, borderBottom: noBorders ? "none" : `0.063rem solid ${variant.header.ruleColor}`, children: _jsx(Box, { display: "flex", alignItems: "center", justifyContent: "center", children: isSelecting ? (_jsx(Spinner, { size: "sm", color: theme.colors.accentText })) : (_jsx(CheckBox, { "aria-label": "Select all rows", variant: "outline", onChange: () => handleCheckbox(0), isChecked: isLoading ? false : checked === true, isIndeterminate: checked === "indeterminate" })) }) }));
29
28
  });
30
29
  const ContentSpacerCell = memo(({ noBorders }) => {
31
30
  const variant = useTableVariant();
@@ -43,7 +42,7 @@ const SortingIcon = memo(({ label, sortState, isVisible, handleSortClick, }) =>
43
42
  color: accentTextOnCanvas(theme.colors.secondary, variant.header.bg),
44
43
  }, children: [sortState === "none" && _jsx(ChevronsUpDown, { size: 14 }), sortState === "asc" && _jsx(ChevronUp, { size: 14 }), sortState === "desc" && _jsx(ChevronDown, { size: 14 })] }));
45
44
  });
46
- const ColumnHeader = memo(({ header, index, columnWidths, isCheckbox, noBorders, columnsSort, headerRefs, columnsSearch, setColumnsSearch, handleSort, openFilterId, setOpenFilterId, }) => {
45
+ const ColumnHeader = memo(({ header, index, columnWidths, slotWidth, isCheckbox, noBorders, columnsSort, headerRefs, columnsSearch, setColumnsSearch, handleSort, openFilterId, setOpenFilterId, }) => {
47
46
  var _a, _b;
48
47
  const [isHovered, setIsHovered] = useState(false);
49
48
  const theme = useCustomTheme();
@@ -51,7 +50,7 @@ const ColumnHeader = memo(({ header, index, columnWidths, isCheckbox, noBorders,
51
50
  const isFrozen = header.isFreeze;
52
51
  const align = (_a = header.align) !== null && _a !== void 0 ? _a : "start";
53
52
  const leftOffset = isFrozen
54
- ? calculateLeftOffset(columnWidths, index) + (isCheckbox ? CHECKBOX_SLOT_OFFSET : 0)
53
+ ? calculateLeftOffset(columnWidths, index) + (isCheckbox ? slotWidth : 0)
55
54
  : undefined;
56
55
  const isSorting = header.isSort
57
56
  ? columnsSort.find((o) => o.column === header.id)
@@ -90,7 +89,7 @@ const ColumnHeader = memo(({ header, index, columnWidths, isCheckbox, noBorders,
90
89
  isSortActive ||
91
90
  openFilterId === header.id ||
92
91
  !!columnsSearch[header.id];
93
- return (_jsx(Th, { scope: "col", ref: (el) => (headerRefs.current[index] = el), "aria-sort": ariaSort, textAlign: align, textTransform: variant.header.textTransform, fontSize: variant.header.fontSize, fontWeight: variant.header.fontWeight, letterSpacing: variant.header.letterSpacing, position: isFrozen ? "sticky" : undefined, left: isFrozen ? leftOffset : undefined, minWidth: 120, backgroundColor: variant.header.bg, maxWidth: 500, zIndex: isFrozen ? 2 : 1, py: 2, pr: align === "end" ? undefined : 0, overflow: "hidden", borderTop: "none", borderBottom: noBorders ? "none" : `0.063rem solid ${variant.header.ruleColor}`, borderRight: noBorders || !variant.header.showColumnRule
92
+ return (_jsx(Th, { scope: "col", ref: (el) => (headerRefs.current[index] = el), "aria-sort": ariaSort, textAlign: align, textTransform: variant.header.textTransform, fontSize: variant.header.fontSize, fontWeight: variant.header.fontWeight, letterSpacing: variant.header.letterSpacing, position: isFrozen ? "sticky" : undefined, left: isFrozen ? `${leftOffset}px` : undefined, minWidth: 120, backgroundColor: variant.header.bg, maxWidth: 500, zIndex: isFrozen ? 2 : 1, py: 2, pr: align === "end" ? undefined : 0, overflow: "hidden", borderTop: "none", borderBottom: noBorders ? "none" : `0.063rem solid ${variant.header.ruleColor}`, borderRight: noBorders || !variant.header.showColumnRule
94
93
  ? "none"
95
94
  : `0.063rem solid ${variant.header.ruleColor}`, className: `columns ${isFrozen ? "sticky-columns" : "scrollable-columns"}`, height: variant.header.height, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: _jsxs(Box, { display: "flex", color: variant.header.ink, lineHeight: "1.25rem", justifyContent: LABEL_JUSTIFY[align], flexDirection: align === "end" ? "row-reverse" : "row", gap: align === "end" ? 1 : undefined, alignItems: "center", position: "relative", children: [_jsx(Box, { textAlign: align, textOverflow: "ellipsis", whiteSpace: "nowrap", display: "inline-block", overflow: "hidden", children: header.label }), _jsxs(Box, { display: "inline-flex", alignItems: "center", opacity: showActions ? 1 : 0, transition: transitionFor(["opacity"], "base"), sx: reducedMotionSx(), _focusWithin: { opacity: 1 }, children: [header.isSort && (_jsx(SortingIcon, { label: String(header.label), sortState: (_b = isSorting === null || isSorting === void 0 ? void 0 : isSorting.direction) !== null && _b !== void 0 ? _b : "none", isVisible: isHovered || isSortActive, handleSortClick: () => {
96
95
  let direction = "asc";
@@ -6,6 +6,5 @@ export const DENSITY_ROWS = {
6
6
  export const TOOLBAR_HEIGHT = 45;
7
7
  export const GROUP_HEADER_HEIGHT = 42;
8
8
  export const EMPTY_STATE_HEIGHT = 200;
9
- export const CHECKBOX_SLOT_OFFSET = 50;
10
9
  export const rowHeightFor = (density) => DENSITY_ROWS[density !== null && density !== void 0 ? density : "normal"].height;
11
10
  export const pxOf = (length) => parseFloat(length);
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { SortMultiColumnData } from "../../../Utils/table.js";
2
+ import { searchAndSortData, SortMultiColumnData } from "../../../Utils/table.js";
3
3
  export const DEFAULT_PAGE_SIZE = 20;
4
4
  const defaultPageSize = DEFAULT_PAGE_SIZE;
5
5
  const mergeColumnWithPreference = (pref, originalCol) => {
@@ -15,10 +15,25 @@ const mergeColumnWithPreference = (pref, originalCol) => {
15
15
  }
16
16
  return next;
17
17
  };
18
- const useTable = ({ data, isPagination, selections, onSelection, tablePreferences, savePreferences, noOfRowsPerPage, isServerPagination, totalRecords, columns, onNoOfRowsPerPageChange, defaultVisibleColumns, }) => {
18
+ const useTable = ({ data, isPagination, selections, onSelection, tablePreferences, savePreferences, noOfRowsPerPage, isServerPagination, totalRecords, columns, onNoOfRowsPerPageChange, defaultVisibleColumns, columnsSearch, }) => {
19
19
  var _a, _b;
20
20
  const headerRefs = useRef([]);
21
+ const slotObserver = useRef(null);
21
22
  const [columnWidths, setColumnWidths] = useState([]);
23
+ const [slotWidth, setSlotWidth] = useState(0);
24
+ // Frozen columns butt against the select-all cell, so its width is watched, not assumed.
25
+ const slotRef = useCallback((node) => {
26
+ var _a;
27
+ (_a = slotObserver.current) === null || _a === void 0 ? void 0 : _a.disconnect();
28
+ slotObserver.current = null;
29
+ if (!node) {
30
+ setSlotWidth(0);
31
+ return;
32
+ }
33
+ const observer = new ResizeObserver(() => setSlotWidth(node.offsetWidth));
34
+ observer.observe(node);
35
+ slotObserver.current = observer;
36
+ }, []);
22
37
  const [selection, setSelection] = useState(selections !== null && selections !== void 0 ? selections : []);
23
38
  const [columnsSort, setColumnsSort] = useState([]);
24
39
  const [currentPage, setCurrentPage] = useState(0);
@@ -80,27 +95,45 @@ const useTable = ({ data, isPagination, selections, onSelection, tablePreference
80
95
  if (!isServerPagination)
81
96
  setCurrentPage(0);
82
97
  }, [tableData.length, isServerPagination]);
98
+ const hasColumnSearch = useMemo(() => Object.values(columnsSearch).some((value) => value.trim() !== ""), [columnsSearch]);
99
+ // Column search runs before the page slice, or it would only ever see one page.
100
+ const searchedData = useMemo(() => (hasColumnSearch ? searchAndSortData(tableData, columnsSearch) : tableData), [hasColumnSearch, columnsSearch, tableData]);
83
101
  const pages = useMemo(() => {
84
102
  if (isServerPagination) {
85
103
  return Math.ceil(totalRecords / (rowsPerPage || 1));
86
104
  }
87
- return isPagination ? Math.ceil(tableData.length / rowsPerPage) : 0;
105
+ return isPagination ? Math.ceil(searchedData.length / rowsPerPage) : 0;
88
106
  }, [
89
107
  isPagination,
90
108
  rowsPerPage,
91
- tableData.length,
109
+ searchedData.length,
92
110
  totalRecords,
93
111
  isServerPagination,
94
112
  ]);
95
113
  const startRow = useMemo(() => (!isPagination ? 0 : currentPage * rowsPerPage), [currentPage, isPagination, rowsPerPage]);
96
- const endRow = useMemo(() => (!isPagination ? tableData.length : startRow + rowsPerPage), [isPagination, rowsPerPage, startRow, tableData.length]);
114
+ const endRow = useMemo(() => (!isPagination ? searchedData.length : startRow + rowsPerPage), [isPagination, rowsPerPage, startRow, searchedData.length]);
97
115
  const filteredData = useMemo(() => {
98
116
  if (isServerPagination) {
99
- return tableData;
117
+ return searchedData;
100
118
  }
101
- const sortedData = SortMultiColumnData(tableData, columnsSort);
119
+ const sortedData = SortMultiColumnData(searchedData, columnsSort);
102
120
  return sortedData.slice(startRow, endRow);
103
- }, [columnsSort, startRow, endRow, tableData, isServerPagination]);
121
+ }, [columnsSort, startRow, endRow, searchedData, isServerPagination]);
122
+ const searchKey = useMemo(() => JSON.stringify(columnsSearch), [columnsSearch]);
123
+ const prevSearchKey = useRef(searchKey);
124
+ useEffect(() => {
125
+ if (prevSearchKey.current === searchKey)
126
+ return;
127
+ prevSearchKey.current = searchKey;
128
+ setCurrentPage(0);
129
+ }, [searchKey]);
130
+ // A narrower result set can strand the user on a page that no longer exists.
131
+ useEffect(() => {
132
+ if (isServerPagination || !isPagination)
133
+ return;
134
+ const lastPage = Math.max(0, pages - 1);
135
+ setCurrentPage((page) => Math.min(page, lastPage));
136
+ }, [pages, isServerPagination, isPagination]);
104
137
  const handlePageSizeChange = (size) => {
105
138
  const value = Number(size);
106
139
  setRowsPerPage(value !== 0 ? value : defaultPageSize);
@@ -195,9 +228,12 @@ const useTable = ({ data, isPagination, selections, onSelection, tablePreference
195
228
  isLink,
196
229
  headerRefs,
197
230
  columnWidths,
231
+ slotRef,
232
+ slotWidth,
198
233
  handleSort,
199
234
  handleCheckbox,
200
235
  filteredData,
236
+ searchedData,
201
237
  startRow,
202
238
  endRow,
203
239
  selection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixelize-design-library",
3
- "version": "2.4.2-beta.47",
3
+ "version": "2.4.2-beta.48",
4
4
  "description": "React component library for Pixelize apps: themeable Chakra-based components, design tokens and light/dark brand theming.",
5
5
  "keywords": [
6
6
  "react",