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
package/CHANGELOG.md CHANGED
@@ -63,6 +63,59 @@ next major.
63
63
 
64
64
  ### Fixed
65
65
 
66
+ - **`Table` — a column search only ever matched the page you were looking at.** The per-column
67
+ search ran on `filteredData`, which `useTable` had already sliced to the current page, so a match
68
+ on page 3 was invisible from page 1 and the pager still counted every row. The search now runs
69
+ before the slice: page count, the `1 - 20 of N` readout and the rendered rows all come from the
70
+ matched set, and typing a new search returns to page 1. Grouped mode already searched the whole
71
+ source and is unchanged; server pagination still searches only the loaded page, since the client
72
+ cannot see the rest.
73
+ - **`Table` — an empty page was a dead end, and a page-size change left the body scrolled.** The
74
+ pager's step controls were hidden whenever the loaded rows were empty, so a server page that came
75
+ back with nothing stranded the user with no way back; stepping back now survives an empty result
76
+ and only stepping forward is disabled. A narrowing client-side result also clamps the current page
77
+ instead of leaving it past the end. Changing the page size at page 0 swaps the rows without moving
78
+ `startRow`, which used to leave the virtualized body scrolled into the old page — the scroll reset
79
+ now keys on the page size too.
80
+ - **`Table` — a full-bleed status cell ignored `columns[].align`.** `align` is documented as aligning
81
+ the header label and the cells together, but a bleed status cell hardcoded centre. It now follows
82
+ `align` when the column sets one, and still centres when it does not, so existing tables look the
83
+ same.
84
+ - **`Select` — the popup's filter field did not line up with the options under it.** Its wrapper
85
+ used a hardcoded `p={2}` while option rows inset by the size spec's `mx` (6px at `sm`, 4px at
86
+ `xs`), so the field's edges sat 2-4px inside the list's, and its text origin 2px further in. The
87
+ field was also fixed at `size="sm"` whatever the `Select`'s own size, so a `lg` picker got a small
88
+ search box. Both now follow the active size spec; measured edge- and text-aligned at every size.
89
+ - **`Select` — a multi-select with chips rendered 2px taller than every other field.** The chip row's
90
+ vertical padding was computed against the control height without allowing for the trigger's own
91
+ 1px rule top and bottom, which a border-box trigger adds on top. The sizing now lives in one
92
+ shared helper (`Common/chipTriggerSize`) that `Select` and the deprecated `SelectV2` both read.
93
+ - **`SearchSelect` — infinite-scroll paging could duplicate a page, loop forever on an
94
+ exact-multiple total, leak a pending fetch into a closed/unmounted menu, and never scrolled the
95
+ keyboard-highlighted row into view.** `handleScroll` now reads the live option count at fire
96
+ time instead of the value captured when its 200ms debounce was scheduled, and won't re-request a
97
+ `startIndex` it already asked for. `hasMore` takes new optional `pagination.hasMore` /
98
+ `pagination.totalRecords` overrides, defaults `limit` to 10 (it used to never update when `limit`
99
+ was omitted), and stops after one page that doesn't grow `options`. The pending page timer is
100
+ cleared on close/unmount, and the active row now scrolls into view with `block: "nearest"`.
101
+ - **`Table` — frozen columns sat at the wrong offset, and the pagination bar was misaligned.** The
102
+ sticky `left` was passed as a bare number, which Chakra reads off the space scale: a 48px offset
103
+ rendered as `12rem` (192px), and any cumulative frozen-column width landing on the scale jumped
104
+ 4x. It stayed hidden because the old `CHECKBOX_SLOT_OFFSET = 50` is not a scale value — but 50
105
+ also missed the real 48px select-all cell by 2px, leaving a slit that scrolled content bled
106
+ through, and was wrong outright for `variant="studio"`'s 56px slot. The slot is now measured with
107
+ a `ResizeObserver`, and both offsets interpolate explicit pixels. The pager is one 24px row: the
108
+ `mt={3}` that faked centring is gone, the steps are `size="xs"` with 16px icons instead of
109
+ Chakra's 40px default, and the page-size select no longer fights `justify="flex-end"` with
110
+ `w="100%"`.
111
+ - **`Table` — virtualized body jumped to the top on an appended page, and never scrolled on a real
112
+ page change below the virtualize threshold.** The scroll-reset effect now keys on `startRow`
113
+ actually changing instead of on the 30-row virtualize threshold flipping, so infinite-scroll and
114
+ grouped `groupLoadMore` appends no longer yank the user to the top, while a real page change
115
+ resets correctly at any size. A shrinking `data` set no longer strands the window past the end,
116
+ the offset-lookup binary-searches instead of scanning from 0, and measured row heights are
117
+ pruned per page instead of growing forever.
118
+
66
119
  - **`SegmentedControl` / `Switch` guessed the backdrop it was drawn on.** `brandSlide` painted an
67
120
  8% brand tint composited over `backgroundColor.main` and lifted its unselected-label ink against
68
121
  that same assumed canvas, so on a brand-filled bar the tint sank into the bar and the label
@@ -434,6 +434,37 @@ set the required state on your own wrapping `FormControl`. Note the obvious one-
434
434
  component is not neutral: Chakra's `useFormControl` also emits native `required`, which would add
435
435
  browser validation to a *filter* input that was never submitted before.
436
436
 
