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

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 (36) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/COMPONENT-GOTCHAS.md +41 -8
  3. package/DESIGN-GUIDE.md +1 -1
  4. package/dist/Components/Common/chipTriggerSize.d.ts +10 -0
  5. package/dist/Components/Common/chipTriggerSize.js +15 -0
  6. package/dist/Components/SearchSelect/SearchSelect.js +48 -7
  7. package/dist/Components/SearchSelect/SearchSelectProps.d.ts +5 -1
  8. package/dist/Components/Select/SelectNative.d.ts +2 -0
  9. package/dist/Components/Select/SelectPopup.js +97 -36
  10. package/dist/Components/Select/SelectProps.d.ts +4 -0
  11. package/dist/Components/SelectV2/SelectV2.d.ts +2 -9
  12. package/dist/Components/SelectV2/SelectV2.js +2 -10
  13. package/dist/Components/Table/Table.js +6 -7
  14. package/dist/Components/Table/TableProps.d.ts +9 -1
  15. package/dist/Components/Table/components/Pagination.js +8 -5
  16. package/dist/Components/Table/components/TableBody.d.ts +2 -1
  17. package/dist/Components/Table/components/TableBody.js +82 -15
  18. package/dist/Components/Table/components/TableHeader.d.ts +1 -1
  19. package/dist/Components/Table/components/TableHeader.js +8 -9
  20. package/dist/Components/Table/components/rowMetrics.d.ts +0 -1
  21. package/dist/Components/Table/components/rowMetrics.js +1 -2
  22. package/dist/Components/Table/hooks/useTable.d.ts +5 -1
  23. package/dist/Components/Table/hooks/useTable.js +43 -7
  24. package/dist/Theme/tokens/primitives/controlSizes.js +2 -2
  25. package/dist/esm/Components/Common/chipTriggerSize.js +11 -0
  26. package/dist/esm/Components/SearchSelect/SearchSelect.js +48 -7
  27. package/dist/esm/Components/Select/SelectPopup.js +97 -36
  28. package/dist/esm/Components/SelectV2/SelectV2.js +2 -9
  29. package/dist/esm/Components/Table/Table.js +6 -7
  30. package/dist/esm/Components/Table/components/Pagination.js +8 -5
  31. package/dist/esm/Components/Table/components/TableBody.js +81 -16
  32. package/dist/esm/Components/Table/components/TableHeader.js +8 -9
  33. package/dist/esm/Components/Table/components/rowMetrics.js +0 -1
  34. package/dist/esm/Components/Table/hooks/useTable.js +44 -8
  35. package/dist/esm/Theme/tokens/primitives/controlSizes.js +2 -2
  36. package/package.json +1 -1
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isControlSize = exports.DEFAULT_CONTROL_SIZE = exports.CONTROL_SIZE_NAMES = exports.CONTROL_SIZES = void 0;
4
4
  /** One height scale for fields, selects and buttons, so controls side by side line up. */
5
5
  exports.CONTROL_SIZES = {
6
- xs: { height: "1.5rem", fontSize: "0.75rem", paddingX: "0.5rem", iconSize: 12 },
7
- sm: { height: "2rem", fontSize: "0.8125rem", paddingX: "0.75rem", iconSize: 14 },
6
+ xs: { height: "1.75rem", fontSize: "0.75rem", paddingX: "0.5rem", iconSize: 12 },
7
+ sm: { height: "2.1875rem", fontSize: "0.8125rem", paddingX: "0.75rem", iconSize: 14 },
8
8
  md: { height: "2.5rem", fontSize: "0.875rem", paddingX: "1rem", iconSize: 16 },
9
9
  lg: { height: "3rem", fontSize: "1rem", paddingX: "1.5rem", iconSize: 18 },
10
10
  };
@@ -0,0 +1,11 @@
1
+ import { CONTROL_SIZES } from "../../Theme/tokens/primitives/controlSizes.js";
2
+ const CHIP_TAG_HEIGHT_PX = { xs: 20, sm: 24 };
3
+ const CONTROL_BORDER_PX = 2;
4
+ const controlHeightPx = (size) => parseFloat(CONTROL_SIZES[size].height) * 16;
5
+ const chipRowPaddingY = (size, chipSize) => `${(controlHeightPx(size) - CHIP_TAG_HEIGHT_PX[chipSize] - CONTROL_BORDER_PX) / 2 / 16}rem`;
6
+ /** Sizes a select trigger so a chip row lands on the same height as a plain one. */
7
+ export const chipTriggerSizeStyles = (size, hasChips, chipSize = "sm") => {
8
+ const spec = CONTROL_SIZES[size];
9
+ const base = { minH: spec.height, fontSize: spec.fontSize, px: spec.paddingX };
10
+ return hasChips ? { ...base, py: chipRowPaddingY(size, chipSize) } : { ...base, h: spec.height };
11
+ };
@@ -53,6 +53,9 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
53
53
  const dropdownMenuRef = useRef(null);
54
54
  const [dropdownPos, setDropdownPos] = useState({ top: 0, left: 0, width: 0 });
55
55
  const scrollTimeoutRef = useRef(null);
56
+ const optionsLengthRef = useRef(options.length);
57
+ const lastPageStartRef = useRef(null);
58
+ const optionsListRef = useRef(null);
56
59
  const [focused, setFocused] = useState(false);
57
60
  const [customSelectOpen, setCustomSelectOpen] = useState(false);
58
61
  const customSelectRef = useRef(null);
@@ -103,10 +106,22 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
103
106
  return () => clearTimeout(delayDebounce);
104
107
  }, [inputValue]);
105
108
  useEffect(() => {
106
- if (pagination === null || pagination === void 0 ? void 0 : pagination.limit) {
107
- setHasMore(options.length % pagination.limit === 0);
109
+ optionsLengthRef.current = options.length;
110
+ }, [options.length]);
111
+ useEffect(() => {
112
+ if ((pagination === null || pagination === void 0 ? void 0 : pagination.hasMore) !== undefined) {
113
+ setHasMore(pagination.hasMore);
114
+ return;
115
+ }
116
+ if ((pagination === null || pagination === void 0 ? void 0 : pagination.totalRecords) !== undefined) {
117
+ setHasMore(options.length < pagination.totalRecords);
118
+ return;
108
119
  }
109
- }, [options.length, pagination === null || pagination === void 0 ? void 0 : pagination.limit]);
120
+ if (!(pagination === null || pagination === void 0 ? void 0 : pagination.onScroll))
121
+ return;
122
+ const limit = pagination.limit || 10;
123
+ setHasMore(options.length % limit === 0);
124
+ }, [options.length, pagination === null || pagination === void 0 ? void 0 : pagination.hasMore, pagination === null || pagination === void 0 ? void 0 : pagination.totalRecords, pagination === null || pagination === void 0 ? void 0 : pagination.limit, pagination === null || pagination === void 0 ? void 0 : pagination.onScroll]);
110
125
  const handleSelect = (option) => {
111
126
  var _a;
112
127
  if (isMultiple && insideSelect) {
@@ -286,6 +301,13 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
286
301
  }
287
302
  setActiveIndex((prev) => (prev >= 0 && prev < filteredOptions.length ? prev : -1));
288
303
  }, [isOpen, filteredOptions.length]);
304
+ useEffect(() => {
305
+ var _a, _b;
306
+ if (!isOpen || activeIndex < 0 || !optionsListRef.current)
307
+ return;
308
+ (_b = (_a = optionsListRef.current
309
+ .querySelector(`[data-index="${activeIndex}"]`)) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, { block: "nearest" });
310
+ }, [isOpen, activeIndex]);
289
311
  // NOTE: only restores focus when a keyboard close actually unmounted Input and dropped focus to body.
