tp-react-elements 3.0.16 → 3.0.18

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.
@@ -1,3 +1,3 @@
1
- var InputRangeField = {};
1
+ var DateInput = {};
2
2
 
3
- export { InputRangeField as __exports };
3
+ export { DateInput as __exports };
@@ -1,3 +1,3 @@
1
- var Month = {};
1
+ var InputRangeField = {};
2
2
 
3
- export { Month as __exports };
3
+ export { InputRangeField as __exports };
@@ -1,3 +1,3 @@
1
- var DateInput = {};
1
+ var Month = {};
2
2
 
3
- export { DateInput as __exports };
3
+ export { Month as __exports };
@@ -1,3 +1,3 @@
1
- var DateRangePicker = {};
1
+ var Calendar = {};
2
2
 
3
- export { DateRangePicker as __exports };
3
+ export { Calendar as __exports };
@@ -1,3 +1,3 @@
1
- var Calendar = {};
1
+ var DateRangePicker = {};
2
2
 
3
- export { Calendar as __exports };
3
+ export { DateRangePicker as __exports };
@@ -83,7 +83,7 @@ const DateRangePickerComponent = ({ item, control, watch, setValue, getValues, c
83
83
  const displayValue = state[0].startDate && state[0].endDate
84
84
  ? `${format(state[0].startDate, 'dd/MM/yyyy')} - ${format(state[0].endDate, 'dd/MM/yyyy')}`
85
85
  : '';
86
- const label = variant !== 'standard' ? `${item.label}${item.required ? ' *' : ''}` : '';
86
+ const label = variant !== 'standard' && item.labelVariant !== 'static' ? `${item.label}${item.required ? ' *' : ''}` : '';
87
87
  return (jsxs(Fragment, { children: [jsx(Box, { onClick: handleClick, sx: { width: '100%' }, children: jsx(Controller, { name: fromDateName, control: control, render: ({ fieldState }) => (jsxs(Fragment, { children: [renderLabel(variant, {
88
88
  item,
89
89
  ...rest,
@@ -17,6 +17,7 @@ import FormTextFieldWithSelect from '../FormComponents/FormTextFieldWithSelect/F
17
17
  import DateRangePickerComponent from '../DateRangePicker/DateRangePicker.esm.js';
18
18
  import RichTextEditorWrapper from '../FormComponents/RichTextEditor/RichTextEditorWrapper.esm.js';
19
19
  import SingleSelectSearchApi from '../FormComponents/Select/SingleSelectSearchApi.esm.js';
20
+ import MultiSelectSearchApi from '../FormComponents/Select/MultiSelectSearchApi.esm.js';
20
21
  import DatetimepickerWrapper from '../FormComponents/DatePicker/DatetimepickerWrapper.esm.js';
21
22
 
22
23
  // Lazy-loaded heavy components
@@ -90,6 +91,8 @@ const RenderForm = (props) => {
90
91
  return jsx(MultiSelectAutocomplete, { props: props, variant: variant });
91
92
  case 'single-select-search':
92
93
  return jsx(SingleSelectSearchApi, { props: props, variant: variant });
94
+ case 'multi-select-search':
95
+ return jsx(MultiSelectSearchApi, { props: props, variant: variant });
93
96
  // Dropdown select (non auto-complete)
94
97
  case 'select-v2':
95
98
  case 'basic-select': // new alias
@@ -0,0 +1,88 @@
1
+ import { FormRenderProps, VariantProps } from '../../../utils';
2
+ import type { SearchOption } from './SingleSelectSearchApi';
3
+ export type { SearchOption };
4
+ /**
5
+ * Multi-select autocomplete that fetches options from a remote API — the
6
+ * multi-value counterpart of {@link SingleSelectSearchApi}, combining its
7
+ * debounced-search/API mechanics with `MultiSelectAutocomplete`'s chip +
8
+ * checkbox multi-select UI.
9
+ *
10
+ * ### Authentication
11
+ * Prefer `fetchOptions` — a consumer-supplied async function that handles auth
12
+ * internally and returns `SearchOption[]`. Falls back to `item.api` +
13
+ * `item.jwtHeader` via axios when `fetchOptions` is not provided.
14
+ *
15
+ * ### Values
16
+ * Stored as **`string[]`** in the form field (same convention as
17
+ * `MultiSelectAutocomplete`). CSV strings are accepted and normalised
18
+ * automatically.
19
+ *
20
+ * ### Pre-selected values (edit mode)
21
+ * Pass every pre-selected option in `item.options` so their labels display
22
+ * immediately without a search round-trip — same pattern as
23
+ * `SingleSelectSearchApi`. Unlike the single-select version there is no
24
+ * `autoFIll` equivalent here: with N pre-selected ids there is no single
25
+ * search term to seed, so `item.options` is the only way to pre-populate
26
+ * labels. Any pre-selected id never seen via `item.options` or a live search
27
+ * still renders as a chip (falling back to the raw id as its label) rather
28
+ * than silently disappearing from the field.
29
+ *
30
+ * ### API call guard
31
+ * Searches fire only after `minSearchLength` chars AND after the
32
+ * `debounceMs` debounce delay — preventing a request per keystroke.
33
+ *
34
+ * ### onChangeFn
35
+ * Only fires on genuine user interactions (select / remove / clear-all) —
36
+ * not when the form is populated programmatically (`defaultValues`,
37
+ * `reset()`, external `setValue()`). Receives `(values: string[], options:
38
+ * SearchOption[])`.
39
+ *
40
+ * ### Reviewing/managing selections — the info icon
41
+ * Once at least one option is selected, an info icon appears at the end of
42
+ * the input (next to MUI's own clear/dropdown icons). Clicking it opens a
43
+ * popover listing every selected label with its own remove button — a
44
+ * click-based, keyboard/touch-friendly alternative to the "N selected" chip's
45
+ * hover-only tooltip, and the only way to review selections once they've
46
+ * collapsed past `maxChips` (the dropdown itself only shows the *current
47
+ * search's* results, not the full selection).
48
+ *
49
+ * ### maxSelections — capping how many can be picked
50
+ * When set, further options are disabled (greyed out, not just hidden) once
51
+ * the cap is reached — already-selected ones stay clickable so the user can
52
+ * still deselect. A "`N/max` selected" caption appears below the field, and
53
+ * the dropdown's empty-state message explains the cap once it's hit. Omitted
54
+ * by default — unlimited selections, unchanged from before this prop existed.
55
+ */
56
+ declare const MultiSelectSearchApi: ({ props, variant, minSearchLength, debounceMs, fetchOptions, maxChips, maxSelections, }: {
57
+ props: FormRenderProps;
58
+ variant: VariantProps;
59
+ /** Minimum characters before the search fires. Default: `3`. */
60
+ minSearchLength?: number;
61
+ /** Debounce delay in milliseconds. Default: `300`. */
62
+ debounceMs?: number;
63
+ /**
64
+ * Preferred way to load options. Takes precedence over `item.api`.
65
+ * Receives the current search text and an AbortSignal for cancellation.
66
+ *
67
+ * ```tsx
68
+ * fetchOptions={async (text, signal) => {
69
+ * const res = await myAuthenticatedApi.search(text, { signal });
70
+ * return res.items.map(r => ({ label: r.name, value: r.id }));
71
+ * }}
72
+ * ```
73
+ */
74
+ fetchOptions?: (searchText: string, signal: AbortSignal) => Promise<SearchOption[]>;
75
+ /**
76
+ * Maximum number of chips shown individually before collapsing into
77
+ * "N selected". Default: `2`.
78
+ */
79
+ maxChips?: number;
80
+ /**
81
+ * Maximum number of options that can be selected at once. When the cap is
82
+ * reached, unselected options are disabled (still visible, not removed
83
+ * from the list) until the user frees up a slot by deselecting one.
84
+ * Omitted by default — no limit.
85
+ */
86
+ maxSelections?: number;
87
+ }) => import("react/jsx-runtime").JSX.Element;
88
+ export default MultiSelectSearchApi;
@@ -0,0 +1,375 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { Autocomplete, TextField, InputAdornment, IconButton, Box, Tooltip, Chip, Checkbox, Paper, Popover, List, ListItem, ListItemText, Typography } from '@mui/material';
3
+ import CloseIcon from '@mui/icons-material/Close';
4
+ import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
5
+ import '../../../node_modules/axios/index.esm.js';
6
+ import { useMemo, useState, useRef, useEffect, useCallback } from 'react';
7
+ import { useWatch, Controller } from 'react-hook-form';
8
+ import 'dayjs';
9
+ import { renderLabel } from '../../../utils/Constants/FormConstants.esm.js';
10
+ import FormBottomField from '../FormBottomField/FormBottomField.esm.js';
11
+ import axios from '../../../node_modules/axios/lib/axios.esm.js';
12
+
13
+ // ─── Module-level helpers ─────────────────────────────────────────────────────
14
+ /**
15
+ * Appends a query parameter correctly regardless of whether the base URL
16
+ * already contains a query string (`?` or `&`).
17
+ */
18
+ const appendQueryParam = (url, key, value) => {
19
+ const sep = url.includes('?') ? '&' : '?';
20
+ return `${url}${sep}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
21
+ };
22
+ /** True when the error is a request cancellation (AbortController / axios). */
23
+ const isCancellation = (err) => axios.isCancel?.(err) ||
24
+ (err instanceof Error && (err.name === 'AbortError' || err.name === 'CanceledError'));
25
+ /**
26
+ * Normalises any value shape coming from react-hook-form to a plain string[].
27
+ *
28
+ * | Input shape | Output |
29
+ * |-----------------------|-------------------|
30
+ * | `string[]` | as-is (coerced) |
31
+ * | `'1,2,3'` (CSV) | `['1','2','3']` |
32
+ * | `null` / `undefined` | `[]` |
33
+ * | any other scalar | `[String(value)]` |
34
+ */
35
+ const normalizeToStringArray = (value) => {
36
+ if (Array.isArray(value))
37
+ return value.map((v) => String(v));
38
+ if (typeof value === 'string') {
39
+ return value
40
+ .split(',')
41
+ .map((v) => v.trim())
42
+ .filter((v) => v.length > 0);
43
+ }
44
+ if (value == null)
45
+ return [];
46
+ return [String(value)];
47
+ };
48
+ /**
49
+ * MUI Autocomplete change reasons that represent a genuine user interaction.
50
+ * `'blur'` is deliberately excluded because it fires programmatically when
51
+ * focus moves away (e.g. on form reset / programmatic setValue), which would
52
+ * trigger `onChangeFn` → API calls for pre-selected values.
53
+ */
54
+ const USER_CHANGE_REASONS = new Set(['selectOption', 'removeOption', 'clear']);
55
+ // ─── Debounce hook ────────────────────────────────────────────────────────────
56
+ function useDebounce(value, delayMs) {
57
+ const [debounced, setDebounced] = useState(value);
58
+ useEffect(() => {
59
+ const id = setTimeout(() => setDebounced(value), delayMs);
60
+ return () => clearTimeout(id);
61
+ }, [value, delayMs]);
62
+ return debounced;
63
+ }
64
+ // ─── Component ───────────────────────────────────────────────────────────────
65
+ /**
66
+ * Multi-select autocomplete that fetches options from a remote API — the
67
+ * multi-value counterpart of {@link SingleSelectSearchApi}, combining its
68
+ * debounced-search/API mechanics with `MultiSelectAutocomplete`'s chip +
69
+ * checkbox multi-select UI.
70
+ *
71
+ * ### Authentication
72
+ * Prefer `fetchOptions` — a consumer-supplied async function that handles auth
73
+ * internally and returns `SearchOption[]`. Falls back to `item.api` +
74
+ * `item.jwtHeader` via axios when `fetchOptions` is not provided.
75
+ *
76
+ * ### Values
77
+ * Stored as **`string[]`** in the form field (same convention as
78
+ * `MultiSelectAutocomplete`). CSV strings are accepted and normalised
79
+ * automatically.
80
+ *
81
+ * ### Pre-selected values (edit mode)
82
+ * Pass every pre-selected option in `item.options` so their labels display
83
+ * immediately without a search round-trip — same pattern as
84
+ * `SingleSelectSearchApi`. Unlike the single-select version there is no
85
+ * `autoFIll` equivalent here: with N pre-selected ids there is no single
86
+ * search term to seed, so `item.options` is the only way to pre-populate
87
+ * labels. Any pre-selected id never seen via `item.options` or a live search
88
+ * still renders as a chip (falling back to the raw id as its label) rather
89
+ * than silently disappearing from the field.
90
+ *
91
+ * ### API call guard
92
+ * Searches fire only after `minSearchLength` chars AND after the
93
+ * `debounceMs` debounce delay — preventing a request per keystroke.
94
+ *
95
+ * ### onChangeFn
96
+ * Only fires on genuine user interactions (select / remove / clear-all) —
97
+ * not when the form is populated programmatically (`defaultValues`,
98
+ * `reset()`, external `setValue()`). Receives `(values: string[], options:
99
+ * SearchOption[])`.
100
+ *
101
+ * ### Reviewing/managing selections — the info icon
102
+ * Once at least one option is selected, an info icon appears at the end of
103
+ * the input (next to MUI's own clear/dropdown icons). Clicking it opens a
104
+ * popover listing every selected label with its own remove button — a
105
+ * click-based, keyboard/touch-friendly alternative to the "N selected" chip's
106
+ * hover-only tooltip, and the only way to review selections once they've
107
+ * collapsed past `maxChips` (the dropdown itself only shows the *current
108
+ * search's* results, not the full selection).
109
+ *
110
+ * ### maxSelections — capping how many can be picked
111
+ * When set, further options are disabled (greyed out, not just hidden) once
112
+ * the cap is reached — already-selected ones stay clickable so the user can
113
+ * still deselect. A "`N/max` selected" caption appears below the field, and
114
+ * the dropdown's empty-state message explains the cap once it's hit. Omitted
115
+ * by default — unlimited selections, unchanged from before this prop existed.
116
+ */
117
+ const MultiSelectSearchApi = ({ props, variant, minSearchLength = 3, debounceMs = 300, fetchOptions, maxChips = 2, maxSelections, }) => {
118
+ // ── Stable references from props ──────────────────────────────────────────
119
+ const { control, errors, item, setValue, clearErrors } = props;
120
+ const { name, label, required, disable, placeholder, helperText, setLoadList, api, jwtHeader, searchValue: searchParamKey, options: rawOptions, sx: itemSx, labelVariant, } = item;
121
+ // ── Form value (reactive, no function call in deps) ───────────────────────
122
+ const formValue = useWatch({ control, name });
123
+ const selectedIds = useMemo(() => normalizeToStringArray(formValue), [formValue]);
124
+ const isError = !!errors?.[name];
125
+ const isShowBorderError = isError && selectedIds.length === 0;
126
+ // ── State ─────────────────────────────────────────────────────────────────
127
+ const [open, setOpen] = useState(false);
128
+ /** Anchor for the "view/manage selected items" popover — null when closed. */
129
+ const [infoAnchorEl, setInfoAnchorEl] = useState(null);
130
+ /** Text shown in the input (display). */
131
+ const [displayText, setDisplayText] = useState('');
132
+ /** Text that drives API calls — only updated when user types. */
133
+ const [searchQuery, setSearchQuery] = useState('');
134
+ /** Live search results for the current query. */
135
+ const [options, setOptions] = useState([]);
136
+ const [isLoading, setIsLoading] = useState(false);
137
+ const [fetchError, setFetchError] = useState(null);
138
+ const tagsContainerRef = useRef(null);
139
+ /**
140
+ * Persisted label cache, keyed by `String(value)`, for every option ever
141
+ * seen — seeded from `item.options`, then grown as live search results and
142
+ * user selections come in. This is what lets a chip keep its label after a
143
+ * later search replaces `options` with unrelated results (`options` only
144
+ * ever reflects the *current* query's matches).
145
+ */
146
+ const [knownOptions, setKnownOptions] = useState(() => {
147
+ const map = new Map();
148
+ (rawOptions || []).forEach((o) => map.set(String(o.value), { label: String(o.label), value: o.value }));
149
+ return map;
150
+ });
151
+ const debouncedSearch = useDebounce(searchQuery, debounceMs);
152
+ // ── Grow the label cache as live search results come in ───────────────────
153
+ useEffect(() => {
154
+ if (options.length === 0)
155
+ return;
156
+ setKnownOptions((prev) => {
157
+ let changed = false;
158
+ const next = new Map(prev);
159
+ for (const o of options) {
160
+ const key = String(o.value);
161
+ if (!next.has(key)) {
162
+ next.set(key, o);
163
+ changed = true;
164
+ }
165
+ }
166
+ return changed ? next : prev;
167
+ });
168
+ }, [options]);
169
+ // ── API fetch ─────────────────────────────────────────────────────────────
170
+ useEffect(() => {
171
+ if (!debouncedSearch || debouncedSearch.length < minSearchLength) {
172
+ setOptions([]);
173
+ return;
174
+ }
175
+ const controller = new AbortController();
176
+ setIsLoading(true);
177
+ setFetchError(null);
178
+ const load = async () => {
179
+ try {
180
+ let data;
181
+ if (fetchOptions) {
182
+ // ── Preferred path: consumer-supplied function (auth-aware) ──
183
+ data = await fetchOptions(debouncedSearch, controller.signal);
184
+ }
185
+ else if (api) {
186
+ // ── Fallback: item.api + item.jwtHeader via axios ─────────────
187
+ const url = appendQueryParam(api, searchParamKey || 'searchvalue', debouncedSearch);
188
+ const res = await axios.get(url, { signal: controller.signal, headers: jwtHeader });
189
+ data = res.data;
190
+ }
191
+ else {
192
+ data = [];
193
+ }
194
+ setOptions(data);
195
+ setLoadList?.(data);
196
+ setIsLoading(false);
197
+ }
198
+ catch (err) {
199
+ if (isCancellation(err))
200
+ return; // silently ignore aborted requests
201
+ setFetchError('Failed to load options. Please try again.');
202
+ setIsLoading(false);
203
+ }
204
+ };
205
+ load();
206
+ return () => controller.abort();
207
+ }, [debouncedSearch, minSearchLength, api, fetchOptions]); // eslint-disable-line react-hooks/exhaustive-deps
208
+ // ── Stable callbacks ──────────────────────────────────────────────────────
209
+ const getOptionLabel = useCallback((option) => option?.label ?? '', []);
210
+ const isOptionEqualToValue = useCallback((option, value) => String(option?.value) === String(value?.value), []);
211
+ const handleChange = useCallback((_, newValue, reason) => {
212
+ // Defensive guard — getOptionDisabled already blocks picking past the
213
+ // cap at the UI level, this just protects against any other path in.
214
+ if (typeof maxSelections === 'number' && (newValue?.length ?? 0) > maxSelections)
215
+ return;
216
+ const values = (newValue || []).map((o) => String(o.value));
217
+ setValue(name, values, { shouldValidate: false, shouldDirty: true });
218
+ // Cache labels immediately so chips never flash blank before the
219
+ // options-grow effect above runs on the next render.
220
+ setKnownOptions((prev) => {
221
+ const next = new Map(prev);
222
+ (newValue || []).forEach((o) => next.set(String(o.value), o));
223
+ return next;
224
+ });
225
+ if (USER_CHANGE_REASONS.has(reason)) {
226
+ item.onChangeFn?.(values, newValue);
227
+ }
228
+ clearErrors?.(name);
229
+ }, [setValue, clearErrors, name, item.onChangeFn, maxSelections]);
230
+ const handleInputChange = useCallback((_, newInputValue, reason) => {
231
+ setDisplayText(newInputValue);
232
+ if (reason === 'input') {
233
+ // User is typing → trigger debounced API search
234
+ setSearchQuery(newInputValue);
235
+ }
236
+ if (reason === 'clear') {
237
+ // Clear (×) icon — resets the search text/results; selected chips are
238
+ // cleared separately via handleChange's 'clear' reason.
239
+ setSearchQuery('');
240
+ setOptions([]);
241
+ setDisplayText('');
242
+ }
243
+ // 'reset' fires when MUI resets the input text after a selection — no
244
+ // search should trigger here.
245
+ }, []);
246
+ /** Clears ALL selections — wired to the delete button on the overflow chip. */
247
+ const handleClearAll = useCallback(() => {
248
+ setValue(name, [], { shouldValidate: false, shouldDirty: true });
249
+ item.onChangeFn?.([], []); // user explicitly cleared all → fire callback
250
+ clearErrors?.(name);
251
+ }, [setValue, clearErrors, name, item.onChangeFn]);
252
+ // ── Resolved Autocomplete value ───────────────────────────────────────────
253
+ // Every selected id is resolved to its full option via: label cache →
254
+ // current search results → raw id as a last-resort label (never silently
255
+ // dropped, unlike a lookup miss on the single-select counterpart).
256
+ const resolvedValues = useMemo(() => {
257
+ return selectedIds.map((id) => {
258
+ const known = knownOptions.get(id);
259
+ if (known)
260
+ return known;
261
+ const live = options.find((o) => String(o.value) === id);
262
+ if (live)
263
+ return live;
264
+ return { label: id, value: id };
265
+ });
266
+ }, [selectedIds, knownOptions, options]);
267
+ /** Removes a single selection — wired to each row in the info popover. */
268
+ const handleRemoveOne = useCallback((valueToRemove) => {
269
+ const remaining = resolvedValues.filter((o) => String(o.value) !== String(valueToRemove));
270
+ const values = remaining.map((o) => String(o.value));
271
+ setValue(name, values, { shouldValidate: false, shouldDirty: true });
272
+ item.onChangeFn?.(values, remaining);
273
+ clearErrors?.(name);
274
+ }, [resolvedValues, setValue, clearErrors, name, item.onChangeFn]);
275
+ /**
276
+ * Backspace with an empty search box removes the last-selected chip —
277
+ * standard multi-select affordance (Gmail "To:" field, MUI Chip patterns).
278
+ * Not native here because `inputValue` is externally controlled for the
279
+ * debounced-search wiring, which suppresses MUI Autocomplete's own default
280
+ * handling of this key combination.
281
+ */
282
+ const handleInputKeyDown = useCallback((e) => {
283
+ if (e.key === 'Backspace' && displayText === '' && resolvedValues.length > 0) {
284
+ handleRemoveOne(resolvedValues[resolvedValues.length - 1].value);
285
+ }
286
+ }, [displayText, resolvedValues, handleRemoveOne]);
287
+ const isOverflowing = resolvedValues.length > maxChips;
288
+ const selectedTooltip = useMemo(() => resolvedValues.map((o) => String(o.label)).join(', '), [resolvedValues]);
289
+ const atCap = typeof maxSelections === 'number' && selectedIds.length >= maxSelections;
290
+ // ── noOptionsText ─────────────────────────────────────────────────────────
291
+ const noOptionsText = useMemo(() => {
292
+ if (atCap) {
293
+ return `Maximum ${maxSelections} selected — remove one to add another`;
294
+ }
295
+ if (searchQuery.length < minSearchLength) {
296
+ return `Type at least ${minSearchLength} character${minSearchLength === 1 ? '' : 's'} to search`;
297
+ }
298
+ return fetchError ?? 'No results found';
299
+ }, [atCap, maxSelections, searchQuery, minSearchLength, fetchError]);
300
+ const chipSx = useMemo(() => ({
301
+ mr: 0.5,
302
+ height: 20,
303
+ fontSize: '10px',
304
+ borderRadius: '10px',
305
+ '& .MuiChip-label': { px: 0.75, py: 0 },
306
+ }), []);
307
+ // ── Render ────────────────────────────────────────────────────────────────
308
+ return (jsx(Controller, { control: control, name: name, render: ({ field: { ref, onBlur } }) => (jsxs(Fragment, { children: [renderLabel(variant, props), jsx(Autocomplete, { ref: ref, fullWidth: true, multiple: true, disableCloseOnSelect: true, open: open, onOpen: () => setOpen(true), onClose: () => setOpen(false), value: resolvedValues, inputValue: displayText, onInputChange: handleInputChange, onChange: handleChange, onBlur: onBlur, options: options, getOptionLabel: getOptionLabel, isOptionEqualToValue: isOptionEqualToValue, getOptionDisabled: (option) => {
309
+ if (option?.disable)
310
+ return true;
311
+ // At the cap, only already-selected options stay pickable (so
312
+ // the user can still deselect); everything else disables.
313
+ if (atCap)
314
+ return !selectedIds.includes(String(option.value));
315
+ return false;
316
+ },
317
+ // Options come pre-filtered from the server — disable client-side filtering
318
+ filterOptions: (x) => x, loading: isLoading, disabled: disable, size: "small", autoHighlight: true, selectOnFocus: true, clearOnBlur: false, noOptionsText: noOptionsText, sx: {
319
+ '& .MuiAutocomplete-input': {
320
+ padding: '0px 0px 0px 5px !important',
321
+ },
322
+ '& .MuiAutocomplete-inputRoot': {
323
+ flexWrap: 'nowrap',
324
+ alignItems: 'center',
325
+ },
326
+ // MUI docks its clear/dropdown-arrow group flush against the right
327
+ // edge via `position: absolute; right: 9px`. Shifting it left makes
328
+ // room there for the info icon, which is rendered as its own
329
+ // absolutely-positioned adornment docked at the true right edge —
330
+ // safer than fighting MUI's internal layout of the clear/arrow pair.
331
+ '& .MuiAutocomplete-endAdornment': {
332
+ right: '32px !important',
333
+ },
334
+ '& .MuiOutlinedInput-root': {
335
+ paddingRight: '52px !important',
336
+ },
337
+ '& .css-erkti9-MuiFormLabel-root-MuiInputLabel-root': {
338
+ top: '-5px',
339
+ },
340
+ ...itemSx,
341
+ }, slotProps: {
342
+ popper: { sx: { zIndex: 20000 } },
343
+ listbox: { sx: { fontSize: '11px' } },
344
+ }, PaperComponent: ({ children, ...paperProps }) => (jsx(Paper, { elevation: 4, ...paperProps, children: children })), renderOption: (renderProps, option, state) => {
345
+ const { key, ...otherProps } = renderProps;
346
+ return (jsxs("li", { ...otherProps, style: { fontSize: '11px' }, children: [jsx(Checkbox, { checked: state.selected, size: "small", sx: { mr: 1, p: '2px' } }), option.label] }, key));
347
+ }, renderTags: (tagValue, getTagProps) => (jsx(Box, { ref: tagsContainerRef, sx: {
348
+ display: 'flex',
349
+ flexWrap: 'nowrap',
350
+ overflow: 'hidden',
351
+ alignItems: 'center',
352
+ '& :hover': { cursor: 'pointer' },
353
+ }, children: isOverflowing ? (
354
+ // ── Collapsed chip: shows "N selected" with tooltip ──────
355
+ jsx(Tooltip, { title: selectedTooltip, placement: "top", children: jsx(Chip, { size: "small", label: `${tagValue.length} selected`, onClick: () => setOpen(true), onDelete: handleClearAll, clickable: true, sx: chipSx }) })) : (
356
+ // ── Individual chips: up to maxChips items ───────────────
357
+ tagValue.map((option, index) => {
358
+ const { key: tagKey, ...tagProps } = getTagProps({ index });
359
+ return (jsx(Tooltip, { title: String(option?.label ?? option), placement: "top", children: jsx(Chip, { ...tagProps, size: "small", label: String(option?.label ?? option), sx: chipSx }) }, tagKey ?? String(option?.value ?? option?.label ?? index)));
360
+ })) })), renderInput: (params) => (jsx(TextField, { ...params, error: isShowBorderError, label: variant !== 'standard' && labelVariant !== 'static' ? `${label}${required ? ' *' : ''}` : '', placeholder: placeholder, inputProps: {
361
+ ...params.inputProps,
362
+ onKeyDown: (e) => {
363
+ params.inputProps.onKeyDown?.(e);
364
+ handleInputKeyDown(e);
365
+ },
366
+ 'aria-labelledby': `${name}-label`,
367
+ 'aria-describedby': `${name}-helper ${name}-error`,
368
+ 'aria-required': required ? true : undefined,
369
+ }, InputProps: {
370
+ ...params.InputProps,
371
+ endAdornment: (jsxs(Fragment, { children: [params.InputProps.endAdornment, resolvedValues.length > 0 && (jsx(InputAdornment, { position: "end", sx: { position: 'absolute', right: '2px', top: '50%', transform: 'translateY(-50%)' }, children: jsx(IconButton, { size: "small", edge: "end", onClick: (e) => setInfoAnchorEl(e.currentTarget), "aria-label": `View ${resolvedValues.length} selected item${resolvedValues.length === 1 ? '' : 's'}`, children: jsx(InfoOutlinedIcon, { fontSize: "small" }) }) }))] })),
372
+ } })) }), jsx(Popover, { open: Boolean(infoAnchorEl), anchorEl: infoAnchorEl, onClose: () => setInfoAnchorEl(null), anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'right' }, slotProps: { paper: { sx: { zIndex: 20001 } } }, children: jsx(List, { dense: true, sx: { minWidth: 220, maxWidth: 320, maxHeight: 300, overflow: 'auto', py: 0.5 }, children: resolvedValues.length === 0 ? (jsx(ListItem, { children: jsx(ListItemText, { primaryTypographyProps: { fontSize: '12px' }, primary: "No items selected" }) })) : (resolvedValues.map((opt) => (jsx(ListItem, { secondaryAction: jsx(IconButton, { size: "small", edge: "end", onClick: () => handleRemoveOne(opt.value), "aria-label": `Remove ${opt.label}`, children: jsx(CloseIcon, { sx: { fontSize: 14 } }) }), sx: { py: 0.25 }, children: jsx(ListItemText, { primaryTypographyProps: { fontSize: '12px' }, primary: opt.label }) }, String(opt.value))))) }) }), typeof maxSelections === 'number' && (jsxs(Typography, { variant: "caption", color: atCap ? 'error' : 'text.secondary', sx: { mt: 0.25, display: 'block' }, children: [selectedIds.length, "/", maxSelections, " selected", atCap ? ' — maximum reached' : ''] })), helperText && (jsx(Typography, { variant: "caption", color: "primary", sx: { mt: 0.25, display: 'block' }, children: helperText })), fetchError && (jsx(Typography, { variant: "caption", color: "error", sx: { mt: 0.25, display: 'block' }, children: fetchError })), jsx(FormBottomField, { ...props })] })) }));
373
+ };
374
+
375
+ export { MultiSelectSearchApi as default };
@@ -76,7 +76,7 @@ const SingleSelectSearchApi = ({ props, variant, minSearchLength = 3, debounceMs
76
76
  // ── Stable references from props ──────────────────────────────────────────
77
77
  const { control, errors, item, setValue, clearErrors } = props;
78
78
  const { name, label, required, disable, placeholder, helperText, autoFIll: autoFill, // match the (typo'd) field name in FormSectionPropsItem
79
- setLoadList, api, jwtHeader, searchValue: searchParamKey, sx: itemSx, } = item;
79
+ setLoadList, api, jwtHeader, searchValue: searchParamKey, sx: itemSx, labelVariant, } = item;
80
80
  // ── Form value (reactive, no function call in deps) ───────────────────────
81
81
  const formValue = useWatch({ control, name });
82
82
  const isError = !!errors?.[name];
@@ -259,7 +259,7 @@ const SingleSelectSearchApi = ({ props, variant, minSearchLength = 3, debounceMs
259
259
  },
260
260
  ],
261
261
  },
262
- }, children: jsx(TextField, { ...params, error: isError && !formValue, label: variant !== 'standard'
262
+ }, children: jsx(TextField, { ...params, error: isError && !formValue, label: variant !== 'standard' && labelVariant !== 'static'
263
263
  ? `${label}${required ? ' *' : ''}`
264
264
  : '', placeholder: placeholder, inputProps: {
265
265
  ...params.inputProps,
@@ -6,6 +6,7 @@ export { FormCheckBox } from '../CheckBox';
6
6
  export { default as SingleSelect } from './Select/SingleSelect';
7
7
  export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete';
8
8
  export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi';
9
+ export { default as MultiSelectSearchApi } from './Select/MultiSelectSearchApi';
9
10
  export { default as MultiSelectV1 } from './Select/MultiSelectv1';
10
11
  export { default as MultiSelectTable } from './Select/MultiSelectTable';
11
12
  export { default as SearchableSelect } from './Select/SearchableSelect';
@@ -7,6 +7,7 @@ export { default as FormCheckBox } from './FormCheckBox/FormCheckBox.esm.js';
7
7
  export { default as SingleSelect } from './Select/SingleSelect.esm.js';
8
8
  export { default as SingleSelectNonAutoComplete } from './Select/SingleSelectNonAutoComplete.esm.js';
9
9
  export { default as SingleSelectSearchApi } from './Select/SingleSelectSearchApi.esm.js';
10
+ export { default as MultiSelectSearchApi } from './Select/MultiSelectSearchApi.esm.js';
10
11
  export { default as MultiSelectV1 } from './Select/MultiSelectv1.esm.js';
11
12
  export { default as MultiSelectTable } from './Select/MultiSelectTable.esm.js';
12
13
  export { default as SearchableSelect } from './Select/SearchableSelect.esm.js';
@@ -1,6 +1,7 @@
1
1
  export { default as SingleSelect } from '../FormComponents/Select/SingleSelect';
2
2
  export { default as SingleSelectNonAutoComplete } from '../FormComponents/Select/SingleSelectNonAutoComplete';
3
3
  export { default as SingleSelectSearchApi } from '../FormComponents/Select/SingleSelectSearchApi';
4
+ export { default as MultiSelectSearchApi } from '../FormComponents/Select/MultiSelectSearchApi';
4
5
  export { default as MultiSelectV1 } from '../FormComponents/Select/MultiSelectv1';
5
6
  declare const _default: {};
6
7
  export default _default;
@@ -1,6 +1,7 @@
1
1
  export { default as SingleSelect } from '../FormComponents/Select/SingleSelect.esm.js';
2
2
  export { default as SingleSelectNonAutoComplete } from '../FormComponents/Select/SingleSelectNonAutoComplete.esm.js';
3
3
  export { default as SingleSelectSearchApi } from '../FormComponents/Select/SingleSelectSearchApi.esm.js';
4
+ export { default as MultiSelectSearchApi } from '../FormComponents/Select/MultiSelectSearchApi.esm.js';
4
5
  export { default as MultiSelectV1 } from '../FormComponents/Select/MultiSelectv1.esm.js';
5
6
 
6
7
  var index = {};
@@ -1,4 +1,4 @@
1
- import { __exports as Calendar } from '../../../../../_virtual/index.esm7.js';
1
+ import { __exports as Calendar } from '../../../../../_virtual/index.esm6.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDayCell } from '../DayCell/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateInput } from '../../../../../_virtual/index.esm14.js';
1
+ import { __exports as DateInput } from '../../../../../_virtual/index.esm12.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireClassnames } from '../../../../classnames/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm6.js';
1
+ import { __exports as DateRangePicker } from '../../../../../_virtual/index.esm7.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDateRange } from '../DateRange/index.esm.js';
@@ -1,4 +1,4 @@
1
- import { __exports as InputRangeField } from '../../../../../_virtual/index.esm12.js';
1
+ import { __exports as InputRangeField } from '../../../../../_virtual/index.esm13.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
 
@@ -1,4 +1,4 @@
1
- import { __exports as Month } from '../../../../../_virtual/index.esm13.js';
1
+ import { __exports as Month } from '../../../../../_virtual/index.esm14.js';
2
2
  import require$$0__default from 'react';
3
3
  import { __require as requirePropTypes } from '../../../../prop-types/index.esm.js';
4
4
  import { __require as requireDayCell } from '../DayCell/index.esm.js';
@@ -11,7 +11,7 @@ export interface TextFieldInputProps {
11
11
  export interface FormSectionPropsItem {
12
12
  name: string;
13
13
  label: string;
14
- inputType: 'text' | 'password' | 'number' | 'select' | 'autocomplete-select' | 'autocomplete-multi-select' | 'single-select-search' | 'datepicker' | 'datetimepicker' | 'multiselect' | 'select-v2' | 'basic-select' | 'searchable-select' | 'decimal' | 'alpha-numerical' | 'yearpicker' | 'dateRangePicker' | 'monthpicker' | 'monthpicker-multi' | 'multiselect' | 'file' | 'multifile' | 'multifile-with-preview' | 'textarea' | 'phoneNumber' | 'pincode' | 'email' | 'toggleSwitch' | 'rich-text-editor' | 'multiEmail' | 'timepicker' | 'checkbox-group' | 'radio-group' | 'checkbox' | 'custom' | 'input-select' | 'date-range-picker' | 'multiselect-table' | '';
14
+ inputType: 'text' | 'password' | 'number' | 'select' | 'autocomplete-select' | 'autocomplete-multi-select' | 'single-select-search' | 'multi-select-search' | 'datepicker' | 'datetimepicker' | 'multiselect' | 'select-v2' | 'basic-select' | 'searchable-select' | 'decimal' | 'alpha-numerical' | 'yearpicker' | 'dateRangePicker' | 'monthpicker' | 'monthpicker-multi' | 'multiselect' | 'file' | 'multifile' | 'multifile-with-preview' | 'textarea' | 'phoneNumber' | 'pincode' | 'email' | 'toggleSwitch' | 'rich-text-editor' | 'multiEmail' | 'timepicker' | 'checkbox-group' | 'radio-group' | 'checkbox' | 'custom' | 'input-select' | 'date-range-picker' | 'multiselect-table' | '';
15
15
  options?: OptionsProps[];
16
16
  required?: boolean;
17
17
  errorMessage?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tp-react-elements",
3
- "version": "3.0.16",
3
+ "version": "3.0.18",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "React form components library built with React Hook Form and Yup",