437
+ ### `Select` with `onSearchChange` no longer filters locally (beta.48)
438
+
439
+ Passing `onSearchChange` means the server owns matching, so `Select` stops applying its own
440
+ substring filter to `options`. Before this, a fuzzy or ranked server result whose label did not
441
+ literally contain the typed text was fetched and then hidden. Pass `filterOptions={true}` to keep
442
+ the local pass on top, or `filterOptions={false}` to drop it even without `onSearchChange`.
443
+
444
+ **Apply:** if a server-backed `Select` suddenly shows more rows than it used to, that is this fix,
445
+ not a regression. Paged `Select`s should also pass `hasMore={false}` once the last page lands —
446
+ without it, `onLoadMore` fires again every time the user reaches the bottom.
447
+
448
+ ### `Select` with `isMulti` keeps picks that leave `options` (beta.48)
449
+
450
+ `onChange` used to be built from the options currently in the list, so replacing `options` with a
451
+ server-search result silently dropped every earlier pick the new page did not contain — the chips
452
+ vanished and the next selection emitted a truncated array. Selected options are now remembered
453
+ independently of `options`.
454
+
455
+ **Apply:** nothing to change. If you worked around this by merging the old selection back in
456
+ yourself, that workaround is now redundant.
457
+
458
+ ### `Table`'s column search covers every page (beta.48)
459
+
460
+ The per-column search box used to run after the page slice, so it matched only the rows on screen
461
+ while the pager still counted the whole data set. It now runs first: the page count, the
462
+ `1 - 20 of N` readout and the rows all come from the matched set, and a new search returns to page
463
+ 1. Under `paginationMode="server"` the client still only sees the loaded page — filter on the
464
+ server there.
465
+
466
+ **Apply:** a client-side table that looked like it had "no matches" past page 1 now finds them.
467
+
437
468
  ### `Select`'s `onChange` never fires for the built-in placeholder
438
469
 
439
470
  `Select` resolves the changed value against its `options` list and calls `onChange` only on a hit.
@@ -1219,14 +1250,16 @@ configured has no visible effect at all.
1219
1250
  **Apply:** to change the main field's height, change `size`; `height` only matters when
1220
1251
  `insideSelect` is also passed.
1221
1252
 
1222
- ### `SearchSelect`'s `pagination.limit` also drives a "has more" guess, not just page size
1253
+ ### `SearchSelect`'s `pagination` now takes an explicit "has more" signal — prefer it at an exact-multiple total
1223
1254
 
1224
- `hasMore` is computed as `options.length % pagination.limit === 0` — there is no real end-of-data
1225
- signal from the consumer. A total result count that happens to be an exact multiple of `limit`
1226
- looks indistinguishable from "more pages exist," firing one extra, empty `onScroll` call.
1255
+ Without `pagination.hasMore`/`pagination.totalRecords`, `hasMore` still falls back to guessing from
1256
+ `options.length % (pagination.limit ?? 10) === 0`. A total that's an exact multiple of `limit`
1257
+ (30 items, limit 10) still looks like "more pages exist" for the *first* extra scroll, firing one
1258
+ empty `onScroll` call — after that empty response, `SearchSelect` stops asking (it no longer loops
1259
+ forever the way earlier versions did).
1227
1260
 
1228
- **Apply:** if exact pagination boundaries matter, pad or report a real "no more results" state some
1229
- other way; don't rely on `hasMore` being accurate at a page boundary.
1261
+ **Apply:** pass `pagination.hasMore` (a boolean) or `pagination.totalRecords` (a count) so the field
1262
+ never fires that one avoidable empty request; both take precedence over the `limit` guess.
1230
1263
 
1231
1264
  ### `RolesPermission`'s `RoleData.permission`/`designation`/`makeSuperAdmin`/`automation`/`trackLocation`/`show_records` are dead weight in this component
1232
1265
 
@@ -0,0 +1,10 @@
1
+ import { type ControlSize } from "../../Theme/tokens/primitives/controlSizes";
2
+ export type ChipSize = "xs" | "sm";
3
+ /** Sizes a select trigger so a chip row lands on the same height as a plain one. */
4
+ export declare const chipTriggerSizeStyles: (size: ControlSize, hasChips: boolean, chipSize?: ChipSize) => {
5
+ minH: string;
6
+ fontSize: string;
7
+ px: string;
8
+ h?: string;
9
+ py?: string;
10
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.chipTriggerSizeStyles = void 0;
4
+ const controlSizes_1 = require("../../Theme/tokens/primitives/controlSizes");
5
+ const CHIP_TAG_HEIGHT_PX = { xs: 20, sm: 24 };
6
+ const CONTROL_BORDER_PX = 2;
7
+ const controlHeightPx = (size) => parseFloat(controlSizes_1.CONTROL_SIZES[size].height) * 16;
8
+ const chipRowPaddingY = (size, chipSize) => `${(controlHeightPx(size) - CHIP_TAG_HEIGHT_PX[chipSize] - CONTROL_BORDER_PX) / 2 / 16}rem`;
9
+ /** Sizes a select trigger so a chip row lands on the same height as a plain one. */
10
+ const chipTriggerSizeStyles = (size, hasChips, chipSize = "sm") => {
11
+ const spec = controlSizes_1.CONTROL_SIZES[size];
12
+ const base = { minH: spec.height, fontSize: spec.fontSize, px: spec.paddingX };
13
+ return hasChips ? { ...base, py: chipRowPaddingY(size, chipSize) } : { ...base, h: spec.height };
14
+ };
15
+ exports.chipTriggerSizeStyles = chipTriggerSizeStyles;
@@ -91,6 +91,9 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
91
91
  const dropdownMenuRef = (0, react_1.useRef)(null);
92
92
  const [dropdownPos, setDropdownPos] = (0, react_1.useState)({ top: 0, left: 0, width: 0 });
93
93
  const scrollTimeoutRef = (0, react_1.useRef)(null);
94
+ const optionsLengthRef = (0, react_1.useRef)(options.length);
95
+ const lastPageStartRef = (0, react_1.useRef)(null);
96
+ const optionsListRef = (0, react_1.useRef)(null);
94
97
  const [focused, setFocused] = (0, react_1.useState)(false);
95
98
  const [customSelectOpen, setCustomSelectOpen] = (0, react_1.useState)(false);
96
99
  const customSelectRef = (0, react_1.useRef)(null);
@@ -141,10 +144,22 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
141
144
  return () => clearTimeout(delayDebounce);
142
145
  }, [inputValue]);