290
312
  useEffect(() => {
291
313
  var _a, _b, _c, _d;
@@ -316,11 +338,30 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
316
338
  clearTimeout(scrollTimeoutRef.current);
317
339
  scrollTimeoutRef.current = setTimeout(() => {
318
340
  var _a;
319
- const startIndex = options.length + 1;
341
+ scrollTimeoutRef.current = null;
342
+ const startIndex = optionsLengthRef.current + 1;
343
+ if (lastPageStartRef.current === startIndex) {
344
+ setHasMore(false);
345
+ return;
346
+ }
347
+ lastPageStartRef.current = startIndex;
320
348
  (_a = pagination.onScroll) === null || _a === void 0 ? void 0 : _a.call(pagination, startIndex, pagination.limit || 10, inputValue);
321
349
  }, 200);
322
350
  }
323
- }, [pagination, options.length, inputValue, hasMore]);
351
+ }, [pagination, inputValue, hasMore]);
352
+ useEffect(() => {
353
+ if (isOpen)
354
+ return;
355
+ if (scrollTimeoutRef.current) {
356
+ clearTimeout(scrollTimeoutRef.current);
357
+ scrollTimeoutRef.current = null;
358
+ }
359
+ lastPageStartRef.current = null;
360
+ }, [isOpen]);
361
+ useEffect(() => () => {
362
+ if (scrollTimeoutRef.current)
363
+ clearTimeout(scrollTimeoutRef.current);
364
+ }, []);
324
365
  useOutsideClick({
325
366
  ref: customSelectRef,
326
367
  handler: (e) => {
@@ -498,10 +539,10 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
498
539
  else {
499
540
  openMenu();
500
541
  }
501
- }, "aria-label": "toggle-dropdown", display: "flex", alignItems: "center", borderRadius: "sm", color: theme.colors.textMuted, cursor: isDisabled ? "not-allowed" : "pointer", sx: focusVisibleStyles(theme.colors), children: isOpen ? (_jsx(ChevronUp, { size: s.chevronSize })) : (_jsx(ChevronDown, { size: s.chevronSize })) }))] })] }) }), isOpen && (_jsx(Portal, { children: _jsxs(Box, { ref: setDropdownMenuRef, id: listboxId, role: "listbox", "aria-multiselectable": isMultiple || undefined, position: "fixed", top: `${dropdownPos.top}px`, left: `${dropdownPos.left}px`, width: `${dropdownPos.width}px`, zIndex: 1500, maxH: "20rem", borderWidth: 1, borderColor: theme.colors.boxborder[200], borderRadius: "xl", overflow: "hidden", bg: theme.colors.surfaceRaised, boxShadow: theme.shadows.lg, display: "flex", flexDirection: "column", children: [isMultiple && isSelectAll && (_jsxs(Box, { py: spec.py, px: spec.px, minH: spec.minH, cursor: "pointer", onClick: handleSelectAll, display: "flex", alignItems: "center", gap: spec.gap, color: theme.colors.text[700], borderBottom: `0.063rem solid ${theme.colors.boxborder[200]}`, children: [_jsx(Checkbox, { isChecked: allFilteredSelected, sx: { pointerEvents: "none" }, size: s.checkboxSize }), _jsx(Text, { fontSize: spec.fontSize, children: allFilteredSelected ? "Unselect All" : "Select All" })] })), _jsx(Box, { flex: "1", overflowY: "auto", onScroll: handleScroll, maxH: "15rem", borderBottom: "0.063rem solid", borderColor: theme.colors.boxborder[200], children: isOptionLoading && filteredOptions.length === 0 ? (_jsxs(Box, { py: spec.py, px: spec.px, display: "flex", alignItems: "center", gap: spec.gap, minH: spec.minH, color: theme.colors.textMuted, children: [_jsx(Spinner, { size: s.spinnerSize }), _jsx(Text, { fontSize: spec.fontSize, children: loadingText })] })) : !isOptionLoading && filteredOptions.length === 0 ? (_jsx(Text, { p: 3, fontSize: spec.fontSize, color: theme.colors.textMuted, children: "No results found" })) : (_jsxs(_Fragment, { children: [filteredOptions.map((option, index) => {
542
+ }, "aria-label": "toggle-dropdown", display: "flex", alignItems: "center", borderRadius: "sm", color: theme.colors.textMuted, cursor: isDisabled ? "not-allowed" : "pointer", sx: focusVisibleStyles(theme.colors), children: isOpen ? (_jsx(ChevronUp, { size: s.chevronSize })) : (_jsx(ChevronDown, { size: s.chevronSize })) }))] })] }) }), isOpen && (_jsx(Portal, { children: _jsxs(Box, { ref: setDropdownMenuRef, id: listboxId, role: "listbox", "aria-multiselectable": isMultiple || undefined, position: "fixed", top: `${dropdownPos.top}px`, left: `${dropdownPos.left}px`, width: `${dropdownPos.width}px`, zIndex: 1500, maxH: "20rem", borderWidth: 1, borderColor: theme.colors.boxborder[200], borderRadius: "xl", overflow: "hidden", bg: theme.colors.surfaceRaised, boxShadow: theme.shadows.lg, display: "flex", flexDirection: "column", children: [isMultiple && isSelectAll && (_jsxs(Box, { py: spec.py, px: spec.px, minH: spec.minH, cursor: "pointer", onClick: handleSelectAll, display: "flex", alignItems: "center", gap: spec.gap, color: theme.colors.text[700], borderBottom: `0.063rem solid ${theme.colors.boxborder[200]}`, children: [_jsx(Checkbox, { isChecked: allFilteredSelected, sx: { pointerEvents: "none" }, size: s.checkboxSize }), _jsx(Text, { fontSize: spec.fontSize, children: allFilteredSelected ? "Unselect All" : "Select All" })] })), _jsx(Box, { ref: optionsListRef, flex: "1", overflowY: "auto", onScroll: handleScroll, maxH: "15rem", borderBottom: "0.063rem solid", borderColor: theme.colors.boxborder[200], children: isOptionLoading && filteredOptions.length === 0 ? (_jsxs(Box, { py: spec.py, px: spec.px, display: "flex", alignItems: "center", gap: spec.gap, minH: spec.minH, color: theme.colors.textMuted, children: [_jsx(Spinner, { size: s.spinnerSize }), _jsx(Text, { fontSize: spec.fontSize, children: loadingText })] })) : !isOptionLoading && filteredOptions.length === 0 ? (_jsx(Text, { p: 3, fontSize: spec.fontSize, color: theme.colors.textMuted, children: "No results found" })) : (_jsxs(_Fragment, { children: [filteredOptions.map((option, index) => {
502
543
  const selected = isSelected(option.id);
503
544
  const singleSelected = selected && !isMultiple;
504
- return (_jsxs(Box, { id: getOptionId(index), role: "option", "aria-selected": selected, py: spec.py, px: spec.px, minH: spec.minH, cursor: "pointer", bg: selected
545
+ return (_jsxs(Box, { id: getOptionId(index), "data-index": index, role: "option", "aria-selected": selected, py: spec.py, px: spec.px, minH: spec.minH, cursor: "pointer", bg: selected
505
546
  ? index === activeIndex
506
547
  ? rowBackgrounds.selectedActive
507
548
  : rowBackgrounds.selected
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { Box, Checkbox, FormControl, Input, Popover, PopoverAnchor, PopoverContent, Portal, Spinner, useDisclosure, useMergeRefs, } from "@chakra-ui/react";
4
4
  import { Check, ChevronDown, X } from "lucide-react";
5
5
  import { useCustomTheme } from "../../Theme/useCustomTheme.js";
@@ -11,6 +11,7 @@ import { scrollbarStyles } from "../../Theme/tokens/builders/scrollbar.js";
11
11
  import { CONTROL_SIZES, DEFAULT_CONTROL_SIZE } from "../../Theme/tokens/primitives/controlSizes.js";
12
12
  import { transitionFor } from "../../Theme/tokens/primitives/motion.js";
13
13
  import { reducedMotionSx } from "../../Theme/chakra/motion.js";
14
+ import { chipTriggerSizeStyles } from "../Common/chipTriggerSize.js";
14
15
  import { fieldControlStyles } from "../Common/fieldStyles.js";
15
16
  import { useFieldIds } from "../Common/useFieldIds.js";
16
17
  import FieldMessage from "../Common/FieldMessage.js";
@@ -23,14 +24,6 @@ const PAGE_STEP = 8;
23
24
  const LOAD_MORE_THRESHOLD_PX = 8;
24
25
  const DEFAULT_MAX_VISIBLE_CHIPS = 6;
25
26
  const VIRTUAL_OVERSCAN = 4;
26
- const CHIP_TAG_HEIGHT_PX = { xs: 20, sm: 24 };
27
- const controlHeightPx = (size) => parseFloat(CONTROL_SIZES[size].height) * 16;
28
- const chipRowPaddingY = (size, chipSize) => `${(controlHeightPx(size) - CHIP_TAG_HEIGHT_PX[chipSize]) / 2 / 16}rem`;
29
- const chipTriggerSizeStyles = (size, hasChips, chipSize = "sm") => {
30
- const spec = CONTROL_SIZES[size];
31
- const base = { minH: spec.height, fontSize: spec.fontSize, px: spec.paddingX };
32
- return hasChips ? { ...base, py: chipRowPaddingY(size, chipSize) } : { ...base, h: spec.height };
33
- };
34
27
  const asIdSet = (values) => new Set((values !== null && values !== void 0 ? values : []).map((v) => v.toString()));
35
28
  /** Regression-guarded id scheme — tickets e2e (`e2e/tests/helpers/selectV2.ts`) resolves it verbatim. */
36
29
  const optionDomId = (listboxId, id) => `${listboxId}-option-${id.toString().replace(/[^a-zA-Z0-9_-]/g, "-")}`;
@@ -60,7 +53,7 @@ const buildGroupedRows = (options) => {
60
53
  /** The unified `Select`'s popup mode (`native={false}`) — absorbs SelectV2's behaviour, plus clear/create/groups/virtualization. */
61
54
  const SelectPopup = forwardRef(function SelectPopup(props, ref) {
62
55
  var _a, _b, _c, _d;
63
- const { options = [], id, name, placeholder = "Select Option", label, error = false, errorMessage, helperText, isDisabled = false, isReadOnly = false, isRequired = false, isInformation = false, informationMessage, size = DEFAULT_CONTROL_SIZE, isSearchable = true, width, formControlStyle, onBlur, onFocus, renderOption, renderValue, maxMenuHeight = "16rem", defaultOpen = false, isLoading = false, loadingText = "Loading...", onLoadMore, onSearchChange, searchValue, defaultSearchValue, searchDebounceMs = 0, isClearable = false, onClear, noOptionsMessage, onCreate, createLabel, virtualizeAfter, "data-testid": dataTestId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, } = props;
56
+ const { options = [], id, name, placeholder = "Select Option", label, error = false, errorMessage, helperText, isDisabled = false, isReadOnly = false, isRequired = false, isInformation = false, informationMessage, size = DEFAULT_CONTROL_SIZE, isSearchable = true, width, formControlStyle, onBlur, onFocus, renderOption, renderValue, maxMenuHeight = "16rem", defaultOpen = false, isLoading = false, loadingText = "Loading...", onLoadMore, hasMore, onSearchChange, searchValue, defaultSearchValue, searchDebounceMs = 0, filterOptions, isClearable = false, onClear, noOptionsMessage, onCreate, createLabel, virtualizeAfter, "data-testid": dataTestId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, } = props;
64
57
  const theme = useCustomTheme();
65
58
  const { colors } = theme;
66
59
  const { isOpen, onOpen, onClose } = useDisclosure({ defaultIsOpen: defaultOpen });
@@ -70,6 +63,8 @@ const SelectPopup = forwardRef(function SelectPopup(props, ref) {
70
63
  const mergedTriggerRef = useMergeRefs(triggerRef, ref);
71
64
  const filterInputRef = useRef(null);
72
65
  const listRef = useRef(null);
66
+ const lastActiveIndex = useRef(-1);
67
+ const wasVirtualized = useRef(false);
73
68
  const [triggerWidth, setTriggerWidth] = useState(0);
74
69
  const ids = useFieldIds(id, { error, errorMessage, helperText });
75
70
  const listboxId = `${ids.inputId}-listbox`;
@@ -87,13 +82,28 @@ const SelectPopup = forwardRef(function SelectPopup(props, ref) {
87
82
  const selectedIds = useMemo(() => props.isMulti
88
83
  ? asIdSet(currentValue)
89
84
  : asIdSet(currentValue !== undefined ? [currentValue] : undefined), [props.isMulti, currentValue]);
90
- const selectedOptions = useMemo(() => options.filter((option) => selectedIds.has(option.id.toString())), [options, selectedIds]);
85
+ const selectionCacheRef = useRef(new Map());
86
+ const selectedOptions = useMemo(() => {
87
+ const cache = selectionCacheRef.current;
88
+ options.forEach((option) => {
89
+ if (selectedIds.has(option.id.toString()))
90
+ cache.set(option.id.toString(), option);
91
+ });
92
+ Array.from(cache.keys()).forEach((cachedId) => {
93
+ if (!selectedIds.has(cachedId))
94
+ cache.delete(cachedId);
95
+ });
96
+ return Array.from(selectedIds)
97
+ .map((selectedId) => cache.get(selectedId))
98
+ .filter((option) => option !== undefined);
99
+ }, [options, selectedIds]);
100
+ const shouldFilterLocally = filterOptions !== null && filterOptions !== void 0 ? filterOptions : !onSearchChange;
91
101
  const filteredOptions = useMemo(() => {
92
- if (!isSearchable || !currentSearch.trim())
102
+ if (!isSearchable || !currentSearch.trim() || !shouldFilterLocally)
93
103
  return options;
94
104
  const needle = currentSearch.trim().toLowerCase();
95
105
  return options.filter((option) => option.label.toString().toLowerCase().includes(needle));
96
- }, [options, currentSearch, isSearchable]);
106
+ }, [options, currentSearch, isSearchable, shouldFilterLocally]);
97
107
  const hasGroups = useMemo(() => options.some((option) => option.group), [options]);
98
108
  const resetAndClose = useCallback((opts) => {
99
109
  if (props.isMulti && (opts === null || opts === void 0 ? void 0 : opts.keepOpenForMulti))
@@ -115,15 +125,19 @@ const SelectPopup = forwardRef(function SelectPopup(props, ref) {
115
125
  : currentValue;
116
126
  setActiveIndex(filteredOptions.findIndex((option) => option.id.toString() === (firstSelected === null || firstSelected === void 0 ? void 0 : firstSelected.toString())));
117
127
  };
128
+ const selectedIdsRef = useRef(selectedIds);
129
+ selectedIdsRef.current = selectedIds;
130
+ const selectedOptionsRef = useRef(selectedOptions);
131
+ selectedOptionsRef.current = selectedOptions;
118
132
  const handleSelect = useCallback((option) => {
119
133
  var _a, _b;
120
134
  if (option.isDisabled)
121
135
  return;
122
136
  if (props.isMulti) {
123
- const alreadySelected = selectedIds.has(option.id.toString());
137
+ const alreadySelected = selectedIdsRef.current.has(option.id.toString());
124
138
  const next = alreadySelected
125
- ? selectedOptions.filter((selected) => selected.id.toString() !== option.id.toString())
126
- : [...selectedOptions, option];
139
+ ? selectedOptionsRef.current.filter((selected) => selected.id.toString() !== option.id.toString())
140
+ : [...selectedOptionsRef.current, option];
127
141
  if (!isValueControlled)
128
142
  setUncontrolledValue(next.map((o) => o.id));
129
143
  props.onChange(next);
@@ -138,7 +152,7 @@ const SelectPopup = forwardRef(function SelectPopup(props, ref) {
138
152
  (_b = triggerRef.current) === null || _b === void 0 ? void 0 : _b.focus();
139
153
  },
140
154
  // eslint-disable-next-line react-hooks/exhaustive-deps
141
- [props.isMulti, props.onChange, selectedIds, selectedOptions, resetAndClose, isValueControlled]);
155
+ [props.isMulti, props.onChange, resetAndClose, isValueControlled]);
142
156
  const handleCreate = useCallback(() => {
143
157
  if (!onCreate)
144
158
  return;
@@ -180,24 +194,47 @@ const SelectPopup = forwardRef(function SelectPopup(props, ref) {
180
194
  const maxMenuHeightPx = useMemo(() => parseRemToPx(maxMenuHeight, 256), [maxMenuHeight]);
181
195
  const shouldVirtualize = !!virtualizeAfter && !hasGroups && filteredOptions.length > virtualizeAfter;
182
196
  const [scrollTop, setScrollTop] = useState(0);
183
- const visibleCount = Math.ceil(maxMenuHeightPx / spec.virtualRowHeightPx) + VIRTUAL_OVERSCAN * 2;
197
+ // A description, an avatar or a renderOption row outgrows the spec's single-line height.
198
+ const [rowHeightPx, setRowHeightPx] = useState(spec.virtualRowHeightPx);
199
+ const visibleCount = Math.ceil(maxMenuHeightPx / rowHeightPx) + VIRTUAL_OVERSCAN * 2;
184
200
  const startIndex = shouldVirtualize
185
- ? Math.max(0, Math.floor(scrollTop / spec.virtualRowHeightPx) - VIRTUAL_OVERSCAN)
201
+ ? Math.max(0, Math.floor(scrollTop / rowHeightPx) - VIRTUAL_OVERSCAN)
186
202
  : 0;
187
203
  const endIndex = shouldVirtualize
188
204
  ? Math.min(filteredOptions.length, startIndex + visibleCount)
189
205
  : filteredOptions.length;
190
206
  const renderedOptions = shouldVirtualize ? filteredOptions.slice(startIndex, endIndex) : filteredOptions;
191
- const topPadPx = shouldVirtualize ? startIndex * spec.virtualRowHeightPx : 0;
192
- const bottomPadPx = shouldVirtualize ? (filteredOptions.length - endIndex) * spec.virtualRowHeightPx : 0;
207
+ const topPadPx = shouldVirtualize ? startIndex * rowHeightPx : 0;
208
+ const bottomPadPx = shouldVirtualize ? (filteredOptions.length - endIndex) * rowHeightPx : 0;
209
+ useLayoutEffect(() => {
210
+ var _a;
211
+ if (!isOpen)
212
+ return;
213
+ const row = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[role="option"]');
214
+ if (!row)
215
+ return;
216
+ const measured = row.getBoundingClientRect().height;
217
+ if (measured > 0 && Math.abs(measured - rowHeightPx) > 0.5)
218
+ setRowHeightPx(measured);
219
+ }, [isOpen, rowHeightPx, filteredOptions.length]);
220
+ // Windowing starts mid-list, so the window must open where the user already is.
221
+ useLayoutEffect(() => {
222
+ var _a, _b;
223
+ if (shouldVirtualize && !wasVirtualized.current)
224
+ setScrollTop((_b = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0);
225
+ wasVirtualized.current = shouldVirtualize;
226
+ }, [shouldVirtualize]);
193
227
  useEffect(() => {
194
228
  var _a, _b;
195
229
  if (activeIndex < 0 || !listRef.current)
196
230
  return;
231
+ if (lastActiveIndex.current === activeIndex)
232
+ return;
233
+ lastActiveIndex.current = activeIndex;
197
234
  const el = listRef.current;
198
235
  if (shouldVirtualize) {
199
- const targetTop = activeIndex * spec.virtualRowHeightPx;
200
- const targetBottom = targetTop + spec.virtualRowHeightPx;
236
+ const targetTop = activeIndex * rowHeightPx;
237
+ const targetBottom = targetTop + rowHeightPx;
201
238
  if (targetTop < el.scrollTop) {
202
239
  el.scrollTop = targetTop;
203
240
  setScrollTop(targetTop);
@@ -210,18 +247,44 @@ const SelectPopup = forwardRef(function SelectPopup(props, ref) {
210
247
  return;
211
248
  }
212
249
  (_b = (_a = el.querySelector(`[data-index="${activeIndex}"]`)) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, { block: "nearest" });
213
- }, [activeIndex, shouldVirtualize, maxMenuHeightPx, spec.virtualRowHeightPx]);
250
+ }, [activeIndex, shouldVirtualize, maxMenuHeightPx, rowHeightPx]);
251
+ const loadMoreInFlightRef = useRef(false);
252
+ const prevOptionsLengthRef = useRef(options.length);
253
+ const prevIsLoadingRef = useRef(isLoading);
254
+ useEffect(() => {
255
+ if (options.length !== prevOptionsLengthRef.current)
256
+ loadMoreInFlightRef.current = false;
257
+ prevOptionsLengthRef.current = options.length;
258
+ }, [options.length]);
259
+ useEffect(() => {
260
+ if (prevIsLoadingRef.current && !isLoading)
261
+ loadMoreInFlightRef.current = false;
262
+ prevIsLoadingRef.current = isLoading;
263
+ }, [isLoading]);
264
+ const canLoadMore = !!onLoadMore && hasMore !== false;
265
+ const triggerLoadMore = useCallback(() => {
266
+ if (!onLoadMore || hasMore === false || isLoading || loadMoreInFlightRef.current)
267
+ return;
268
+ loadMoreInFlightRef.current = true;
269
+ onLoadMore();
270
+ }, [onLoadMore, hasMore, isLoading]);
214
271
  const moveActive = (delta) => {
215
272
  if (!filteredOptions.length)
216
273
  return;
217
- setActiveIndex((current) => {
218
- const next = current + delta;
219
- if (next < 0)
220
- return filteredOptions.length - 1;
221
- if (next >= filteredOptions.length)
222
- return 0;
223
- return next;
224
- });
274
+ const next = activeIndex + delta;
275
+ if (delta > 0 && next >= filteredOptions.length) {
276
+ if (canLoadMore) {
277
+ triggerLoadMore();
278
+ return;
279
+ }
280
+ setActiveIndex(0);
281
+ return;
282
+ }
283
+ if (next < 0) {
284
+ setActiveIndex(filteredOptions.length - 1);
285
+ return;
286
+ }
287
+ setActiveIndex(next);
225
288
  };
226
289
  const moveActiveBy = (delta) => {
227
290
  if (!filteredOptions.length)
@@ -308,10 +371,8 @@ const SelectPopup = forwardRef(function SelectPopup(props, ref) {
308
371
  const { scrollTop: nextScrollTop, clientHeight, scrollHeight } = event.currentTarget;
309
372
  if (shouldVirtualize)
310
373
  setScrollTop(nextScrollTop);
311
- if (!onLoadMore || isLoading)
312
- return;
313
374
  if (nextScrollTop + clientHeight >= scrollHeight - LOAD_MORE_THRESHOLD_PX)
314
- onLoadMore();
375
+ triggerLoadMore();
315
376
  };
316
377
  const handleFilterChange = (event) => {
317
378
  const nextText = event.target.value;
@@ -373,7 +434,7 @@ const SelectPopup = forwardRef(function SelectPopup(props, ref) {
373
434
  const selectedValueIds = selectedOptions.map((option) => option.id.toString());
374
435
  const hiddenValues = !name ? [] : props.isMulti ? selectedValueIds : [(_c = selectedValueIds[0]) !== null && _c !== void 0 ? _c : ""];
375
436
  const groupedRows = useMemo(() => (hasGroups ? buildGroupedRows(filteredOptions) : []), [hasGroups, filteredOptions]);
376
- return (_jsxs(FormControl, { isInvalid: error, isDisabled: isDisabled, isReadOnly: isReadOnly, style: formControlStyle, width: width, children: [label && (_jsx(TextLabel, { label: label, id: ids.inputId, isRequired: isRequired, isInformation: isInformation, informationMessage: informationMessage })), _jsxs(Popover, { isOpen: isOpen, onClose: () => resetAndClose(), placement: "bottom-start", autoFocus: false, returnFocusOnClose: false, gutter: 4, children: [_jsx(PopoverAnchor, { children: _jsxs(Box, { as: "div", ref: mergedTriggerRef, ...ids.controlProps, role: "combobox", tabIndex: isDisabled ? -1 : 0, "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-owns": `${listboxId} ${filterInputId}`, "aria-activedescendant": activeDescendantId, "aria-disabled": isDisabled || undefined, "data-readonly": isReadOnly || undefined, "aria-required": isRequired || undefined, "aria-label": triggerAriaLabel(), "aria-labelledby": ariaLabelledBy, onClick: handleOpen, onKeyDown: handleTriggerKeyDown, display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", textAlign: "left", cursor: isDisabled ? "not-allowed" : isReadOnly ? "default" : "pointer", "data-testid": dataTestId, sx: triggerSx, children: [_jsx(Box, { as: "span", display: "inline-flex", flexWrap: showChips ? "wrap" : "nowrap", alignItems: "center", gap: showChips ? 1 : 1.5, minW: 0, flex: 1, isTruncated: !showChips, children: showChips ? (_jsxs(_Fragment, { children: [visibleChipOptions.map((option) => (_jsx(Tag, { label: String(option.label), size: chipSize, variant: "subtle", icon: isInteractive ? X : undefined, onIconClick: isInteractive ? () => handleSelect(option) : undefined }, option.id))), overflowChipCount > 0 && (_jsx(Tag, { label: `+${overflowChipCount}`, size: chipSize, variant: "subtle", colorScheme: "gray" }))] })) : (_jsxs(_Fragment, { children: [triggerIcon ? (_jsx(Box, { as: "span", display: "inline-flex", flexShrink: 0, children: triggerIcon })) : null, _jsx(Box, { as: "span", isTruncated: true, children: renderValue ? renderValue(selectedOptions) : defaultTriggerContent() })] })) }), showClear && (_jsx(Box, { as: "button", type: "button", "aria-label": "Clear selection", onClick: handleClear, display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0, ml: 2, color: colors.textMuted, _hover: { color: colors.text[700] }, children: _jsx(X, { size: CONTROL_SIZES[size].iconSize }) })), _jsx(Box, { as: "span", display: "inline-flex", flexShrink: 0, ml: 2, transition: CHEVRON_TRANSITION, transform: isOpen ? "rotate(180deg)" : "rotate(0deg)", color: colors.textMuted, sx: reducedMotionSx(), children: _jsx(ChevronDown, { size: CONTROL_SIZES[size].iconSize }) })] }) }), _jsx(Portal, { children: _jsxs(PopoverContent, { width: "max-content", minWidth: triggerWidth ? `${triggerWidth}px` : undefined, maxWidth: "min(24rem, calc(100vw - 2rem))", borderRadius: "xl", boxShadow: theme.shadows.lg, overflow: "hidden", onKeyDown: handleListKeyDown, children: [_jsx(Box, { p: 2, borderBottom: `0.063rem solid ${colors.boxborder[200]}`, display: isSearchable ? undefined : "none", children: _jsx(Input, { ref: filterInputRef, id: filterInputId, size: "sm", placeholder: "Type to filter...", value: currentSearch, onChange: handleFilterChange, "aria-activedescendant": activeDescendantId, "aria-controls": listboxId, "aria-label": "Filter options", tabIndex: isSearchable ? undefined : -1 }) }), _jsxs(Box, { id: listboxId, ref: listRef, role: "listbox", "aria-label": (_d = label !== null && label !== void 0 ? label : ariaLabel) !== null && _d !== void 0 ? _d : placeholder, "aria-multiselectable": props.isMulti || undefined, "aria-busy": isLoading || undefined, maxH: maxMenuHeight, overflowY: "auto", py: 1, sx: listScrollStyles, onScroll: handleListScroll, children: [filteredOptions.length === 0 && !isLoading && !showCreateRow && (_jsx(Box, { mx: spec.mx, px: spec.px, py: spec.py, fontSize: spec.fontSize, color: colors.textMuted, children: noOptionsMessage !== null && noOptionsMessage !== void 0 ? noOptionsMessage : "No Options" })), showCreateRow && (_jsx(Box, { id: createRowId, role: "option", "aria-selected": false, "data-index": -1, onClick: handleCreate, mx: spec.mx, px: spec.px, py: spec.py, minH: spec.minH, borderRadius: "md", cursor: "pointer", fontSize: spec.fontSize, fontWeight: 600, color: colors.primary[600], bg: activeDescendantId === createRowId ? rowBackgrounds.active : "transparent", children: createLabel ? createLabel(currentSearch.trim()) : `Create "${currentSearch.trim()}"` })), hasGroups
437
+ return (_jsxs(FormControl, { isInvalid: error, isDisabled: isDisabled, isReadOnly: isReadOnly, style: formControlStyle, width: width, children: [label && (_jsx(TextLabel, { label: label, id: ids.inputId, isRequired: isRequired, isInformation: isInformation, informationMessage: informationMessage })), _jsxs(Popover, { isOpen: isOpen, onClose: () => resetAndClose(), placement: "bottom-start", autoFocus: false, returnFocusOnClose: false, gutter: 4, children: [_jsx(PopoverAnchor, { children: _jsxs(Box, { as: "div", ref: mergedTriggerRef, ...ids.controlProps, role: "combobox", tabIndex: isDisabled ? -1 : 0, "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-owns": `${listboxId} ${filterInputId}`, "aria-activedescendant": activeDescendantId, "aria-disabled": isDisabled || undefined, "data-readonly": isReadOnly || undefined, "aria-required": isRequired || undefined, "aria-label": triggerAriaLabel(), "aria-labelledby": ariaLabelledBy, onClick: handleOpen, onKeyDown: handleTriggerKeyDown, display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", textAlign: "left", cursor: isDisabled ? "not-allowed" : isReadOnly ? "default" : "pointer", "data-testid": dataTestId, sx: triggerSx, children: [_jsx(Box, { as: "span", display: "inline-flex", flexWrap: showChips ? "wrap" : "nowrap", alignItems: "center", gap: showChips ? 1 : 1.5, minW: 0, flex: 1, isTruncated: !showChips, children: showChips ? (_jsxs(_Fragment, { children: [visibleChipOptions.map((option) => (_jsx(Tag, { label: String(option.label), size: chipSize, variant: "subtle", icon: isInteractive ? X : undefined, onIconClick: isInteractive ? () => handleSelect(option) : undefined }, option.id))), overflowChipCount > 0 && (_jsx(Tag, { label: `+${overflowChipCount}`, size: chipSize, variant: "subtle", colorScheme: "gray" }))] })) : (_jsxs(_Fragment, { children: [triggerIcon ? (_jsx(Box, { as: "span", display: "inline-flex", flexShrink: 0, children: triggerIcon })) : null, _jsx(Box, { as: "span", isTruncated: true, children: renderValue ? renderValue(selectedOptions) : defaultTriggerContent() })] })) }), showClear && (_jsx(Box, { as: "button", type: "button", "aria-label": "Clear selection", onClick: handleClear, display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: 0, ml: 2, color: colors.textMuted, _hover: { color: colors.text[700] }, children: _jsx(X, { size: CONTROL_SIZES[size].iconSize }) })), _jsx(Box, { as: "span", display: "inline-flex", flexShrink: 0, ml: 2, transition: CHEVRON_TRANSITION, transform: isOpen ? "rotate(180deg)" : "rotate(0deg)", color: colors.textMuted, sx: reducedMotionSx(), children: _jsx(ChevronDown, { size: CONTROL_SIZES[size].iconSize }) })] }) }), _jsx(Portal, { children: _jsxs(PopoverContent, { width: "max-content", minWidth: triggerWidth ? `${triggerWidth}px` : undefined, maxWidth: "min(24rem, calc(100vw - 2rem))", borderRadius: "xl", boxShadow: theme.shadows.lg, overflow: "hidden", onKeyDown: handleListKeyDown, children: [_jsx(Box, { p: spec.mx, borderBottom: `0.063rem solid ${colors.boxborder[200]}`, display: isSearchable ? undefined : "none", children: _jsx(Input, { ref: filterInputRef, id: filterInputId, size: size, px: spec.px, placeholder: "Type to filter...", value: currentSearch, onChange: handleFilterChange, "aria-activedescendant": activeDescendantId, "aria-controls": listboxId, "aria-label": "Filter options", tabIndex: isSearchable ? undefined : -1 }) }), _jsxs(Box, { id: listboxId, ref: listRef, role: "listbox", "aria-label": (_d = label !== null && label !== void 0 ? label : ariaLabel) !== null && _d !== void 0 ? _d : placeholder, "aria-multiselectable": props.isMulti || undefined, "aria-busy": isLoading || undefined, maxH: maxMenuHeight, overflowY: "auto", py: 1, sx: listScrollStyles, onScroll: handleListScroll, children: [filteredOptions.length === 0 && !isLoading && !showCreateRow && (_jsx(Box, { mx: spec.mx, px: spec.px, py: spec.py, fontSize: spec.fontSize, color: colors.textMuted, children: noOptionsMessage !== null && noOptionsMessage !== void 0 ? noOptionsMessage : "No Options" })), showCreateRow && (_jsx(Box, { id: createRowId, role: "option", "aria-selected": false, "data-index": -1, onClick: handleCreate, mx: spec.mx, px: spec.px, py: spec.py, minH: spec.minH, borderRadius: "md", cursor: "pointer", fontSize: spec.fontSize, fontWeight: 600, color: colors.primary[600], bg: activeDescendantId === createRowId ? rowBackgrounds.active : "transparent", children: createLabel ? createLabel(currentSearch.trim()) : `Create "${currentSearch.trim()}"` })), hasGroups
377
438
  ? groupedRows.map((row) => {
378
439
  var _a;
379
440
  return row.kind === "header" ? (_jsx(Box, { mx: spec.mx, px: spec.px, pt: 2, pb: 1, fontSize: "0.6875rem", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.03em", color: colors.textMuted, children: row.group }, row.key)) : (_jsx(OptionRow, { option: row.option, index: row.index, listboxId: listboxId, isSelected: selectedIds.has(row.option.id.toString()), isActive: row.index === activeIndex, isMulti: (_a = props.isMulti) !== null && _a !== void 0 ? _a : false, theme: theme, backgrounds: rowBackgrounds, spec: spec, renderOption: renderOption, onSelect: handleSelect, onActivate: setActiveIndex }, row.key));
@@ -1,15 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from "react";
3
3
  import Select from "../Select/Select.js";
4
- import { CONTROL_SIZES } from "../../Theme/tokens/primitives/controlSizes.js";
5
- const CHIP_TAG_HEIGHT_PX = { xs: 20, sm: 24 };
6
- const controlHeightPx = (size) => parseFloat(CONTROL_SIZES[size].height) * 16;
7
- const chipRowPaddingY = (size, chipSize) => `${(controlHeightPx(size) - CHIP_TAG_HEIGHT_PX[chipSize]) / 2 / 16}rem`;
8
- export const chipTriggerSizeStyles = (size, hasChips, chipSize = "sm") => {
9
- const spec = CONTROL_SIZES[size];
10
- const base = { minH: spec.height, fontSize: spec.fontSize, px: spec.paddingX };
11
- return hasChips ? { ...base, py: chipRowPaddingY(size, chipSize) } : { ...base, h: spec.height };
12
- };
4
+ import { chipTriggerSizeStyles } from "../Common/chipTriggerSize.js";
5
+ export { chipTriggerSizeStyles };
13
6
  /** @deprecated Use `Select` with `native={false}`. See MERGE-NOTES-select-deprecation.md. */
14
7
  const SelectV2 = forwardRef(function SelectV2(props, ref) {
15
8
  return _jsx(Select, { ...props, native: false, ref: ref });
@@ -77,7 +77,7 @@ export default function Table({ data, columns, onSelection, isLoading, isCheckbo
77
77
  }
78
78
  return {};
79
79
  }, [tablePreferencesData]);
80
- const { tableData, isContent, isLink, headerRefs, columnWidths, handleSort, handleCheckbox, filteredData, startRow, selection, columnsSort, currentPage, pages, rowsPerPage, handlePageSizeChange, setCurrentPage, columnsList, handleColumnPreferences, isSelecting, selectAllRowsRef, } = useTable({
80
+ const { tableData, isContent, isLink, headerRefs, columnWidths, slotRef, slotWidth, handleSort, handleCheckbox, filteredData, searchedData, startRow, selection, columnsSort, currentPage, pages, rowsPerPage, handlePageSizeChange, setCurrentPage, columnsList, handleColumnPreferences, isSelecting, selectAllRowsRef, } = useTable({
81
81
  data,
82
82
  isPagination,
83
83
  selections,
@@ -90,6 +90,7 @@ export default function Table({ data, columns, onSelection, isLoading, isCheckbo
90
90
  isServerPagination,
91
91
  onNoOfRowsPerPageChange,
92
92
  defaultVisibleColumns,
93
+ columnsSearch,
93
94
  });
94
95
  // Density + grouping are managed here so the Table Settings tabs can change +
95
96
  // persist them (seeded from the prop / saved preferences when they load).
@@ -172,9 +173,7 @@ export default function Table({ data, columns, onSelection, isLoading, isCheckbo
172
173
  setGroupByState(group);
173
174
  handleColumnPreferences(cols, { density: densityState, groupBy: group !== null && group !== void 0 ? group : null });
174
175
  };
175
- const _filteredData = useMemo(() => {
176
- return searchAndSortData(filteredData, columnsSearch);
177
- }, [columnsSearch, filteredData]);
176
+ const _filteredData = filteredData;
178
177
  // --- Monday-style grouping. Active whenever the user has picked a group column
179
178
  // (in Settings > Group). When active, the numbered pager / infinite scroll are
180
179
  // hidden and the body renders colored, collapsible groups over the loaded rows.
@@ -279,7 +278,7 @@ export default function Table({ data, columns, onSelection, isLoading, isCheckbo
279
278
  : "Showing all records";
280
279
  const tablePaginationText = isServerPagination
281
280
  ? `${startRow + 1} - ${Math.min(startRow + rowsPerPage, totalRecords)} of ${totalRecords}`
282
- : `${startRow + 1} - ${Math.min(startRow + rowsPerPage, tableData.length)} of ${tableData.length}`;
281
+ : `${startRow + 1} - ${Math.min(startRow + rowsPerPage, searchedData.length)} of ${searchedData.length}`;
283
282
  const rowHeight = rowHeightFor(densityState);
284
283
  const headerHeight = pxOf(variantTokens.header.height);
285
284
  const actualRows = isGrouped ? groupedSource.length : _filteredData.length;
@@ -400,7 +399,7 @@ export default function Table({ data, columns, onSelection, isLoading, isCheckbo
400
399
  headerActions && (_jsx(HeaderActions, { actions: headerActions, selections: selection }, "actions")),
401
400
  ]
402
401
  .filter(Boolean)
403
- .flatMap((tool, i) => (i === 0 ? [tool] : [_jsx(VeritcalDivider, {}, `divider-${i}`), tool])), _jsx(ActiveFilters, { columns: columnsList, columnsSearch: columnsSearch, setColumnsSearch: setColumnsSearch }), _jsxs(Box, { ml: "auto", display: "flex", alignItems: "center", gap: 2, children: [groupLoadMoreActive && loadMorePosition === "top" && (groupLoadMoreCaption || canGroupLoadMore) && (_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, flex: "0 0 auto", children: [groupLoadMoreCaption && (_jsx(Box, { fontSize: "0.75rem", color: theme.colors.text[500], whiteSpace: "nowrap", children: groupLoadMoreCaption })), canGroupLoadMore && (_jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", isLoading: isLoadingMore, loadingText: loadMoreText, onClick: handleGroupLoadMore, label: loadMoreText }))] })), (isPagination || isServerPagination) && !isGrouped && !canInfinite && !isCompactHeader && (_jsx("div", { children: _jsx(Pagination, { currentPage: currentPage, setCurrentPage: setCurrentPage, rowsPerPage: rowsPerPage, pages: pages, paginationText: tablePaginationText, handlePageSizeChange: handlePageSizeChange, dataLength: tableData.length, isServerPagination: isServerPagination, paginationSelectOptions: paginationSelectOptions }) })), (isPagination || isServerPagination) && !isGrouped && !canInfinite && isCompactHeader && tableData.length > 0 && (_jsxs(Popover, { isOpen: isPaginationPopoverOpen, onClose: () => setIsPaginationPopoverOpen(false), placement: "bottom-end", children: [_jsx(PopoverTrigger, { children: _jsx(IconButton, { "aria-label": "Pagination", size: "sm", variant: "ghost", icon: _jsx(EllipsisVertical, { size: 18 }), color: theme.colors.text[500], _hover: {
402
+ .flatMap((tool, i) => (i === 0 ? [tool] : [_jsx(VeritcalDivider, {}, `divider-${i}`), tool])), _jsx(ActiveFilters, { columns: columnsList, columnsSearch: columnsSearch, setColumnsSearch: setColumnsSearch }), _jsxs(Box, { ml: "auto", display: "flex", alignItems: "center", gap: 2, children: [groupLoadMoreActive && loadMorePosition === "top" && (groupLoadMoreCaption || canGroupLoadMore) && (_jsxs(Box, { display: "flex", alignItems: "center", gap: 2, flex: "0 0 auto", children: [groupLoadMoreCaption && (_jsx(Box, { fontSize: "0.75rem", color: theme.colors.text[500], whiteSpace: "nowrap", children: groupLoadMoreCaption })), canGroupLoadMore && (_jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", isLoading: isLoadingMore, loadingText: loadMoreText, onClick: handleGroupLoadMore, label: loadMoreText }))] })), (isPagination || isServerPagination) && !isGrouped && !canInfinite && !isCompactHeader && (_jsx("div", { children: _jsx(Pagination, { currentPage: currentPage, setCurrentPage: setCurrentPage, rowsPerPage: rowsPerPage, pages: pages, paginationText: tablePaginationText, handlePageSizeChange: handlePageSizeChange, dataLength: tableData.length, isServerPagination: isServerPagination, paginationSelectOptions: paginationSelectOptions }) })), (isPagination || isServerPagination) && !isGrouped && !canInfinite && isCompactHeader && (_jsxs(Popover, { isOpen: isPaginationPopoverOpen, onClose: () => setIsPaginationPopoverOpen(false), placement: "bottom-end", children: [_jsx(PopoverTrigger, { children: _jsx(IconButton, { "aria-label": "Pagination", size: "sm", variant: "ghost", icon: _jsx(EllipsisVertical, { size: 18 }), color: theme.colors.text[500], _hover: {
404
403
  color: theme.colors.accentText,
405
404
  }, onClick: () => setIsPaginationPopoverOpen((open) => !open) }) }), _jsx(Portal, { children: _jsx(PopoverContent, { maxW: "22rem", p: 2, zIndex: 1600, bg: theme.colors.surfaceRaised, boxShadow: "lg", children: _jsx(PopoverBody, { p: 0, children: _jsx(Pagination, { currentPage: currentPage, setCurrentPage: setCurrentPage, rowsPerPage: rowsPerPage, pages: pages, paginationText: tablePaginationText, handlePageSizeChange: handlePageSizeChange, dataLength: tableData.length, isServerPagination: isServerPagination, paginationSelectOptions: paginationSelectOptions }) }) }) })] }))] })] }), _jsxs(TableContainer, { ref: tableContainerRef, position: "relative", h: isTableLoading ? tableMaxH : undefined, maxH: !isTableLoading ? tableMaxH : undefined, pb: isTableLoading ? 0 : compactScrollbarPadding, overflowY: isCompactRows ? "hidden" : "auto", overflowX: "auto", sx: {
406
405
  ...scrollShadowStyles(theme.colors.background[50]),
@@ -430,5 +429,5 @@ export default function Table({ data, columns, onSelection, isLoading, isCheckbo
430
429
  backgroundColor: "transparent",
431
430
  zIndex: 999,
432
431
  },
433
- }, children: [_jsx(Thead, { position: "sticky", top: 0, zIndex: 4, bg: variantTokens.header.bg, children: _jsx(TableHeader, { columns: columnsList, isCheckbox: isCheckbox, freezedTextColor: freezedTextColor !== null && freezedTextColor !== void 0 ? freezedTextColor : theme.colors.text[500], handleSort: handleSort, headerRefs: headerRefs, columnWidths: columnWidths, columnsSort: columnsSort, noBorders: noBorders, handleCheckbox: handleCheckbox, isLoading: isTableLoading, checked: headerChecked, isContent: isContent, isLink: isLink, isActionFreeze: isActionFreeze, setColumnsSearch: setColumnsSearch, columnsSearch: columnsSearch, isSelecting: isSelecting }) }), _jsx(Tbody, { children: _jsx(TableBody, { data: isGrouped ? groupedSource : _filteredData, groups: renderGroups, onAddItem: onAddItem, columns: columnsList, startRow: isGrouped ? 0 : startRow, scrollContainerRef: tableContainerRef, isCheckbox: isCheckbox, columnWidths: columnWidths, noBorders: noBorders, freezedBgColor: freezedBgColor !== null && freezedBgColor !== void 0 ? freezedBgColor : theme.colors.backgroundColor.secondary, freezedTextColor: freezedTextColor !== null && freezedTextColor !== void 0 ? freezedTextColor : theme.colors.text[500], handleCheckbox: handleCheckbox, selections: selection, isLoading: isTableLoading, loadingSkeletonRows: loadingSkeletonRows, onRowClick: onRowClick, isContent: isContent, isLink: isLink, isActionFreeze: isActionFreeze, density: densityState, stripe: stripe, emptyState: emptyState }) })] }), canInfinite && isLoadingMore && (_jsxs(Flex, { justify: "center", align: "center", py: 3, gap: 2, children: [_jsx(Spinner, { size: "sm", color: theme.colors.accentText }), _jsx(Box, { fontSize: "0.75rem", color: theme.colors.text[500], children: "Loading more\u2026" })] }))] }), groupLoadMoreActive && loadMorePosition === "bottom" && (groupLoadMoreCaption || canGroupLoadMore) && (_jsxs(Flex, { justify: "center", align: "center", gap: 3, py: 3, borderTop: `0.063rem solid ${theme.colors.boxborder[200]}`, children: [groupLoadMoreCaption && (_jsx(Box, { fontSize: "0.75rem", color: theme.colors.text[500], children: groupLoadMoreCaption })), canGroupLoadMore && (_jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", isLoading: isLoadingMore, loadingText: loadMoreText, onClick: handleGroupLoadMore, label: loadMoreText }))] }))] })] })] })] }));
432
+ }, children: [_jsx(Thead, { position: "sticky", top: 0, zIndex: 4, bg: variantTokens.header.bg, children: _jsx(TableHeader, { columns: columnsList, isCheckbox: isCheckbox, freezedTextColor: freezedTextColor !== null && freezedTextColor !== void 0 ? freezedTextColor : theme.colors.text[500], handleSort: handleSort, headerRefs: headerRefs, columnWidths: columnWidths, slotRef: slotRef, slotWidth: slotWidth, columnsSort: columnsSort, noBorders: noBorders, handleCheckbox: handleCheckbox, isLoading: isTableLoading, checked: headerChecked, isContent: isContent, isLink: isLink, isActionFreeze: isActionFreeze, setColumnsSearch: setColumnsSearch, columnsSearch: columnsSearch, isSelecting: isSelecting }) }), _jsx(Tbody, { children: _jsx(TableBody, { data: isGrouped ? groupedSource : _filteredData, groups: renderGroups, onAddItem: onAddItem, columns: columnsList, startRow: isGrouped ? 0 : startRow, rowsPerPage: rowsPerPage, scrollContainerRef: tableContainerRef, isCheckbox: isCheckbox, columnWidths: columnWidths, slotWidth: slotWidth, noBorders: noBorders, freezedBgColor: freezedBgColor !== null && freezedBgColor !== void 0 ? freezedBgColor : theme.colors.backgroundColor.secondary, freezedTextColor: freezedTextColor !== null && freezedTextColor !== void 0 ? freezedTextColor : theme.colors.text[500], handleCheckbox: handleCheckbox, selections: selection, isLoading: isTableLoading, loadingSkeletonRows: loadingSkeletonRows, onRowClick: onRowClick, isContent: isContent, isLink: isLink, isActionFreeze: isActionFreeze, density: densityState, stripe: stripe, emptyState: emptyState }) })] }), canInfinite && isLoadingMore && (_jsxs(Flex, { justify: "center", align: "center", py: 3, gap: 2, children: [_jsx(Spinner, { size: "sm", color: theme.colors.accentText }), _jsx(Box, { fontSize: "0.75rem", color: theme.colors.text[500], children: "Loading more\u2026" })] }))] }), groupLoadMoreActive && loadMorePosition === "bottom" && (groupLoadMoreCaption || canGroupLoadMore) && (_jsxs(Flex, { justify: "center", align: "center", gap: 3, py: 3, borderTop: `0.063rem solid ${theme.colors.boxborder[200]}`, children: [groupLoadMoreCaption && (_jsx(Box, { fontSize: "0.75rem", color: theme.colors.text[500], children: groupLoadMoreCaption })), canGroupLoadMore && (_jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", isLoading: isLoadingMore, loadingText: loadMoreText, onClick: handleGroupLoadMore, label: loadMoreText }))] }))] })] })] })] }));
434
433
  }
@@ -7,6 +7,8 @@ import { DEFAULT_PAGE_SIZE } from "../hooks/useTable.js";
7
7
  import { useCustomTheme } from "../../../Theme/useCustomTheme.js";
8
8
  import ToolTip from "../../ToolTip/ToolTip.js";
9
9
  import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, } from "lucide-react";
10
+ const STEP_SIZE = "1.5rem";
11
+ const STEP_ICON = 16;
10
12
  const Pagination = React.memo(({ paginationText, handlePageSizeChange, rowsPerPage, currentPage, pages, setCurrentPage, dataLength, isServerPagination, paginationSelectOptions, }) => {
11
13
  const computedOptions = useMemo(() => {
12
14
  if (paginationSelectOptions && paginationSelectOptions.length) {
@@ -18,10 +20,11 @@ const Pagination = React.memo(({ paginationText, handlePageSizeChange, rowsPerPa
18
20
  return [...sizes].sort((a, b) => a - b);
19
21
  }, [paginationSelectOptions, dataLength, rowsPerPage]);
20
22
  const theme = useCustomTheme();
21
- // const isLeftDisabled = currentPage === 0;
22
- // const isRightDisabled = currentPage >= pages - 1;
23
- const isLeftDisabled = dataLength === 0 || currentPage === 0;
24
- const isRightDisabled = dataLength === 0 || currentPage >= pages - 1;
25
- return (_jsxs(Flex, { justify: "flex-end", align: "center", flexWrap: "nowrap", children: [(!isServerPagination || paginationSelectOptions) && (_jsx(Select, { native: false, isSearchable: false, "aria-label": "Rows per page", onChange: (option) => option && handlePageSizeChange(Number(option.id)), value: rowsPerPage, size: "xs", width: "4.5rem", isDisabled: dataLength === 0, options: computedOptions.map((size) => ({ id: size, label: String(size) })) })), dataLength > 0 && (_jsxs(Flex, { w: "100%", children: [_jsx(ToolTip, { label: "First", placement: "top", children: _jsx(IconButton, { "aria-label": "first-page", color: isLeftDisabled ? theme.colors.textMuted : theme.colors.text[500], rounded: "full", variant: isLeftDisabled ? "plain" : "ghost", onClick: () => setCurrentPage(0), _hover: !isLeftDisabled ? { color: theme.colors.accentText } : {}, children: _jsx(ChevronsLeft, {}) }) }), _jsx(ToolTip, { label: "Previous", placement: "top", children: _jsx(IconButton, { "aria-label": "previous-page", color: isLeftDisabled ? theme.colors.textMuted : theme.colors.text[500], rounded: "full", variant: isLeftDisabled ? "plain" : "ghost", onClick: () => currentPage > 0 && setCurrentPage(currentPage - 1), _hover: !isLeftDisabled ? { color: theme.colors.accentText } : {}, children: _jsx(ChevronLeft, {}) }) }), _jsx(Text, { mt: 3, fontSize: 12, whiteSpace: "nowrap", color: theme.colors.text[500], children: paginationText }), _jsx(ToolTip, { label: "Next", placement: "top", children: _jsx(IconButton, { "aria-label": "next-page", color: isRightDisabled ? theme.colors.textMuted : theme.colors.text[500], rounded: "full", variant: isRightDisabled ? "plain" : "ghost", onClick: () => currentPage < pages - 1 && setCurrentPage(currentPage + 1), _hover: !isRightDisabled ? { color: theme.colors.accentText } : {}, children: _jsx(ChevronRight, {}) }) }), _jsx(ToolTip, { label: "Last", placement: "top", children: _jsx(IconButton, { "aria-label": "last-page", color: isRightDisabled ? theme.colors.textMuted : theme.colors.text[500], rounded: "full", variant: isRightDisabled ? "plain" : "ghost", onClick: () => setCurrentPage(pages - 1), _hover: !isRightDisabled ? { color: theme.colors.accentText } : {}, children: _jsx(ChevronsRight, {}) }) })] }))] }));
23
+ const lastPage = Math.max(0, pages - 1);
24
+ // Stepping back is how a user escapes an empty page, so it survives an empty result.
25
+ const isLeftDisabled = currentPage === 0;
26
+ const isRightDisabled = dataLength === 0 || currentPage >= lastPage;
27
+ const step = (label, ariaLabel, icon, isDisabled, onClick) => (_jsx(ToolTip, { label: label, placement: "top", children: _jsx(IconButton, { "aria-label": ariaLabel, icon: icon, variant: "ghost", size: "xs", minW: STEP_SIZE, h: STEP_SIZE, rounded: "full", color: isDisabled ? theme.colors.textMuted : theme.colors.text[500], isDisabled: isDisabled, onClick: onClick, _hover: isDisabled ? {} : { color: theme.colors.accentText } }) }));
28
+ return (_jsxs(Flex, { justify: "flex-end", align: "center", flexWrap: "nowrap", gap: "0.75rem", children: [(!isServerPagination || paginationSelectOptions) && (_jsx(Select, { native: false, isSearchable: false, "aria-label": "Rows per page", onChange: (option) => option && handlePageSizeChange(Number(option.id)), value: rowsPerPage, size: "xs", width: "3.75rem", isDisabled: dataLength === 0, options: computedOptions.map((size) => ({ id: size, label: String(size) })) })), (dataLength > 0 || currentPage > 0) && (_jsxs(Flex, { align: "center", gap: "0.125rem", children: [step("First", "first-page", _jsx(ChevronsLeft, { size: STEP_ICON }), isLeftDisabled, () => setCurrentPage(0)), step("Previous", "previous-page", _jsx(ChevronLeft, { size: STEP_ICON }), isLeftDisabled, () => setCurrentPage((page) => Math.max(0, page - 1))), _jsx(Text, { px: "0.375rem", fontSize: "0.75rem", lineHeight: STEP_SIZE, whiteSpace: "nowrap", color: theme.colors.text[500], sx: { fontVariantNumeric: "tabular-nums" }, children: paginationText }), step("Next", "next-page", _jsx(ChevronRight, { size: STEP_ICON }), isRightDisabled, () => setCurrentPage((page) => Math.min(lastPage, page + 1))), step("Last", "last-page", _jsx(ChevronsRight, { size: STEP_ICON }), isRightDisabled, () => setCurrentPage(lastPage))] }))] }));
26
29
  });
27
30
  export default Pagination;