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.
- package/CHANGELOG.md +53 -0
- package/COMPONENT-GOTCHAS.md +39 -6
- package/dist/Components/Common/chipTriggerSize.d.ts +10 -0
- package/dist/Components/Common/chipTriggerSize.js +15 -0
- package/dist/Components/SearchSelect/SearchSelect.js +48 -7
- package/dist/Components/SearchSelect/SearchSelectProps.d.ts +5 -1
- package/dist/Components/Select/SelectNative.d.ts +2 -0
- package/dist/Components/Select/SelectPopup.js +97 -36
- package/dist/Components/Select/SelectProps.d.ts +4 -0
- package/dist/Components/SelectV2/SelectV2.d.ts +2 -9
- package/dist/Components/SelectV2/SelectV2.js +2 -10
- package/dist/Components/Table/Table.js +6 -7
- package/dist/Components/Table/TableProps.d.ts +9 -1
- package/dist/Components/Table/components/Pagination.js +8 -5
- package/dist/Components/Table/components/TableBody.d.ts +2 -1
- package/dist/Components/Table/components/TableBody.js +82 -15
- package/dist/Components/Table/components/TableHeader.d.ts +1 -1
- package/dist/Components/Table/components/TableHeader.js +8 -9
- package/dist/Components/Table/components/rowMetrics.d.ts +0 -1
- package/dist/Components/Table/components/rowMetrics.js +1 -2
- package/dist/Components/Table/hooks/useTable.d.ts +5 -1
- package/dist/Components/Table/hooks/useTable.js +43 -7
- package/dist/esm/Components/Common/chipTriggerSize.js +11 -0
- package/dist/esm/Components/SearchSelect/SearchSelect.js +48 -7
- package/dist/esm/Components/Select/SelectPopup.js +97 -36
- package/dist/esm/Components/SelectV2/SelectV2.js +2 -9
- package/dist/esm/Components/Table/Table.js +6 -7
- package/dist/esm/Components/Table/components/Pagination.js +8 -5
- package/dist/esm/Components/Table/components/TableBody.js +81 -16
- package/dist/esm/Components/Table/components/TableHeader.js +8 -9
- package/dist/esm/Components/Table/components/rowMetrics.js +0 -1
- package/dist/esm/Components/Table/hooks/useTable.js +44 -8
- 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
|
package/COMPONENT-GOTCHAS.md
CHANGED
|
@@ -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
|
|
1253
|
+
### `SearchSelect`'s `pagination` now takes an explicit "has more" signal — prefer it at an exact-multiple total
|
|
1223
1254
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
looks
|
|
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:**
|
|
1229
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
175
|
+
const alreadySelected = selectedIdsRef.current.has(option.id.toString());
|
|
162
176
|
const next = alreadySelected
|
|
163
|
-
?
|
|
164
|
-
: [...
|
|
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,
|
|
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
|
-
|
|
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 /
|
|
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 *
|
|
230
|
-
const bottomPadPx = shouldVirtualize ? (filteredOptions.length - endIndex) *
|
|
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 *
|
|
238
|
-
const targetBottom = targetTop +
|
|
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,
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
2
|
+
import { chipTriggerSizeStyles } from "../Common/chipTriggerSize";
|
|
3
3
|
import type { SelectV2Props } from "./SelectV2Props";
|
|
4
|
-
|
|
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
|
|
11
|
-
|
|
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 });
|