143
146
  (0, react_1.useEffect)(() => {
144
- if (pagination === null || pagination === void 0 ? void 0 : pagination.limit) {
145
- setHasMore(options.length % pagination.limit === 0);
147
+ optionsLengthRef.current = options.length;
148
+ }, [options.length]);
149
+ (0, react_1.useEffect)(() => {
150
+ if ((pagination === null || pagination === void 0 ? void 0 : pagination.hasMore) !== undefined) {
151
+ setHasMore(pagination.hasMore);
152
+ return;
153
+ }
154
+ if ((pagination === null || pagination === void 0 ? void 0 : pagination.totalRecords) !== undefined) {
155
+ setHasMore(options.length < pagination.totalRecords);
156
+ return;
146
157
  }
147
- }, [options.length, pagination === null || pagination === void 0 ? void 0 : pagination.limit]);
158
+ if (!(pagination === null || pagination === void 0 ? void 0 : pagination.onScroll))
159
+ return;
160
+ const limit = pagination.limit || 10;
161
+ setHasMore(options.length % limit === 0);
162
+ }, [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]);
148
163
  const handleSelect = (option) => {
149
164
  var _a;
150
165
  if (isMultiple && insideSelect) {
@@ -324,6 +339,13 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
324
339
  }
325
340
  setActiveIndex((prev) => (prev >= 0 && prev < filteredOptions.length ? prev : -1));
326
341
  }, [isOpen, filteredOptions.length]);
342
+ (0, react_1.useEffect)(() => {
343
+ var _a, _b;
344
+ if (!isOpen || activeIndex < 0 || !optionsListRef.current)
345
+ return;
346
+ (_b = (_a = optionsListRef.current
347
+ .querySelector(`[data-index="${activeIndex}"]`)) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, { block: "nearest" });
348
+ }, [isOpen, activeIndex]);
327
349
  // NOTE: only restores focus when a keyboard close actually unmounted Input and dropped focus to body.
