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