react-core-ts 1.9.17 → 2.0.0

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.
@@ -0,0 +1,1244 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+ import ReactDOM from 'react-dom';
10
+
11
+ import { AutoSuggestionInputProps, TabPops, ValueProps } from './commontypes';
12
+ import DropdownList from './components/DropdownList';
13
+ import { useSuggestions } from './utilities/autosuggestions';
14
+ import { debounce } from './utilities/debounce';
15
+ import { deepEqual } from './utilities/deepEqual';
16
+ import { filterSuggestions } from './utilities/filterSuggestions';
17
+ import { AllDropArrow, DropArrow, Search, Spinner, Tools } from './utilities/icons';
18
+ import { default as Tooltip } from './utilities/NewTooltip';
19
+ import { getKeyValue, safeToLowerString } from './utilities/getKeyValue';
20
+ import DropdownFilterTabs from './dropdownFilterTabs';
21
+
22
+ const AutoCompleteWithTabFilter = forwardRef<
23
+ HTMLInputElement,
24
+ AutoSuggestionInputProps
25
+ >(
26
+ (
27
+ {
28
+ label,
29
+ onChange,
30
+ getData = async () => [],
31
+ data = [],
32
+ errors,
33
+ required = false,
34
+ name,
35
+ fullWidth = false,
36
+ placeholder,
37
+ id,
38
+ type = 'custom_select',
39
+ selectedItems: propsSeelctedItems = [],
40
+ readOnly = false,
41
+ disabled = false,
42
+ value,
43
+ searchValue: propsSearchValue = '',
44
+ isMultiple = false,
45
+ desc = 'name',
46
+ descId = 'id',
47
+ singleSelect,
48
+ className,
49
+ async = false,
50
+ paginationEnabled,
51
+ initialLoad = false,
52
+ actionLabel,
53
+ handleAction,
54
+ nextBlock,
55
+ notDataMessage,
56
+ initialDataMessage,
57
+ onFocus,
58
+ onSearchValueChange,
59
+ expandable = false,
60
+ textCount = 10,
61
+ itemCount = 1,
62
+ scrollRef,
63
+ countOnly = true,
64
+ typeOnlyFetch = false,
65
+ tab = [],
66
+ clearTabSwitch = false,
67
+ selectedRowLimit = 2,
68
+ topMargin = 0,
69
+ currentTab = 0,
70
+ selectedLabel = '',
71
+ viewMode = false,
72
+ handleUpdateParent,
73
+ shortCode = '',
74
+ labelCode = '',
75
+ tabInlineSearch = true,
76
+ matchFromStart = false,
77
+ autoDropdown = false,
78
+ enableSelectAll = false,
79
+
80
+
81
+ tabName,
82
+ tabDesc = 'tabName',
83
+ tabDescId = 'tabId',
84
+ tabSelectedItems = [],
85
+ getTabData,
86
+ tabData,
87
+ toolTabs = [],
88
+ enableToolsTab = true,
89
+ toolTabClearSwitch = false,
90
+ onToolTabChange,
91
+ autoRefreshMainDropdown = true,
92
+ },
93
+ ref
94
+ ) => {
95
+ const dropdownRef = useRef<HTMLDivElement>(null);
96
+ const selItemRef = useRef<HTMLDivElement>(null);
97
+ const itemsRef = useRef<HTMLDivElement>(null);
98
+ const dropdownSelectedRef = useRef<HTMLDivElement>(null);
99
+ const tabRef = useRef<HTMLUListElement>(null);
100
+ const dropLevelRef = useRef<string>('bottom');
101
+ // State Hooks Section
102
+ const [isInitialRender, setIsInitialRender] = useState(true);
103
+ const [inputValue, setInputValue] = useState<string>(value);
104
+ const [searchValue, setSearchValue] = useState<string>(propsSearchValue);
105
+ const [searchOldValue, setSearchOldValue] = useState<string>('');
106
+ const [nextPage, setNextPage] = useState<number | undefined>(1);
107
+ const [dropOpen, setDropOpen] = useState<boolean>(false);
108
+ const [selectedItems, setSelectedItems] = useState<ValueProps[]>([]);
109
+ const [searchLoaded, setSearchLoaded] = useState<boolean>(propsSearchValue ? false : true);
110
+ const [showAllSelected, setShowAllSelected] = useState<boolean>(false);
111
+ const [selHeight, setSelHeight] = useState<number>(184);
112
+ const [expandArrowClick, setExpandArrowClick] = useState<
113
+ number | undefined
114
+ >(1);
115
+ const [visible, setVisible] = useState<boolean>(false);
116
+ const [activeTab, setActiveTab] = useState<number>(currentTab);
117
+ const [focusedIndex, setFocusedIndex] = useState(0);
118
+ const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
119
+ // API call for suggestions through a custom hook
120
+ const inputRef = useRef(null);
121
+ const dropRef = useRef(null);
122
+ const inputSearchRef = useRef(null);
123
+ const dropBtnRef = useRef<HTMLButtonElement>(null);
124
+ useImperativeHandle(ref, () => inputRef.current);
125
+ const [dropdownStyle, setDropdownStyle] = useState({
126
+ top: 0,
127
+ left: 0,
128
+ width: 200,
129
+ transform: '',
130
+ });
131
+
132
+ const [selectAll, setSelectAll] = useState(false);
133
+ const [refetchData, setRefetchData] = useState(false);
134
+ const [allDataLoaded, setAllDataLoaded] = useState(false);
135
+ const toolsBtnRef = useRef<HTMLButtonElement>(null);
136
+ const [showToolsTab, setShowToolsTab] = useState(false);
137
+ // Track previous tabSelectedItems to detect new items
138
+ const prevTabSelectedItemsRef = useRef<ValueProps[]>([]);
139
+
140
+ let originalOverflow = '';
141
+ let hasScrollbar = false;
142
+
143
+ const { suggestions, isLoading, handlePickSuggestions, resetSuggections } =
144
+ useSuggestions(
145
+ getData,
146
+ data,
147
+ dropOpen,
148
+ async,
149
+ paginationEnabled,
150
+ initialLoad,
151
+ inputValue,
152
+ isMultiple,
153
+ setNextPage,
154
+ selectedItems,
155
+ typeOnlyFetch
156
+ // nextBlock
157
+ );
158
+
159
+ // Handling the selection of a suggestion
160
+ const handleSuggestionClick = useCallback(
161
+ (suggestion: ValueProps, idx: number) => {
162
+ if (isMultiple) {
163
+ setFocusedIndex(idx);
164
+ setSelectedItems((prev) => {
165
+ const isAdded =
166
+ prev && prev.length > 0
167
+ ? prev.some(
168
+ (item) =>
169
+ getKeyValue(item, descId, 'id') ===
170
+ getKeyValue(suggestion, descId, 'id')
171
+ )
172
+ : false;
173
+ if (isAdded) {
174
+ return prev.filter(
175
+ (item) =>
176
+ getKeyValue(item, descId, 'id') !==
177
+ getKeyValue(suggestion, descId, 'id')
178
+ );
179
+ } else {
180
+ return [...prev, suggestion];
181
+ }
182
+ });
183
+ } else {
184
+ setFocusedIndex(idx);
185
+ setInputValue(String(suggestion[desc]));
186
+ setSearchValue('');
187
+ onChange(suggestion);
188
+ setDropOpen(false);
189
+ setShowToolsTab(false);
190
+ }
191
+ },
192
+ [tab, activeTab, descId, desc, onChange]
193
+ );
194
+
195
+ // Adding debounce to avoid making API calls on every keystroke
196
+ const debouncedUpdate = useCallback(
197
+ debounce((value: string, tabVal?: number | string) => {
198
+ setSearchOldValue(value);
199
+ handlePickSuggestions(value, 1, false, tabVal);
200
+ }, 500),
201
+ []
202
+ );
203
+
204
+ const handleChangeWithDebounce = (value: string) => {
205
+ if ((type === 'auto_complete' || type === 'auto_suggestion') && async) {
206
+ const activeTabVal = tab.length > 0 ? tab?.[activeTab].id : undefined;
207
+ debouncedUpdate(value, activeTabVal);
208
+ }
209
+ };
210
+
211
+ const handleMultiSelect = useCallback((e: any, suggestion: ValueProps) => {
212
+ const { checked } = e.target;
213
+ if (isMultiple) {
214
+ if (checked) {
215
+ setSelectedItems((prev) => [...prev, suggestion]);
216
+ } else {
217
+ setSelectedItems((prev) => {
218
+ return prev.filter(
219
+ (item, i) =>
220
+ getKeyValue(item, descId, 'id') !==
221
+ getKeyValue(suggestion, descId, 'id')
222
+ );
223
+ });
224
+ }
225
+ } else {
226
+ if (checked) {
227
+ setInputValue(String(suggestion[desc]));
228
+ onChange(suggestion);
229
+ } else {
230
+ setInputValue('');
231
+ }
232
+ }
233
+ }, [isMultiple, descId, desc, onChange]);
234
+ useEffect(() => {
235
+ // Only update if different
236
+ if (!deepEqual(selectedItems, propsSeelctedItems)) {
237
+ setSelectedItems(propsSeelctedItems);
238
+ }
239
+ // eslint-disable-next-line react-hooks/exhaustive-deps
240
+ }, [propsSeelctedItems]);
241
+ // Effect to set the input value whenever `value` prop changes
242
+ useEffect(() => {
243
+ setInputValue(value ?? '');
244
+ }, [value]);
245
+
246
+ const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
247
+ const { value } = e.target;
248
+ //setVisibleDrop();
249
+ setSearchValue(value);
250
+ handleChangeWithDebounce(value);
251
+
252
+ if (!tabInlineSearch) {
253
+ onSearchValueChange?.(value);
254
+ }
255
+
256
+ if (!value) {
257
+ if (!typeOnlyFetch) setInputValue('');
258
+ // onChange({ [descId]: '', [desc]: '' });
259
+ }
260
+
261
+ if (!tabInlineSearch) {
262
+ setDropOpen(true);
263
+ setShowToolsTab(false);
264
+ }
265
+ }, [handleChangeWithDebounce, tabInlineSearch, onSearchValueChange, typeOnlyFetch]);
266
+
267
+ const handleSuggestionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
268
+ const { value } = e.target;
269
+ //setVisibleDrop();
270
+ const trimStart = value.trimStart();
271
+ setSearchValue(trimStart);
272
+ handleChangeWithDebounce(trimStart);
273
+ setAllDataLoaded(false);
274
+ }, [handleChangeWithDebounce]);
275
+
276
+ const handleClearSelected = useCallback(() => {
277
+ setSelectedItems([]);
278
+ setShowAllSelected(false);
279
+ if (searchValue && type === 'auto_suggestion' && tabInlineSearch) {
280
+ setSearchValue('');
281
+ }
282
+ if (isMultiple) onChange([]);
283
+ else onChange({ [descId]: '', [desc]: '' });
284
+ if (async && type === 'auto_suggestion' && tabInlineSearch) resetSuggections?.();
285
+ }, [searchValue, type, tabInlineSearch, isMultiple, onChange, descId, desc, async, resetSuggections]);
286
+
287
+ const generateClassName = useCallback(() => {
288
+ return `qbs-textfield-default ${className} ${errors && errors?.message ? 'textfield-error' : 'textfield'
289
+ } ${expandable ? 'expandable' : ''}`;
290
+ }, [errors, name]);
291
+ const handleRemoveSelectedItem = useCallback((index: number) => {
292
+ setSelectedItems((prev) => {
293
+ return prev.filter((_, i) => i !== index);
294
+ });
295
+ }, []);
296
+
297
+ const uniqueDropArrowId = `${name}-drop-arrow-selected-list-icon`;
298
+ useEffect(() => {
299
+ const handleClickOutside = (event: any) => {
300
+ if (
301
+ dropRef.current &&
302
+ event.target instanceof Node &&
303
+ !dropRef.current.contains(event.target) &&
304
+ event.target?.id !== uniqueDropArrowId &&
305
+ !event.target?.closest('.tools-drop-btn')
306
+ ) {
307
+ setTimeout(() => {
308
+ setDropOpen(false);
309
+ setShowToolsTab(false);
310
+ if (!typeOnlyFetch) setSearchValue('');
311
+ }, 200);
312
+ //setSearchValue("");
313
+ }
314
+ };
315
+
316
+ document.addEventListener('mousedown', handleClickOutside as any);
317
+ window.addEventListener('scroll', handleClickOutside as any);
318
+
319
+ const scrollableDivs = document.querySelectorAll(
320
+ 'div[style*="overflow"], .overflow-auto, .overflow-y-auto, .overflow-x-auto'
321
+ );
322
+ scrollableDivs.forEach((div) =>
323
+ div.addEventListener('scroll', handleClickOutside as any)
324
+ );
325
+ if (scrollRef && scrollRef.current && scrollRef.current !== null) {
326
+ scrollRef.current.addEventListener('scroll', handleClickOutside as any);
327
+ }
328
+ return () => {
329
+ document.removeEventListener('mousedown', handleClickOutside as any);
330
+ window.removeEventListener('scroll', handleClickOutside as any);
331
+ scrollableDivs.forEach((div) =>
332
+ div.removeEventListener('scroll', handleClickOutside as any)
333
+ );
334
+ if (scrollRef && scrollRef.current && scrollRef.current !== null) {
335
+ scrollRef.current.removeEventListener(
336
+ 'scroll',
337
+ handleClickOutside as any
338
+ );
339
+ }
340
+ };
341
+ }, []);
342
+
343
+ // Filtering suggestions based on type and search value
344
+ const selected: any = isMultiple ? selectedItems : inputValue;
345
+ const filteredData: ValueProps[] = filterSuggestions(
346
+ suggestions,
347
+ searchValue,
348
+ type,
349
+ desc,
350
+ selected,
351
+ async,
352
+ false,
353
+ true,
354
+ matchFromStart
355
+ );
356
+
357
+ const adjustDropdownPosition = useCallback(() => {
358
+ if (dropdownRef.current) {
359
+ const inputRect = dropdownRef.current.getBoundingClientRect();
360
+ const dropdownPosition = {
361
+ left: inputRect.left + window.scrollX,
362
+ width: inputRect.width,
363
+ top: 0,
364
+ };
365
+ // Check if there's enough space below the input for the dropdown
366
+ const spaceBelow = window.innerHeight - inputRect.bottom;
367
+ const spaceAbove = inputRect.top + window.scrollY;
368
+
369
+ let dropdownHeight = viewMode ? 160 : 300; // Assume a fixed height or calculate based on content
370
+ const defaultHeight = 184;
371
+
372
+ if (countOnly) {
373
+ if (dropdownSelectedRef?.current)
374
+ dropdownHeight += dropdownSelectedRef?.current?.clientHeight;
375
+ }
376
+ if (tab.length > 0) {
377
+ if (tabRef?.current) dropdownHeight += tabRef?.current?.clientHeight;
378
+ }
379
+ if (spaceBelow >= dropdownHeight) {
380
+ dropLevelRef.current = 'bottom';
381
+ dropdownPosition.top =
382
+ inputRect.top + window.scrollY + inputRect.height;
383
+
384
+ setSelHeight(prev => prev !== defaultHeight ? defaultHeight : prev);
385
+ } else if (spaceAbove >= dropdownHeight) {
386
+ let sHeight = tabInlineSearch ? 73 : 113;
387
+ sHeight =
388
+ sHeight -
389
+ (filteredData?.length > 0 && enableSelectAll && isMultiple
390
+ ? 32
391
+ : 0);
392
+ dropLevelRef.current = 'top';
393
+ dropdownPosition.top =
394
+ inputRect.top +
395
+ window.scrollY -
396
+ dropdownHeight +
397
+ sHeight +
398
+ topMargin;
399
+
400
+ setSelHeight(prev => prev !== defaultHeight ? defaultHeight : prev);
401
+ } else {
402
+ dropLevelRef.current = 'bottom';
403
+ dropdownPosition.top =
404
+ inputRect.top + window.scrollY + inputRect.height;
405
+
406
+ if (itemsRef?.current) {
407
+ const totalH = showAllSelected ? 148 : defaultHeight;
408
+ const selH = totalH - (dropdownHeight - spaceBelow);
409
+ const newHeight = selH < 50 ? 50 : selH;
410
+ setSelHeight(prev => prev !== newHeight ? newHeight : prev);
411
+ }
412
+ }
413
+ // setDropdownStyle({
414
+ // ...dropdownPosition,
415
+ // });
416
+ setDropdownStyle((prevStyle) => {
417
+ const newStyle = {
418
+ ...prevStyle,
419
+ transform: `translateY(${dropdownPosition.top}px)`,
420
+ left: inputRect.left + window.scrollX,
421
+ width: inputRect.width,
422
+ };
423
+ // Only update if the style actually changed
424
+ if (prevStyle.transform !== newStyle.transform ||
425
+ prevStyle.left !== newStyle.left ||
426
+ prevStyle.width !== newStyle.width) {
427
+ return newStyle;
428
+ }
429
+ return prevStyle;
430
+ });
431
+ }
432
+ }, [viewMode, countOnly, tab.length, tabInlineSearch, filteredData?.length, enableSelectAll, isMultiple, topMargin, showAllSelected]);
433
+
434
+ useEffect(() => {
435
+ window.addEventListener('resize', adjustDropdownPosition);
436
+ adjustDropdownPosition();
437
+
438
+ return () => {
439
+ window.removeEventListener('resize', adjustDropdownPosition);
440
+ };
441
+ }, [adjustDropdownPosition]);
442
+
443
+ useEffect(() => {
444
+ if (isInitialRender) {
445
+ setIsInitialRender(false);
446
+ } else {
447
+ onChange(selectedItems);
448
+ }
449
+ adjustDropdownPosition();
450
+ setTimeout(() => {
451
+ adjustDropdownPosition();
452
+ }, 200);
453
+ // eslint-disable-next-line react-hooks/exhaustive-deps
454
+ }, [selectedItems, adjustDropdownPosition]);
455
+
456
+ useEffect(() => {
457
+ adjustDropdownPosition();
458
+ setTimeout(() => {
459
+ adjustDropdownPosition();
460
+ }, 200);
461
+ // eslint-disable-next-line react-hooks/exhaustive-deps
462
+ }, [filteredData, adjustDropdownPosition]);
463
+
464
+ useEffect(() => {
465
+ // Handle keyboard navigation
466
+ const handleKeyDown = (e: any) => {
467
+ if (!dropOpen) return;
468
+
469
+ const atBottom = focusedIndex === filteredData.length - 1;
470
+ const atTop = focusedIndex === 0;
471
+ switch (e.key) {
472
+ case 'ArrowDown':
473
+ e.preventDefault();
474
+ if (itemsRef.current) {
475
+ if (!atBottom) {
476
+ itemsRef.current.scrollTop +=
477
+ itemRefs.current[focusedIndex]?.offsetHeight || 50;
478
+ } else {
479
+ itemsRef.current.scrollTop = 0;
480
+ }
481
+ }
482
+ setFocusedIndex((prev) => (prev + 1) % filteredData?.length);
483
+ break;
484
+
485
+ case 'ArrowUp':
486
+ e.preventDefault();
487
+ if (itemsRef.current) {
488
+ if (!atTop) {
489
+ itemsRef.current.scrollTop -=
490
+ itemRefs.current[focusedIndex]?.offsetHeight || 50;
491
+ } else {
492
+ itemsRef.current.scrollTop = 0;
493
+ }
494
+ }
495
+ setFocusedIndex(
496
+ (prev) => (prev - 1 + filteredData?.length) % filteredData?.length
497
+ );
498
+ break;
499
+
500
+ case 'Enter':
501
+ e.preventDefault();
502
+ if (filteredData && filteredData.length > 0)
503
+ handleSuggestionClick(filteredData[focusedIndex], focusedIndex);
504
+ break;
505
+
506
+ case 'Escape':
507
+ e.preventDefault();
508
+ setDropOpen(false);
509
+ break;
510
+
511
+ default:
512
+ break;
513
+ }
514
+ };
515
+
516
+ window.addEventListener('keydown', handleKeyDown);
517
+
518
+ return () => {
519
+ window.removeEventListener('keydown', handleKeyDown);
520
+ };
521
+ }, [filteredData, dropOpen, isLoading, focusedIndex]);
522
+
523
+ const isSelected = (
524
+ item: ValueProps,
525
+ selectedItems: ValueProps[] | string
526
+ ): boolean => {
527
+ if (Array.isArray(selectedItems)) {
528
+ return selectedItems.some(
529
+ (selectedItem) =>
530
+ getKeyValue(selectedItem, desc, 'name') === getKeyValue(item, desc, 'name') ||
531
+ getKeyValue(selectedItem, descId, 'id') === getKeyValue(item, descId, 'id')
532
+ );
533
+ } else {
534
+ return (
535
+ getKeyValue(item, desc, 'name') === safeToLowerString(selectedItems) ||
536
+ getKeyValue(item, descId, 'id') === safeToLowerString(selectedItems)
537
+ );
538
+ }
539
+ };
540
+
541
+ const handleLoadMore = () => {
542
+ if (paginationEnabled) {
543
+ handlePickSuggestions(searchValue, nextPage + 1, true);
544
+ setNextPage(nextPage + 1);
545
+ }
546
+ };
547
+
548
+ const handleOnClick = () => {
549
+ (!disabled && !readOnly && !expandable && !dropOpen) || viewMode
550
+ ? setVisibleDrop()
551
+ : '';
552
+ };
553
+ const onInputFocus = () => {
554
+ if (!dropOpen) {
555
+ if (countOnly) {
556
+ setTimeout(() => {
557
+ if (inputSearchRef.current) inputSearchRef.current.focus();
558
+ }, 10);
559
+ } else {
560
+ if (inputRef.current) inputRef.current.focus();
561
+ }
562
+ // handleOnClick();
563
+ }
564
+ };
565
+
566
+ const handleShowAllSelected = useCallback((enabled: boolean) => {
567
+ setExpandArrowClick(expandArrowClick + 1);
568
+ if (enabled) {
569
+ setShowAllSelected(true);
570
+ } else {
571
+ setShowAllSelected(false);
572
+ }
573
+ }, [expandArrowClick]);
574
+
575
+ const handleCollapseArrowClick = useCallback(() => {
576
+ setExpandArrowClick(expandArrowClick + 1);
577
+ setShowAllSelected(false);
578
+ }, [expandArrowClick]);
579
+
580
+ const tooltipContent =
581
+ selectedItems?.length > itemCount
582
+ ? selectedItems
583
+ ?.slice(itemCount)
584
+ .map((item) => item[desc])
585
+ .join(', ')
586
+ : '';
587
+
588
+ const allSelectedTooltipContent =
589
+ selectedItems?.length > 0
590
+ ? selectedItems?.map((item) => item[desc])
591
+ .join(', ')
592
+ : '';
593
+
594
+ const setVisibleDrop = () => {
595
+ hasScrollbar = document.body.scrollHeight > window.innerHeight;
596
+ if (!originalOverflow && !hasScrollbar) {
597
+ originalOverflow = document.body.style.overflow;
598
+ document.body.style.overflow = 'hidden';
599
+ }
600
+
601
+ setVisible(false);
602
+ if (!dropOpen) {
603
+ setDropOpen(true);
604
+ setShowToolsTab(false);
605
+ }
606
+ setFocusedIndex(0);
607
+
608
+ setTimeout(() => {
609
+ setVisible(true);
610
+
611
+ if (!hasScrollbar) {
612
+ document.body.style.overflow = originalOverflow || '';
613
+ }
614
+ originalOverflow = '';
615
+ }, 200);
616
+ };
617
+
618
+ const handleTabClick = useCallback((index: number) => {
619
+ if (activeTab !== index) {
620
+ if (clearTabSwitch) {
621
+ handleClearSelected();
622
+ if (tabInlineSearch) {
623
+ setSearchValue('');
624
+ setInputValue('');
625
+ handlePickSuggestions('', 1);
626
+ } else {
627
+ resetSuggections?.();
628
+ const activeTabVal =
629
+ tab.length > 0 ? tab?.[index].id : undefined;
630
+ handlePickSuggestions(searchValue, 1, false, activeTabVal);
631
+ }
632
+ setAllDataLoaded(false);
633
+ } else if (!tabInlineSearch) {
634
+ resetSuggections?.();
635
+ const activeTabVal = tab.length > 0 ? tab?.[index].id : undefined;
636
+ handlePickSuggestions(searchValue, 1, false, activeTabVal);
637
+ }
638
+ setActiveTab(index);
639
+ }
640
+ }, [activeTab, clearTabSwitch, handleClearSelected, tabInlineSearch, tab, searchValue, handlePickSuggestions, resetSuggections]);
641
+
642
+ const getSelectedRowLimit = useCallback(() => {
643
+ let maxHeight = 36;
644
+ if (selItemRef?.current && selItemRef?.current.clientWidth < 300) {
645
+ maxHeight = 24;
646
+ }
647
+
648
+ return selectedRowLimit * maxHeight + 5;
649
+ }, [selectedRowLimit]);
650
+
651
+ const getSelectedItems = useCallback((dropdown: boolean) => {
652
+ return (
653
+ <div
654
+ ref={selItemRef}
655
+ className="selected-items-outer-container"
656
+ style={{ maxHeight: `${getSelectedRowLimit()}px` }}
657
+ >
658
+ {selectedItems
659
+ .slice(
660
+ 0,
661
+ selectedItems?.length > itemCount
662
+ ? showAllSelected
663
+ ? selectedItems?.length
664
+ : itemCount
665
+ : selectedItems?.length
666
+ )
667
+ .map((item, index) => (
668
+ <div key={index} className="selected-items-container">
669
+ <Tooltip
670
+ title={String(item?.[desc])}
671
+ enabled={String(item?.[desc])?.length > textCount}
672
+ >
673
+ <div key={item.id} className="selected-item">
674
+ {String(item?.[desc])?.length > textCount
675
+ ? `${String(item?.[desc]).substring(0, textCount)}...`
676
+ : String(item?.[desc])}
677
+ {!viewMode && (
678
+ <button
679
+ onClick={() => handleRemoveSelectedItem(index)}
680
+ className="remove-item-btn"
681
+ aria-label={`Remove ${String(item?.[desc])}`}
682
+ >
683
+ X
684
+ </button>
685
+ )}
686
+ </div>
687
+ </Tooltip>
688
+ </div>
689
+ ))}
690
+ {selectedItems?.length > itemCount && !showAllSelected && (
691
+ <div className="selected-items-container">
692
+ <Tooltip title={tooltipContent} enabled={true}>
693
+ <div
694
+ className={`selected-item-more qbs-rounded-full qbs-min-h-6 qbs-min-w-6 qbs-p-1 ${dropdown ? 'qbs-cursor-pointer' : ''
695
+ }`}
696
+ onClick={() => {
697
+ handleShowAllSelected(dropdown);
698
+ }}
699
+ >
700
+ +{selectedItems?.length - itemCount}{' '}
701
+ <span className="qbs-hidden">more</span>
702
+ </div>
703
+ </Tooltip>
704
+ </div>
705
+ )}
706
+ </div>
707
+ );
708
+ }, [selectedItems, getSelectedRowLimit, itemCount, showAllSelected, desc, textCount, viewMode, handleRemoveSelectedItem, tooltipContent, handleShowAllSelected]);
709
+
710
+ const getTabItems = useCallback(() => {
711
+ return (
712
+ <ul
713
+ className="qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2"
714
+ ref={tabRef}
715
+ >
716
+ {tab.map((item: TabPops, idx: number) => (
717
+ <li className="qbs-flex-1 qbs-tab-items" key={`tab-${idx}`}>
718
+ <span
719
+ className={`qbs-inline-block qbs-tab-item qbs-text-sm qbs-cursor-pointer qbs-w-full qbs-text-center qbs-p-1 qbs-border-b-2 ${activeTab === idx ? 'qbs-tab-active-item' : ''
720
+ }`}
721
+ onClick={() => {
722
+ handleTabClick(idx);
723
+ }}
724
+ >
725
+ {item.label}
726
+ </span>
727
+ </li>
728
+ ))}
729
+ </ul>
730
+ );
731
+ }, [tab, activeTab, handleTabClick]);
732
+
733
+ const handleSelectAll = useCallback(() => {
734
+ if (isMultiple) {
735
+ if (selectAll) {
736
+ setSelectedItems((prev) =>
737
+ prev.filter(
738
+ (item) =>
739
+ !filteredData.some(
740
+ (f) =>
741
+ getKeyValue(f, descId, 'id') === getKeyValue(item, descId, 'id')
742
+ )
743
+ )
744
+ );
745
+ } else {
746
+ // Add only items from current tab - batch all updates in single setState
747
+ setSelectedItems((prev) => {
748
+ const newItems: ValueProps[] = [];
749
+
750
+ filteredData.forEach((suggestion: ValueProps) => {
751
+ const isAdded = prev.some(
752
+ (item) =>
753
+ getKeyValue(item, descId, 'id') ===
754
+ getKeyValue(suggestion, descId, 'id')
755
+ );
756
+ if (!isAdded) {
757
+ newItems.push(suggestion);
758
+ }
759
+ });
760
+
761
+ return [...prev, ...newItems];
762
+ });
763
+ }
764
+ }
765
+ }, [isMultiple, selectAll, filteredData, descId]);
766
+
767
+ const handleOpenDropdown = useCallback((e: any) => {
768
+ if (!suggestions || suggestions?.length === 0 || refetchData) {
769
+ if (autoDropdown && (inputValue === '' || inputValue.trim() === '')) {
770
+ if (tabInlineSearch && tab.length > 0) {
771
+ const activeTabVal =
772
+ tab.length > 0 ? tab?.[activeTab].id : undefined;
773
+ handlePickSuggestions('*', 1, false, activeTabVal);
774
+ } else {
775
+ handlePickSuggestions('*', 1);
776
+ }
777
+ setRefetchData(false);
778
+ setAllDataLoaded(true);
779
+ } else if (!refetchData && inputValue !== '') {
780
+ if (tabInlineSearch && tab.length > 0) {
781
+ const activeTabVal =
782
+ tab.length > 0 ? tab?.[activeTab].id : undefined;
783
+ handlePickSuggestions(inputValue, 1, false, activeTabVal);
784
+ } else {
785
+ handlePickSuggestions(inputValue, 1);
786
+ }
787
+ setRefetchData(true);
788
+ setAllDataLoaded(false);
789
+ }
790
+ }
791
+ }, [suggestions, refetchData, autoDropdown, inputValue, tabInlineSearch, tab, activeTab, handlePickSuggestions]);
792
+
793
+ const handleOpenToolsTab = useCallback((e: any) => {
794
+ setShowToolsTab(showToolsTab ? false : true);
795
+ setDropOpen(false);
796
+ }, [showToolsTab]);
797
+
798
+ useEffect(() => {
799
+ const allSelected =
800
+ filteredData?.length > 0 && selectedItems?.length > 0 &&
801
+ filteredData.every((item) =>
802
+ selectedItems?.some(
803
+ (s) =>
804
+ getKeyValue(s, descId, 'id') === getKeyValue(item, descId, 'id')
805
+ )
806
+ );
807
+
808
+ setSelectAll(allSelected);
809
+ }, [filteredData, selectedItems, descId]);
810
+
811
+ useEffect(() => {
812
+ handleUpdateParent?.(dropOpen, dropLevelRef.current);
813
+ }, [dropOpen]);
814
+
815
+ // Auto-focus search input when dropdown opens with tabInlineSearch
816
+ useEffect(() => {
817
+ if (dropOpen && visible && tabInlineSearch && type === 'auto_suggestion' && !expandable) {
818
+ // Small delay to ensure the dropdown is fully rendered
819
+ setTimeout(() => {
820
+ if (inputSearchRef.current) {
821
+ inputSearchRef.current.focus();
822
+ }
823
+ }, 50);
824
+ }
825
+ }, [dropOpen, visible, tabInlineSearch, type, expandable]);
826
+
827
+ // Auto-refresh main dropdown data when tabSelectedItems change (add/remove)
828
+ useEffect(() => {
829
+ if (autoRefreshMainDropdown && dropOpen && async) {
830
+ // Check if there are changes by comparing with previous state
831
+ const hasChanges = tabSelectedItems.length !== prevTabSelectedItemsRef.current.length ||
832
+ !deepEqual(tabSelectedItems, prevTabSelectedItemsRef.current);
833
+
834
+ // Trigger refresh if there are changes (items added, removed, or modified)
835
+ if (hasChanges) {
836
+ if (tabInlineSearch && toolTabs.length > 0 && searchValue) {
837
+ handlePickSuggestions(searchValue, 1);
838
+ }
839
+ }
840
+
841
+ // Update the previous state
842
+ prevTabSelectedItemsRef.current = tabSelectedItems;
843
+ }
844
+ }, [autoRefreshMainDropdown, dropOpen, tabSelectedItems, activeTab, tab, tabInlineSearch, searchValue, async, handlePickSuggestions]);
845
+
846
+ return (
847
+ <div
848
+ id={id ? `selected-list-${id}` : `selected-list-${name}`}
849
+ className={fullWidth ? 'fullWidth' : 'autoWidth'}
850
+ ref={dropdownRef}
851
+ >
852
+ {label && (
853
+ <div
854
+ style={{
855
+ marginBottom: 5,
856
+ display: 'flex',
857
+ justifyContent: 'space-between',
858
+ }}
859
+ >
860
+ <label className={`labels label-text`}>
861
+ {label}
862
+ {required && <span className="text-error"> *</span>}
863
+ </label>
864
+ <span onClick={() => handleAction?.()} className={`action_label`}>
865
+ {actionLabel}
866
+ </span>
867
+ </div>
868
+ )}
869
+ {/* Displaying selected items for multi-select */}
870
+
871
+ <div
872
+ className={`qbs-relative qbs-autocomplete-selected-comp ${expandable ? 'qbs-expandable-container' : 'qbs-container'
873
+ }`}
874
+ >
875
+ {(selectedItems?.length > 0 || !tabInlineSearch) && (
876
+ <>
877
+ {!countOnly ? (
878
+ getSelectedItems(false)
879
+ ) : !tabInlineSearch ? (
880
+ <div className="selected-items-counter-container qbs-text-sm qbs-gap-1">
881
+ <Tooltip title={allSelectedTooltipContent} enabled={selectedItems?.length > 0 ? true : false}>
882
+ <span className="badge qbs-rounded-full qbs-text-xs qbs-inline-flex qbs-items-center qbs-justify-center qbs-px-2 qbs-py-1 qbs-leading-none qbs-min-w-6 qbs-min-h-6">
883
+ {selectedItems?.length}
884
+ </span>
885
+ </Tooltip>
886
+ </div>
887
+ ) : (
888
+ <div
889
+ className="selected-items-container qbs-text-sm qbs-gap-1"
890
+ onClick={() => onInputFocus()}
891
+ >
892
+ <span
893
+ className={`badge qbs-rounded-full qbs-text-xs qbs-inline-flex qbs-items-center qbs-justify-center qbs-px-2 qbs-py-1 qbs-leading-none qbs-min-w-6 qbs-min-h-6 ${selectedItems?.length > 99 ? 'qbs-text-hundred-plus' : ''
894
+ }`}
895
+ >
896
+ {selectedItems?.length > 99 ? '99+' : selectedItems?.length}
897
+ </span>
898
+ <span className="selected-label-text">
899
+ {selectedLabel && selectedLabel !== '' ? (
900
+ selectedLabel
901
+ ) : (
902
+ <>Item{selectedItems?.length > 1 && 's'} Selected</>
903
+ )}
904
+ </span>
905
+ </div>
906
+ )}
907
+ </>
908
+ )}
909
+
910
+ <div
911
+ className={`qbs-textfield-expandable ${!expandable ? 'qbs-normal' : ''
912
+ }`}
913
+ data-value={
914
+ selectedItems?.length > 0 || searchValue
915
+ ? !tabInlineSearch && searchValue
916
+ ? searchValue
917
+ : placeholder ?? ''
918
+ : placeholder ?? ''
919
+ }
920
+ onClick={() => onInputFocus()}
921
+ >
922
+ <input
923
+ id={id}
924
+ ref={inputRef}
925
+ autoComplete="off"
926
+ type="text"
927
+ value={
928
+ type === 'auto_suggestion' && !expandable && tabInlineSearch
929
+ ? inputValue
930
+ : searchValue || inputValue
931
+ }
932
+ onChange={handleChange}
933
+ // onBlur={handleBlur}
934
+ onFocus={(e) => {
935
+ onFocus?.(e);
936
+ if (!searchLoaded && searchValue && searchValue !== '') {
937
+ handleChangeWithDebounce(searchValue);
938
+ setSearchLoaded(true);
939
+ }
940
+ }}
941
+ onClick={() => handleOnClick()}
942
+ className={generateClassName()}
943
+ placeholder={
944
+ selectedItems?.length > 0
945
+ ? !tabInlineSearch && searchValue
946
+ ? searchValue
947
+ : !tabInlineSearch
948
+ ? placeholder ?? ''
949
+ : ''
950
+ : placeholder ?? ''
951
+ }
952
+ readOnly={
953
+ readOnly ||
954
+ type === 'custom_select' ||
955
+ (type === 'auto_suggestion' && !expandable && tabInlineSearch)
956
+ }
957
+ disabled={disabled}
958
+ data-testid="custom-autocomplete"
959
+ />
960
+ </div>
961
+
962
+ {enableToolsTab && (
963
+ <button
964
+ disabled={disabled ?? readOnly}
965
+ onClick={(e) => handleOpenToolsTab(e)}
966
+ className={`absolute right-2 top-2 tools-drop-btn focus-visible:outline-slate-100 ${viewMode ? 'hidden' : ''} ${tabSelectedItems?.length > 0 ? 'has-selected-items' : ''} ${showToolsTab ? 'tools-drop-btn-active' : ''}`}
967
+ data-testid="tools-drop-arrow"
968
+ type="button"
969
+ id="tools-drop-icon"
970
+ ref={toolsBtnRef}
971
+ >
972
+ <Tools
973
+ uniqueId="tools-arrow-icon"
974
+ className="tools-arrow-icon"
975
+ />
976
+
977
+ </button>
978
+ )}
979
+ {/* Displaying Loading Spinner */}
980
+
981
+ {/* Suggestions Dropdown */}
982
+
983
+ {dropOpen &&
984
+ ReactDOM.createPortal(
985
+ <div
986
+ ref={dropRef}
987
+ style={{
988
+ ...dropdownStyle,
989
+ minHeight: viewMode ? 100 : 192,
990
+ visibility: visible ? 'visible' : 'hidden',
991
+ opacity: visible ? 1 : 0,
992
+ transition: 'opacity 0.2s ease-in-out',
993
+ }}
994
+ className={`qbs-autocomplete-suggestions qbs-autocomplete-selected-suggestions ${viewMode ? 'qbs-dropdown-selected-preview' : ''
995
+ } ${viewMode && selectedItems?.length === 0 ? 'hidden' : ''}`}
996
+ >
997
+ {!viewMode && (
998
+ <>
999
+ <>{tab.length > 0 && getTabItems()}</>
1000
+ {type === 'auto_suggestion' &&
1001
+ !expandable &&
1002
+ tabInlineSearch && (
1003
+ <div
1004
+ style={{ position: 'relative' }}
1005
+ className="react-core-ts-search-container qbs-autocomplete-selected-suggestions-outer"
1006
+ >
1007
+ <span className="dropdown-search-icon">
1008
+ <Search />
1009
+ </span>
1010
+ <input
1011
+ className="dropdown-search-input"
1012
+ onChange={handleSuggestionChange}
1013
+ value={searchValue}
1014
+ placeholder="Type to search"
1015
+ autoComplete="off"
1016
+ ref={inputSearchRef}
1017
+ />
1018
+
1019
+ <>
1020
+ {autoDropdown && !disabled && !readOnly && (
1021
+ <button
1022
+ disabled={disabled ?? readOnly}
1023
+ onClick={(e) => handleOpenDropdown(e)}
1024
+ className="text-[#667085] focus-visible:outline-slate-100 absolute right-2 qbs-all-dropdown-btn"
1025
+ data-testid="drop-arrow"
1026
+ type="button"
1027
+ id="autocomplete-drop-icon"
1028
+ ref={dropBtnRef}
1029
+ >
1030
+ <AllDropArrow
1031
+ type={!allDataLoaded ? 'down' : 'up'}
1032
+ uniqueId="all-dropdow-arrow-icon"
1033
+ className="all-dropdow-arrow-icon"
1034
+ />
1035
+ </button>
1036
+ )}
1037
+ </>
1038
+ </div>
1039
+ )}
1040
+ </>
1041
+ )}
1042
+
1043
+ {filteredData?.length > 0 && enableSelectAll && isMultiple && (
1044
+ <div
1045
+ id="select-all"
1046
+ className={`qbs-select-all-link qbs-cursor-pointer qbs-text-xs`}
1047
+ onClick={handleSelectAll}
1048
+ >
1049
+ <div
1050
+ className={`qbs-autocomplete-listitem-container qbs-autocomplete-checkbox-container`}
1051
+ >
1052
+ <div className="qbs-autocomplete-checkbox">
1053
+ <input
1054
+ type="checkbox"
1055
+ checked={selectAll}
1056
+ id={`qbs-checkbox-all`}
1057
+ />
1058
+ <label htmlFor={`qbs-checkbox-all`}>
1059
+ <svg
1060
+ width="8"
1061
+ height="6"
1062
+ viewBox="0 0 8 6"
1063
+ fill="none"
1064
+ xmlns="http://www.w3.org/2000/svg"
1065
+ >
1066
+ <path
1067
+ d="M0 3.21739L2.89883 6L8 1.06994L6.89494 0L2.89883 3.86768L1.09728 2.14745L0 3.21739Z"
1068
+ fill="white"
1069
+ />
1070
+ </svg>
1071
+ </label>
1072
+ </div>
1073
+ <div className={`qbs-autocomplete-suggestions-item`}>
1074
+ Select All
1075
+ </div>
1076
+ </div>
1077
+ </div>
1078
+ )}
1079
+
1080
+ <div
1081
+ className={`qbs-autocomplete-suggestions-sub qbs-autocomplete-selected-suggestions-outer ${viewMode ? 'hidden' : ''
1082
+ }`}
1083
+ ref={itemsRef}
1084
+ style={{
1085
+ maxHeight: `${selHeight}px`,
1086
+ minHeight: `${selHeight}px`,
1087
+ }}
1088
+ >
1089
+ {filteredData?.length > 0 ? (
1090
+ filteredData.map((suggestion: ValueProps, idx: number) => (
1091
+ <DropdownList
1092
+ idx={idx}
1093
+ suggestion={suggestion}
1094
+ isSelected={isSelected}
1095
+ handleSuggestionClick={() =>
1096
+ handleSuggestionClick(suggestion, idx)
1097
+ }
1098
+ handleMultiSelect={handleMultiSelect}
1099
+ selected={selected}
1100
+ isMultiple={isMultiple}
1101
+ singleSelect={singleSelect}
1102
+ desc={desc}
1103
+ key={suggestion[descId]}
1104
+ shortCode={shortCode}
1105
+ labelCode={labelCode}
1106
+ focusedIndex={focusedIndex}
1107
+ setItemRef={(index, ref) => {
1108
+ itemRefs.current[index] = ref; // Store each ref properly
1109
+ }}
1110
+ />
1111
+ ))
1112
+ ) : (
1113
+ <>
1114
+ {isLoading ||
1115
+ (searchValue !== searchOldValue &&
1116
+ searchValue !== '' &&
1117
+ async) ? (
1118
+ <div className="qbs-flex qbs-align-middle qbs-justify-center qbs-min-emp-h">
1119
+ <div className="qbs-pt-16 qbs-autocomplete-loader">
1120
+ <Spinner />
1121
+ </div>
1122
+ </div>
1123
+ ) : (
1124
+ <div className="qbs-autocomplete-notfound qbs-text-center qbs-justify-center qbs-align-middle qbs-min-emp-h">
1125
+ {searchValue !== ''
1126
+ ? notDataMessage ?? 'No Results Found'
1127
+ : initialDataMessage ?? 'Type to search'}
1128
+ </div>
1129
+ )}
1130
+ </>
1131
+ )}
1132
+ {paginationEnabled &&
1133
+ nextBlock !== 0 &&
1134
+ nextBlock !== undefined &&
1135
+ filteredData?.length > 0 && (
1136
+ <div
1137
+ className="loadMoreSection"
1138
+ onClick={() => handleLoadMore()}
1139
+ >
1140
+ <p style={{ margin: 2 }}>Load More</p>
1141
+ </div>
1142
+ )}
1143
+ </div>
1144
+ <>
1145
+ {countOnly && selectedItems?.length > 0 && (
1146
+ <>
1147
+ <div
1148
+ className={`qbs-expandable-container qbs-dropdown-selected-container`}
1149
+ ref={dropdownSelectedRef}
1150
+ >
1151
+ <div className={`qbs-flex qbs-w-full qbs-text-xs`}>
1152
+ <div className="qbs-selected-lbs qbs-flex-grow">
1153
+ Selected Items
1154
+ </div>
1155
+ <div
1156
+ className={`qbs-clear-link qbs-text-right qbs-cursor-pointer ${viewMode ? 'hidden' : ''
1157
+ }`}
1158
+ onClick={handleClearSelected}
1159
+ >
1160
+ Clear all
1161
+ </div>
1162
+ <div
1163
+ className={`qbs-done-link qbs-text-right qbs-cursor-pointer ${viewMode ? 'hidden' : ''
1164
+ }`}
1165
+ onClick={() => {
1166
+ setDropOpen(false);
1167
+ }}
1168
+ >
1169
+ Done
1170
+ </div>
1171
+ </div>
1172
+
1173
+ {getSelectedItems(true)}
1174
+
1175
+ <>
1176
+ {selectedItems?.length > itemCount &&
1177
+ showAllSelected && (
1178
+ <div className="qbs-readmore-collapse">
1179
+ <div
1180
+ className="qbs-more-collapse"
1181
+ onClick={() => {
1182
+ handleCollapseArrowClick();
1183
+ }}
1184
+ >
1185
+ <DropArrow
1186
+ className={`icon-button-rotate`}
1187
+ uniqueDropArrowId={uniqueDropArrowId}
1188
+ />
1189
+ </div>
1190
+ </div>
1191
+ )}
1192
+ </>
1193
+ </div>
1194
+ </>
1195
+ )}
1196
+ </>
1197
+ </div>,
1198
+ document.body
1199
+ )}
1200
+
1201
+ {enableToolsTab && (
1202
+ <DropdownFilterTabs
1203
+ label=""
1204
+ name={tabName ?? ''}
1205
+ desc={tabDesc}
1206
+ descId={tabDescId}
1207
+ type="auto_suggestion"
1208
+ dropdownRef={dropdownRef}
1209
+ async={true}
1210
+ isMultiple={true}
1211
+ typeOnlyFetch={true}
1212
+ itemCount={itemCount}
1213
+ textCount={textCount}
1214
+ getData={getTabData}
1215
+ placeholder=""
1216
+ selectedItems={tabSelectedItems}
1217
+ onChange={onToolTabChange}
1218
+ countOnly={true}
1219
+ tab={toolTabs}
1220
+ clearTabSwitch={toolTabClearSwitch}
1221
+ tabInlineSearch={true}
1222
+ autoDropdown={autoDropdown}
1223
+ open={showToolsTab}
1224
+ data={tabData}
1225
+ enableSelectAll={enableSelectAll}
1226
+ />)
1227
+ }
1228
+ </div>
1229
+
1230
+ {/* Displaying Validation Error */}
1231
+ {errors && (
1232
+ <div
1233
+ className="text-error text-error-label mt-[1px]"
1234
+ data-testid="autocomplete-error"
1235
+ >
1236
+ {errors.message}
1237
+ </div>
1238
+ )}
1239
+ </div>
1240
+ );
1241
+ }
1242
+ );
1243
+
1244
+ export default React.memo(AutoCompleteWithTabFilter);