328
350
  (0, react_1.useEffect)(() => {
329
351
  var _a, _b, _c, _d;
@@ -354,11 +376,30 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
354
376
  clearTimeout(scrollTimeoutRef.current);
355
377
  scrollTimeoutRef.current = setTimeout(() => {
356
378
  var _a;
357
- const startIndex = options.length + 1;
379
+ scrollTimeoutRef.current = null;
380
+ const startIndex = optionsLengthRef.current + 1;
381
+ if (lastPageStartRef.current === startIndex) {
382
+ setHasMore(false);
383
+ return;
384
+ }
385
+ lastPageStartRef.current = startIndex;
358
386
  (_a = pagination.onScroll) === null || _a === void 0 ? void 0 : _a.call(pagination, startIndex, pagination.limit || 10, inputValue);
359
387
  }, 200);
360
388
  }
361
- }, [pagination, options.length, inputValue, hasMore]);
389
+ }, [pagination, inputValue, hasMore]);
390
+ (0, react_1.useEffect)(() => {
391
+ if (isOpen)
392
+ return;
393
+ if (scrollTimeoutRef.current) {
394
+ clearTimeout(scrollTimeoutRef.current);
395
+ scrollTimeoutRef.current = null;
396
+ }
397
+ lastPageStartRef.current = null;
398
+ }, [isOpen]);
399
+ (0, react_1.useEffect)(() => () => {
400
+ if (scrollTimeoutRef.current)
401
+ clearTimeout(scrollTimeoutRef.current);
402
+ }, []);
362
403
  (0, react_2.useOutsideClick)({
363
404
  ref: customSelectRef,
364
405
  handler: (e) => {
@@ -536,10 +577,10 @@ const SearchSelect = ({ id, label, options, onChange, onSelect, onSearch, isOpti
536
577
  else {
537
578
  openMenu();
538
579
  }
539
- }, "aria-label": "toggle-dropdown", display: "flex", alignItems: "center", borderRadius: "sm", color: theme.colors.textMuted, cursor: isDisabled ? "not-allowed" : "pointer", sx: (0, focusRing_styles_1.focusVisibleStyles)(theme.colors), children: isOpen ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronUp, { size: s.chevronSize })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: s.chevronSize })) }))] })] }) }), isOpen && ((0, jsx_runtime_1.jsx)(react_2.Portal, { children: (0, jsx_runtime_1.jsxs)(react_2.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 && ((0, jsx_runtime_1.jsxs)(react_2.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: [(0, jsx_runtime_1.jsx)(Checkbox_1.default, { isChecked: allFilteredSelected, sx: { pointerEvents: "none" }, size: s.checkboxSize }), (0, jsx_runtime_1.jsx)(react_2.Text, { fontSize: spec.fontSize, children: allFilteredSelected ? "Unselect All" : "Select All" })] })), (0, jsx_runtime_1.jsx)(react_2.Box, { flex: "1", overflowY: "auto", onScroll: handleScroll, maxH: "15rem", borderBottom: "0.063rem solid", borderColor: theme.colors.boxborder[200], children: isOptionLoading && filteredOptions.length === 0 ? ((0, jsx_runtime_1.jsxs)(react_2.Box, { py: spec.py, px: spec.px, display: "flex", alignItems: "center", gap: spec.gap, minH: spec.minH, color: theme.colors.textMuted, children: [(0, jsx_runtime_1.jsx)(react_2.Spinner, { size: s.spinnerSize }), (0, jsx_runtime_1.jsx)(react_2.Text, { fontSize: spec.fontSize, children: loadingText })] })) : !isOptionLoading && filteredOptions.length === 0 ? ((0, jsx_runtime_1.jsx)(react_2.Text, { p: 3, fontSize: spec.fontSize, color: theme.colors.textMuted, children: "No results found" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [filteredOptions.map((option, index) => {
580
+ }, "aria-label": "toggle-dropdown", display: "flex", alignItems: "center", borderRadius: "sm", color: theme.colors.textMuted, cursor: isDisabled ? "not-allowed" : "pointer", sx: (0, focusRing_styles_1.focusVisibleStyles)(theme.colors), children: isOpen ? ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronUp, { size: s.chevronSize })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: s.chevronSize })) }))] })] }) }), isOpen && ((0, jsx_runtime_1.jsx)(react_2.Portal, { children: (0, jsx_runtime_1.jsxs)(react_2.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 && ((0, jsx_runtime_1.jsxs)(react_2.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: [(0, jsx_runtime_1.jsx)(Checkbox_1.default, { isChecked: allFilteredSelected, sx: { pointerEvents: "none" }, size: s.checkboxSize }), (0, jsx_runtime_1.jsx)(react_2.Text, { fontSize: spec.fontSize, children: allFilteredSelected ? "Unselect All" : "Select All" })] })), (0, jsx_runtime_1.jsx)(react_2.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 ? ((0, jsx_runtime_1.jsxs)(react_2.Box, { py: spec.py, px: spec.px, display: "flex", alignItems: "center", gap: spec.gap, minH: spec.minH, color: theme.colors.textMuted, children: [(0, jsx_runtime_1.jsx)(react_2.Spinner, { size: s.spinnerSize }), (0, jsx_runtime_1.jsx)(react_2.Text, { fontSize: spec.fontSize, children: loadingText })] })) : !isOptionLoading && filteredOptions.length === 0 ? ((0, jsx_runtime_1.jsx)(react_2.Text, { p: 3, fontSize: spec.fontSize, color: theme.colors.textMuted, children: "No results found" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [filteredOptions.map((option, index) => {
540
581
  const selected = isSelected(option.id);
541
582
  const singleSelected = selected && !isMultiple;
542
- return ((0, jsx_runtime_1.jsxs)(react_2.Box, { id: getOptionId(index), role: "option", "aria-selected": selected, py: spec.py, px: spec.px, minH: spec.minH, cursor: "pointer", bg: selected
583
+ return ((0, jsx_runtime_1.jsxs)(react_2.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
543
584
  ? index === activeIndex
544
585
  ? rowBackgrounds.selectedActive
545
586
  : rowBackgrounds.selected
@@ -81,10 +81,14 @@ export type SearchSelectProps = {
81
81
  pagination?: {
82
82
  /** startIndex is 1-based (current options.length + 1), not a 0-based offset. */
83
83
  onScroll?: (startIndex: number, limit: number, query: string) => void;
84
- /** Also used to guess whether more pages exist (options.length % limit === 0) — an exact-multiple total triggers one extra scroll fetch. */
84
+ /** Also used to guess whether more pages exist (options.length % limit === 0, refined once a page returns no new rows). Defaults to 10 when omitted. */
85
85
  limit?: number;
86
86
  /** Set this synchronously while a page request is in flight, or fast scrolling can re-fire onScroll for the same page. */
87
87
  scrollLoading?: boolean;
88
+ /** Explicit stop signal; overrides the limit-based heuristic when set. */
89
+ hasMore?: boolean;
90
+ /** Total row count; when set, hasMore is derived as options.length < totalRecords instead of the limit heuristic. */
91
+ totalRecords?: number;
88
92
  };
89
93
  /** Not implemented — the option list always renders unvirtualized; use `pagination` to page in large datasets instead. */
90
94
  virtualization?: boolean;
@@ -38,12 +38,14 @@ declare const SelectNative: React.ForwardRefExoticComponent<{
38
38
  defaultSearchValue?: string;
39
39
  onSearchChange?: (query: string) => void;
40
40
  searchDebounceMs?: number;
41
+ filterOptions?: boolean;
41
42
  isClearable?: boolean;
42
43
  onClear?: () => void;
43
44
  isLoading?: boolean;
44
45
  loadingText?: string;
45
46
  noOptionsMessage?: React.ReactNode;
46
47
  onLoadMore?: () => void;
48
+ hasMore?: boolean;
47
49
  renderOption?: (option: OptionProp, state: {
48
50
  isSelected: boolean;
49
51
  isActive: boolean;
@@ -49,6 +49,7 @@ const scrollbar_1 = require("../../Theme/tokens/builders/scrollbar");
49
49
  const controlSizes_1 = require("../../Theme/tokens/primitives/controlSizes");
50
50
  const motion_1 = require("../../Theme/tokens/primitives/motion");
51
51
  const motion_2 = require("../../Theme/chakra/motion");
52
+ const chipTriggerSize_1 = require("../Common/chipTriggerSize");
52
53
  const fieldStyles_1 = require("../Common/fieldStyles");
53
54
  const useFieldIds_1 = require("../Common/useFieldIds");
54
55
  const FieldMessage_1 = __importDefault(require("../Common/FieldMessage"));
@@ -61,14 +62,6 @@ const PAGE_STEP = 8;
61
62
  const LOAD_MORE_THRESHOLD_PX = 8;
62
63
  const DEFAULT_MAX_VISIBLE_CHIPS = 6;
63
64
  const VIRTUAL_OVERSCAN = 4;
64
- const CHIP_TAG_HEIGHT_PX = { xs: 20, sm: 24 };
65
- const controlHeightPx = (size) => parseFloat(controlSizes_1.CONTROL_SIZES[size].height) * 16;
66
- const chipRowPaddingY = (size, chipSize) => `${(controlHeightPx(size) - CHIP_TAG_HEIGHT_PX[chipSize]) / 2 / 16}rem`;
67
- const chipTriggerSizeStyles = (size, hasChips, chipSize = "sm") => {
68
- const spec = controlSizes_1.CONTROL_SIZES[size];
69
- const base = { minH: spec.height, fontSize: spec.fontSize, px: spec.paddingX };
70
- return hasChips ? { ...base, py: chipRowPaddingY(size, chipSize) } : { ...base, h: spec.height };
71
- };
72
65
  const asIdSet = (values) => new Set((values !== null && values !== void 0 ? values : []).map((v) => v.toString()));
73
66
  /** Regression-guarded id scheme — tickets e2e (`e2e/tests/helpers/selectV2.ts`) resolves it verbatim. */
74
67
  const optionDomId = (listboxId, id) => `${listboxId}-option-${id.toString().replace(/[^a-zA-Z0-9_-]/g, "-")}`;
@@ -98,7 +91,7 @@ const buildGroupedRows = (options) => {
98
91
  /** The unified `Select`'s popup mode (`native={false}`) — absorbs SelectV2's behaviour, plus clear/create/groups/virtualization. */
99
92
  const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
100
93
  var _a, _b, _c, _d;
101
- const { options = [], id, name, placeholder = "Select Option", label, error = false, errorMessage, helperText, isDisabled = false, isReadOnly = false, isRequired = false, isInformation = false, informationMessage, size = controlSizes_1.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;
94
+ const { options = [], id, name, placeholder = "Select Option", label, error = false, errorMessage, helperText, isDisabled = false, isReadOnly = false, isRequired = false, isInformation = false, informationMessage, size = controlSizes_1.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;
102
95
  const theme = (0, useCustomTheme_1.useCustomTheme)();
103
96
  const { colors } = theme;
104
97
  const { isOpen, onOpen, onClose } = (0, react_2.useDisclosure)({ defaultIsOpen: defaultOpen });
@@ -108,6 +101,8 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
108
101
  const mergedTriggerRef = (0, react_2.useMergeRefs)(triggerRef, ref);
109
102
  const filterInputRef = (0, react_1.useRef)(null);
110
103
  const listRef = (0, react_1.useRef)(null);
104
+ const lastActiveIndex = (0, react_1.useRef)(-1);
105
+ const wasVirtualized = (0, react_1.useRef)(false);
111
106
  const [triggerWidth, setTriggerWidth] = (0, react_1.useState)(0);
112
107
  const ids = (0, useFieldIds_1.useFieldIds)(id, { error, errorMessage, helperText });
113
108
  const listboxId = `${ids.inputId}-listbox`;
@@ -125,13 +120,28 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
125
120
  const selectedIds = (0, react_1.useMemo)(() => props.isMulti
126
121
  ? asIdSet(currentValue)
127
122
  : asIdSet(currentValue !== undefined ? [currentValue] : undefined), [props.isMulti, currentValue]);
128
- const selectedOptions = (0, react_1.useMemo)(() => options.filter((option) => selectedIds.has(option.id.toString())), [options, selectedIds]);
123
+ const selectionCacheRef = (0, react_1.useRef)(new Map());
124
+ const selectedOptions = (0, react_1.useMemo)(() => {
125
+ const cache = selectionCacheRef.current;
126
+ options.forEach((option) => {
127
+ if (selectedIds.has(option.id.toString()))
128
+ cache.set(option.id.toString(), option);
129
+ });
130
+ Array.from(cache.keys()).forEach((cachedId) => {
131
+ if (!selectedIds.has(cachedId))
132
+ cache.delete(cachedId);
133
+ });
134
+ return Array.from(selectedIds)
135
+ .map((selectedId) => cache.get(selectedId))
136
+ .filter((option) => option !== undefined);
137
+ }, [options, selectedIds]);
138
+ const shouldFilterLocally = filterOptions !== null && filterOptions !== void 0 ? filterOptions : !onSearchChange;
129
139
  const filteredOptions = (0, react_1.useMemo)(() => {
130
- if (!isSearchable || !currentSearch.trim())
140
+ if (!isSearchable || !currentSearch.trim() || !shouldFilterLocally)
131
141
  return options;
132
142
  const needle = currentSearch.trim().toLowerCase();
133
143
  return options.filter((option) => option.label.toString().toLowerCase().includes(needle));
134
- }, [options, currentSearch, isSearchable]);
144
+ }, [options, currentSearch, isSearchable, shouldFilterLocally]);
135
145
  const hasGroups = (0, react_1.useMemo)(() => options.some((option) => option.group), [options]);
136
146
  const resetAndClose = (0, react_1.useCallback)((opts) => {
137
147
  if (props.isMulti && (opts === null || opts === void 0 ? void 0 : opts.keepOpenForMulti))
@@ -153,15 +163,19 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
153
163
  : currentValue;
154
164
  setActiveIndex(filteredOptions.findIndex((option) => option.id.toString() === (firstSelected === null || firstSelected === void 0 ? void 0 : firstSelected.toString())));
155
165
  };
166
+ const selectedIdsRef = (0, react_1.useRef)(selectedIds);
167
+ selectedIdsRef.current = selectedIds;
168
+ const selectedOptionsRef = (0, react_1.useRef)(selectedOptions);
169
+ selectedOptionsRef.current = selectedOptions;
156
170
  const handleSelect = (0, react_1.useCallback)((option) => {
157
171
  var _a, _b;
158
172
  if (option.isDisabled)
159
173
  return;
160
174
  if (props.isMulti) {
161
- const alreadySelected = selectedIds.has(option.id.toString());
175
+ const alreadySelected = selectedIdsRef.current.has(option.id.toString());
162
176
  const next = alreadySelected
163
- ? selectedOptions.filter((selected) => selected.id.toString() !== option.id.toString())
164
- : [...selectedOptions, option];
177
+ ? selectedOptionsRef.current.filter((selected) => selected.id.toString() !== option.id.toString())
178
+ : [...selectedOptionsRef.current, option];
165
179
  if (!isValueControlled)
166
180
  setUncontrolledValue(next.map((o) => o.id));
167
181
  props.onChange(next);
@@ -176,7 +190,7 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
176
190
  (_b = triggerRef.current) === null || _b === void 0 ? void 0 : _b.focus();
177
191
  },
178
192
  // eslint-disable-next-line react-hooks/exhaustive-deps
179
- [props.isMulti, props.onChange, selectedIds, selectedOptions, resetAndClose, isValueControlled]);
193
+ [props.isMulti, props.onChange, resetAndClose, isValueControlled]);
180
194
  const handleCreate = (0, react_1.useCallback)(() => {
181
195
  if (!onCreate)
182
196
  return;
@@ -218,24 +232,47 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
218
232
  const maxMenuHeightPx = (0, react_1.useMemo)(() => parseRemToPx(maxMenuHeight, 256), [maxMenuHeight]);
219
233
  const shouldVirtualize = !!virtualizeAfter && !hasGroups && filteredOptions.length > virtualizeAfter;
220
234
  const [scrollTop, setScrollTop] = (0, react_1.useState)(0);
221
- const visibleCount = Math.ceil(maxMenuHeightPx / spec.virtualRowHeightPx) + VIRTUAL_OVERSCAN * 2;
235
+ // A description, an avatar or a renderOption row outgrows the spec's single-line height.
236
+ const [rowHeightPx, setRowHeightPx] = (0, react_1.useState)(spec.virtualRowHeightPx);
237
+ const visibleCount = Math.ceil(maxMenuHeightPx / rowHeightPx) + VIRTUAL_OVERSCAN * 2;
222
238
  const startIndex = shouldVirtualize
223
- ? Math.max(0, Math.floor(scrollTop / spec.virtualRowHeightPx) - VIRTUAL_OVERSCAN)
239
+ ? Math.max(0, Math.floor(scrollTop / rowHeightPx) - VIRTUAL_OVERSCAN)
224
240
  : 0;
225
241
  const endIndex = shouldVirtualize
226
242
  ? Math.min(filteredOptions.length, startIndex + visibleCount)
227
243
  : filteredOptions.length;
228
244
  const renderedOptions = shouldVirtualize ? filteredOptions.slice(startIndex, endIndex) : filteredOptions;
229
- const topPadPx = shouldVirtualize ? startIndex * spec.virtualRowHeightPx : 0;
230
- const bottomPadPx = shouldVirtualize ? (filteredOptions.length - endIndex) * spec.virtualRowHeightPx : 0;
245
+ const topPadPx = shouldVirtualize ? startIndex * rowHeightPx : 0;
246
+ const bottomPadPx = shouldVirtualize ? (filteredOptions.length - endIndex) * rowHeightPx : 0;
247
+ (0, react_1.useLayoutEffect)(() => {
248
+ var _a;
249
+ if (!isOpen)
250
+ return;
251
+ const row = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[role="option"]');
252
+ if (!row)
253
+ return;
254
+ const measured = row.getBoundingClientRect().height;
255
+ if (measured > 0 && Math.abs(measured - rowHeightPx) > 0.5)
256
+ setRowHeightPx(measured);
257
+ }, [isOpen, rowHeightPx, filteredOptions.length]);
258
+ // Windowing starts mid-list, so the window must open where the user already is.
259
+ (0, react_1.useLayoutEffect)(() => {
260
+ var _a, _b;
261
+ if (shouldVirtualize && !wasVirtualized.current)
262
+ setScrollTop((_b = (_a = listRef.current) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0);
263
+ wasVirtualized.current = shouldVirtualize;
264
+ }, [shouldVirtualize]);
231
265
  (0, react_1.useEffect)(() => {
232
266
  var _a, _b;
233
267
  if (activeIndex < 0 || !listRef.current)
234
268
  return;
269
+ if (lastActiveIndex.current === activeIndex)
270
+ return;
271
+ lastActiveIndex.current = activeIndex;
235
272
  const el = listRef.current;
236
273
  if (shouldVirtualize) {
237
- const targetTop = activeIndex * spec.virtualRowHeightPx;
238
- const targetBottom = targetTop + spec.virtualRowHeightPx;
274
+ const targetTop = activeIndex * rowHeightPx;
275
+ const targetBottom = targetTop + rowHeightPx;
239
276
  if (targetTop < el.scrollTop) {
240
277
  el.scrollTop = targetTop;
241
278
  setScrollTop(targetTop);
@@ -248,18 +285,44 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
248
285
  return;
249
286
  }
250
287
  (_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" });
251
- }, [activeIndex, shouldVirtualize, maxMenuHeightPx, spec.virtualRowHeightPx]);
288
+ }, [activeIndex, shouldVirtualize, maxMenuHeightPx, rowHeightPx]);
289
+ const loadMoreInFlightRef = (0, react_1.useRef)(false);
290
+ const prevOptionsLengthRef = (0, react_1.useRef)(options.length);
291
+ const prevIsLoadingRef = (0, react_1.useRef)(isLoading);
292
+ (0, react_1.useEffect)(() => {
293
+ if (options.length !== prevOptionsLengthRef.current)
294
+ loadMoreInFlightRef.current = false;
295
+ prevOptionsLengthRef.current = options.length;
296
+ }, [options.length]);
297
+ (0, react_1.useEffect)(() => {
298
+ if (prevIsLoadingRef.current && !isLoading)
299
+ loadMoreInFlightRef.current = false;
300
+ prevIsLoadingRef.current = isLoading;
301
+ }, [isLoading]);
302
+ const canLoadMore = !!onLoadMore && hasMore !== false;
303
+ const triggerLoadMore = (0, react_1.useCallback)(() => {
304
+ if (!onLoadMore || hasMore === false || isLoading || loadMoreInFlightRef.current)
305
+ return;
306
+ loadMoreInFlightRef.current = true;
307
+ onLoadMore();
308
+ }, [onLoadMore, hasMore, isLoading]);
252
309
  const moveActive = (delta) => {
253
310
  if (!filteredOptions.length)
254
311
  return;
255
- setActiveIndex((current) => {
256
- const next = current + delta;
257
- if (next < 0)
258
- return filteredOptions.length - 1;
259
- if (next >= filteredOptions.length)
260
- return 0;
261
- return next;
262
- });
312
+ const next = activeIndex + delta;
313
+ if (delta > 0 && next >= filteredOptions.length) {
314
+ if (canLoadMore) {
315
+ triggerLoadMore();
316
+ return;
317
+ }
318
+ setActiveIndex(0);
319
+ return;
320
+ }
321
+ if (next < 0) {
322
+ setActiveIndex(filteredOptions.length - 1);
323
+ return;
324
+ }
325
+ setActiveIndex(next);
263
326
  };
264
327
  const moveActiveBy = (delta) => {
265
328
  if (!filteredOptions.length)
@@ -346,10 +409,8 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
346
409
  const { scrollTop: nextScrollTop, clientHeight, scrollHeight } = event.currentTarget;
347
410
  if (shouldVirtualize)
348
411
  setScrollTop(nextScrollTop);
349
- if (!onLoadMore || isLoading)
350
- return;
351
412
  if (nextScrollTop + clientHeight >= scrollHeight - LOAD_MORE_THRESHOLD_PX)
352
- onLoadMore();
413
+ triggerLoadMore();
353
414
  };
354
415
  const handleFilterChange = (event) => {
355
416
  const nextText = event.target.value;
@@ -381,7 +442,7 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
381
442
  const showClear = isClearable && isInteractive && selectedOptions.length > 0;
382
443
  const triggerSx = (0, react_1.useMemo)(() => ({
383
444
  ...(0, fieldStyles_1.fieldControlStyles)(colors),
384
- ...chipTriggerSizeStyles(size, showChips, chipSize),
445
+ ...(0, chipTriggerSize_1.chipTriggerSizeStyles)(size, showChips, chipSize),
385
446
  ...(selectedOptions.length ? {} : { color: colors.textMuted }),
386
447
  ...(isOpen
387
448
  ? { borderColor: error ? colors.semanticText.error : colors.primary[500], boxShadow: (0, focusRing_styles_1.focusGlowShadow)(colors) }
@@ -411,7 +472,7 @@ const SelectPopup = (0, react_1.forwardRef)(function SelectPopup(props, ref) {
411
472
  const selectedValueIds = selectedOptions.map((option) => option.id.toString());
412
473
  const hiddenValues = !name ? [] : props.isMulti ? selectedValueIds : [(_c = selectedValueIds[0]) !== null && _c !== void 0 ? _c : ""];
413
474
  const groupedRows = (0, react_1.useMemo)(() => (hasGroups ? buildGroupedRows(filteredOptions) : []), [hasGroups, filteredOptions]);
414
- return ((0, jsx_runtime_1.jsxs)(react_2.FormControl, { isInvalid: error, isDisabled: isDisabled, isReadOnly: isReadOnly, style: formControlStyle, width: width, children: [label && ((0, jsx_runtime_1.jsx)(FormLabel_1.TextLabel, { label: label, id: ids.inputId, isRequired: isRequired, isInformation: isInformation, informationMessage: informationMessage })), (0, jsx_runtime_1.jsxs)(react_2.Popover, { isOpen: isOpen, onClose: () => resetAndClose(), placement: "bottom-start", autoFocus: false, returnFocusOnClose: false, gutter: 4, children: [(0, jsx_runtime_1.jsx)(react_2.PopoverAnchor, { children: (0, jsx_runtime_1.jsxs)(react_2.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: [(0, jsx_runtime_1.jsx)(react_2.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 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [visibleChipOptions.map((option) => ((0, jsx_runtime_1.jsx)(Tag_1.default, { label: String(option.label), size: chipSize, variant: "subtle", icon: isInteractive ? lucide_react_1.X : undefined, onIconClick: isInteractive ? () => handleSelect(option) : undefined }, option.id))), overflowChipCount > 0 && ((0, jsx_runtime_1.jsx)(Tag_1.default, { label: `+${overflowChipCount}`, size: chipSize, variant: "subtle", colorScheme: "gray" }))] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [triggerIcon ? ((0, jsx_runtime_1.jsx)(react_2.Box, { as: "span", display: "inline-flex", flexShrink: 0, children: triggerIcon })) : null, (0, jsx_runtime_1.jsx)(react_2.Box, { as: "span", isTruncated: true, children: renderValue ? renderValue(selectedOptions) : defaultTriggerContent() })] })) }), showClear && ((0, jsx_runtime_1.jsx)(react_2.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: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: controlSizes_1.CONTROL_SIZES[size].iconSize }) })), (0, jsx_runtime_1.jsx)(react_2.Box, { as: "span", display: "inline-flex", flexShrink: 0, ml: 2, transition: CHEVRON_TRANSITION, transform: isOpen ? "rotate(180deg)" : "rotate(0deg)", color: colors.textMuted, sx: (0, motion_2.reducedMotionSx)(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: controlSizes_1.CONTROL_SIZES[size].iconSize }) })] }) }), (0, jsx_runtime_1.jsx)(react_2.Portal, { children: (0, jsx_runtime_1.jsxs)(react_2.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: [(0, jsx_runtime_1.jsx)(react_2.Box, { p: 2, borderBottom: `0.063rem solid ${colors.boxborder[200]}`, display: isSearchable ? undefined : "none", children: (0, jsx_runtime_1.jsx)(react_2.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 }) }), (0, jsx_runtime_1.jsxs)(react_2.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 && ((0, jsx_runtime_1.jsx)(react_2.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 && ((0, jsx_runtime_1.jsx)(react_2.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
475
+ return ((0, jsx_runtime_1.jsxs)(react_2.FormControl, { isInvalid: error, isDisabled: isDisabled, isReadOnly: isReadOnly, style: formControlStyle, width: width, children: [label && ((0, jsx_runtime_1.jsx)(FormLabel_1.TextLabel, { label: label, id: ids.inputId, isRequired: isRequired, isInformation: isInformation, informationMessage: informationMessage })), (0, jsx_runtime_1.jsxs)(react_2.Popover, { isOpen: isOpen, onClose: () => resetAndClose(), placement: "bottom-start", autoFocus: false, returnFocusOnClose: false, gutter: 4, children: [(0, jsx_runtime_1.jsx)(react_2.PopoverAnchor, { children: (0, jsx_runtime_1.jsxs)(react_2.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: [(0, jsx_runtime_1.jsx)(react_2.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 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [visibleChipOptions.map((option) => ((0, jsx_runtime_1.jsx)(Tag_1.default, { label: String(option.label), size: chipSize, variant: "subtle", icon: isInteractive ? lucide_react_1.X : undefined, onIconClick: isInteractive ? () => handleSelect(option) : undefined }, option.id))), overflowChipCount > 0 && ((0, jsx_runtime_1.jsx)(Tag_1.default, { label: `+${overflowChipCount}`, size: chipSize, variant: "subtle", colorScheme: "gray" }))] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [triggerIcon ? ((0, jsx_runtime_1.jsx)(react_2.Box, { as: "span", display: "inline-flex", flexShrink: 0, children: triggerIcon })) : null, (0, jsx_runtime_1.jsx)(react_2.Box, { as: "span", isTruncated: true, children: renderValue ? renderValue(selectedOptions) : defaultTriggerContent() })] })) }), showClear && ((0, jsx_runtime_1.jsx)(react_2.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: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: controlSizes_1.CONTROL_SIZES[size].iconSize }) })), (0, jsx_runtime_1.jsx)(react_2.Box, { as: "span", display: "inline-flex", flexShrink: 0, ml: 2, transition: CHEVRON_TRANSITION, transform: isOpen ? "rotate(180deg)" : "rotate(0deg)", color: colors.textMuted, sx: (0, motion_2.reducedMotionSx)(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { size: controlSizes_1.CONTROL_SIZES[size].iconSize }) })] }) }), (0, jsx_runtime_1.jsx)(react_2.Portal, { children: (0, jsx_runtime_1.jsxs)(react_2.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: [(0, jsx_runtime_1.jsx)(react_2.Box, { p: spec.mx, borderBottom: `0.063rem solid ${colors.boxborder[200]}`, display: isSearchable ? undefined : "none", children: (0, jsx_runtime_1.jsx)(react_2.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 }) }), (0, jsx_runtime_1.jsxs)(react_2.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 && ((0, jsx_runtime_1.jsx)(react_2.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 && ((0, jsx_runtime_1.jsx)(react_2.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
415
476
  ? groupedRows.map((row) => {
416
477
  var _a;
417
478
  return row.kind === "header" ? ((0, jsx_runtime_1.jsx)(react_2.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)) : ((0, jsx_runtime_1.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));
@@ -72,6 +72,8 @@ type SelectCommonProps = {
72
72
  onSearchChange?: (query: string) => void;
73
73
  /** Debounce for `onSearchChange`, in ms. Defaults to 0 — every keystroke, matching SelectV2 today. */
74
74
  searchDebounceMs?: number;
75
+ /** Applies the local label-substring filter. Defaults to false once `onSearchChange` is set (server owns matching), true otherwise. */
76
+ filterOptions?: boolean;
75
77
  /** Shows a clear (x) control once a value is selected. Popup mode only. */
76
78
  isClearable?: boolean;
77
79
  /** Fires after a clear action, in addition to `onChange`. Popup mode only. */
@@ -84,6 +86,8 @@ type SelectCommonProps = {
84
86
  noOptionsMessage?: ReactNode;
85
87
  /** Fires when the list is scrolled to its end and not loading — append the next page. Popup mode only. */
86
88
  onLoadMore?: () => void;
89
+ /** Set explicitly to false once paging is exhausted, to stop further `onLoadMore` calls. Popup mode only. */
90
+ hasMore?: boolean;
87
91
  /** Full control over an option row; receives isSelected/isActive. Popup mode only. */
88
92
  renderOption?: (option: OptionProp, state: {
89
93
  isSelected: boolean;
@@ -1,14 +1,7 @@
1
1
  import React from "react";
2
- import { type ControlSize } from "../../Theme/tokens/primitives/controlSizes";
2
+ import { chipTriggerSizeStyles } from "../Common/chipTriggerSize";
3
3
  import type { SelectV2Props } from "./SelectV2Props";
4
- type ChipSize = "xs" | "sm";
5
- export declare const chipTriggerSizeStyles: (size: ControlSize, hasChips: boolean, chipSize?: ChipSize) => {
6
- minH: string;
7
- fontSize: string;
8
- px: string;
9
- h?: string;
10
- py?: string;
11
- };
4
+ export { chipTriggerSizeStyles };
12
5
  /** @deprecated Use `Select` with `native={false}`. See MERGE-NOTES-select-deprecation.md. */
13
6
  declare const SelectV2: React.ForwardRefExoticComponent<SelectV2Props & React.RefAttributes<HTMLDivElement>>;
14
7
  /** @deprecated Use `Select` with `native={false}`. */
@@ -7,16 +7,8 @@ exports.chipTriggerSizeStyles = void 0;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const react_1 = require("react");
9
9
  const Select_1 = __importDefault(require("../Select/Select"));
10
- const controlSizes_1 = require("../../Theme/tokens/primitives/controlSizes");
11
- const CHIP_TAG_HEIGHT_PX = { xs: 20, sm: 24 };
12
- const controlHeightPx = (size) => parseFloat(controlSizes_1.CONTROL_SIZES[size].height) * 16;
13
- const chipRowPaddingY = (size, chipSize) => `${(controlHeightPx(size) - CHIP_TAG_HEIGHT_PX[chipSize]) / 2 / 16}rem`;
14
- const chipTriggerSizeStyles = (size, hasChips, chipSize = "sm") => {
15
- const spec = controlSizes_1.CONTROL_SIZES[size];
16
- const base = { minH: spec.height, fontSize: spec.fontSize, px: spec.paddingX };
17
- return hasChips ? { ...base, py: chipRowPaddingY(size, chipSize) } : { ...base, h: spec.height };
18
- };
19
- exports.chipTriggerSizeStyles = chipTriggerSizeStyles;
10
+ const chipTriggerSize_1 = require("../Common/chipTriggerSize");
11
+ Object.defineProperty(exports, "chipTriggerSizeStyles", { enumerable: true, get: function () { return chipTriggerSize_1.chipTriggerSizeStyles; } });
20
12
  /** @deprecated Use `Select` with `native={false}`. See MERGE-NOTES-select-deprecation.md. */
21
13
  const SelectV2 = (0, react_1.forwardRef)(function SelectV2(props, ref) {
22
14
  return (0, jsx_runtime_1.jsx)(Select_1.default, { ...props, native: false, ref: ref });