react-core-ts 2.0.12 → 2.0.14

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,1067 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { FieldErrors } from 'react-hook-form';
3
+
4
+ import CustomIcons from './components/customIcons';
5
+ import Spinner from './components/loader/Spinner';
6
+ import Portal from './components/portal';
7
+ import { debounce } from './utilities/debounce';
8
+ import { applyPositionClass } from './utilities/getPosition';
9
+ import { AllDropArrow, DropArrow } from './utilities/icons';
10
+
11
+ type valueProps = {
12
+ id?: string | number;
13
+ name: string;
14
+ label?: string;
15
+ param1?: string | number | null;
16
+ param2?: string | number | null;
17
+ param3?: string | number | null;
18
+ param4?: string | number | null;
19
+ from?: number;
20
+ };
21
+ interface AutoSuggestionInputProps {
22
+ id?: string;
23
+ label?: string;
24
+ fullWidth?: boolean;
25
+ className?: string;
26
+ autoFocus?: boolean;
27
+ required?: boolean;
28
+ hideClear?: boolean;
29
+ value?: valueProps;
30
+ onChange: (value?: valueProps) => void;
31
+ data?: any[];
32
+ type?: "custom_select" | "auto_complete" | "custom_search_select";
33
+ placeholder?: string;
34
+ getData?: ((key?: string) => Promise<any>) | ((key?: string) => any);
35
+ errors?: FieldErrors;
36
+ name: string;
37
+ readOnly?: boolean;
38
+ disabled?: boolean;
39
+ autoFilter?: boolean;
40
+
41
+ insideOpen?: boolean;
42
+ isClose?: boolean;
43
+ noLocalFilter?: boolean;
44
+ isStaticList?: boolean;
45
+ isCustomPlaceholder?: boolean;
46
+ checkParams?: string[];
47
+ fromPrefix?: boolean;
48
+ hasParentError?: boolean;
49
+ width?: number;
50
+ labelTitle?: string;
51
+ isModern?: boolean;
52
+ autoDropdown?: boolean;
53
+ size?: "xs" | "sm" | "md" | "lg" | "xxs";
54
+ tableView?: boolean;
55
+ dropdownMinWidth?: number; // Minimum additional width in pixels to add to dropdown
56
+ additionalColumns?: Array<{
57
+ key: string; // Field name in the suggestion object
58
+ width?: number; // Optional width in pixels (if not set, uses flex-1 for equal width)
59
+ order?: number; // Optional order/position (lower numbers appear first, default maintains array order)
60
+ }>; // Additional columns to display
61
+ }
62
+
63
+ const ReactAutoCompleteTableView: React.FC<AutoSuggestionInputProps> = ({
64
+ label,
65
+ onChange,
66
+
67
+ getData,
68
+ data,
69
+ errors,
70
+ required = false,
71
+ autoFocus,
72
+ name,
73
+ fullWidth,
74
+ placeholder,
75
+ id,
76
+ className: propsClassName,
77
+ type = 'custom_select',
78
+ readOnly,
79
+ disabled = false,
80
+ value,
81
+ autoFilter = false,
82
+ autoDropdown = true,
83
+ insideOpen = false,
84
+ isClose = true,
85
+ noLocalFilter = false,
86
+ isStaticList = false,
87
+ isCustomPlaceholder = false,
88
+ checkParams,
89
+ width,
90
+ hasParentError,
91
+ fromPrefix,
92
+ labelTitle,
93
+ isModern = true,
94
+ size = "md",
95
+ dropdownMinWidth = 200,
96
+ tableView = false,
97
+ additionalColumns = [],
98
+ }) => {
99
+ const [inputValue, setInputValue] = useState<any>(value?.name ?? "");
100
+ const [isHovered, setIsHovered] = useState<boolean>(false);
101
+ const [isLoading, setIsLoading] = useState<boolean>(false);
102
+ const [dropOpen, setDropOpen] = useState<boolean>(false);
103
+
104
+ const [isDisabled, setIsDisabled] = useState<boolean>(disabled);
105
+ const [showClose, setShowClose] = useState(false);
106
+ const [showToolTip, setShowTooltip] = useState(false);
107
+ const [tooltipIsHovered, setTooltipIsHovered] = useState(false);
108
+ const [suggestions, setSuggestions] = useState<any>([]);
109
+ const inputRef = useRef<HTMLInputElement>(null);
110
+ const adorementRef = useRef<HTMLDivElement>(null);
111
+ const dropdownref = useRef<HTMLDivElement>(null);
112
+ const dropBtnRef = useRef<HTMLButtonElement>(null);
113
+ const timerRef = useRef<number>(0); // To fix the no results found issue on second type
114
+ const [selectedIndex, setSelectedIndex] = useState(0);
115
+ const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
116
+ const scrollContainerRef = useRef<HTMLUListElement | null>(null);
117
+ const [refetchData, setRefetchData] = useState(false);
118
+ const [dropPosition, setDropPosition] = useState<any>({
119
+ top: 0,
120
+ left: 0,
121
+ bottom: 0,
122
+ right: 0,
123
+ });
124
+ const checkIncludes = (
125
+ mainString: string,
126
+ subString: string,
127
+ param1?: string | number,
128
+ param2?: string | number,
129
+ param3?: string | number,
130
+ param4?: string | number
131
+ ): boolean => {
132
+ const checkIncludesIfExists = (
133
+ mainString?: string | number,
134
+ substring?: string
135
+ ) => {
136
+ return substring &&
137
+ mainString &&
138
+ typeof mainString === 'string' &&
139
+ typeof subString === 'string'
140
+ ? mainString
141
+ .toString()
142
+ .toLowerCase()
143
+ .startsWith(substring.toString().toLowerCase())
144
+ : false;
145
+ };
146
+ const checkArrayContains = (
147
+ strArray: string[] | undefined,
148
+ param: string
149
+ ): boolean => strArray?.includes(param) ?? false;
150
+
151
+ return (
152
+ checkIncludesIfExists(mainString, subString) ||
153
+ (!!checkParams &&
154
+ ((checkArrayContains(checkParams, 'param1') &&
155
+ checkIncludesIfExists(param1, subString)) ||
156
+ (checkArrayContains(checkParams, 'param2') &&
157
+ checkIncludesIfExists(param2, subString)) ||
158
+ (checkArrayContains(checkParams, 'param3') &&
159
+ checkIncludesIfExists(param3, subString)) ||
160
+ (checkArrayContains(checkParams, 'param4') &&
161
+ checkIncludesIfExists(param4, subString))))
162
+ );
163
+ };
164
+
165
+ const getHeight = () => {
166
+ switch (size) {
167
+ case 'sm':
168
+ return ' h-[34px] ';
169
+ default:
170
+ return ' ';
171
+ }
172
+ };
173
+
174
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
175
+ const { value } = event.target;
176
+ timerRef.current = 0;
177
+
178
+ setInputValue(value);
179
+ handleValChange(value);
180
+ if (!value) {
181
+ setInputValue('');
182
+ onChange({ id: undefined, name: '', from: 2 });
183
+ }
184
+ };
185
+
186
+ const handleValChange = useCallback(
187
+ debounce(
188
+ (value: string) => {
189
+ setDropOpen(true);
190
+ onChange({ id: undefined, name: '', from: 1 });
191
+ if (value.trim() === '' && type === 'auto_complete') {
192
+ setSuggestions([]);
193
+ if (autoFilter) {
194
+ handleDropData('*');
195
+ } else {
196
+ setDropOpen(false);
197
+ }
198
+ } else {
199
+ handleDropData(value);
200
+ }
201
+ setTimeout(() => {
202
+ timerRef.current = 1;
203
+ }, 200);
204
+ },
205
+ isStaticList ? 0 : 1000
206
+ ),
207
+ []
208
+ );
209
+ const handleDropData = (value?: string) => {
210
+ if (type === 'auto_complete') {
211
+ if (isStaticList) {
212
+ loadStaticData();
213
+ const filteredData =
214
+ value && value !== '*'
215
+ ? data?.filter((item) =>
216
+ checkIncludes(
217
+ item.name,
218
+ value,
219
+ item.param1,
220
+ item.param2,
221
+ item.param3
222
+ )
223
+ )
224
+ : data;
225
+ setSuggestions(filteredData);
226
+ } else {
227
+ handlePickSuggestions(value);
228
+ }
229
+ } else if (
230
+ (type === 'custom_select' || type === 'custom_search_select') &&
231
+ (!data || data?.length === 0)
232
+ ) {
233
+ handlePickSuggestions();
234
+ } else if (type === 'custom_search_select' && data && data?.length > 0) {
235
+ const filteredData =
236
+ value && value !== '*'
237
+ ? data.filter((item) =>
238
+ checkIncludes(
239
+ item.name,
240
+ value,
241
+ item.param1,
242
+ item.param2,
243
+ item.param3
244
+ )
245
+ )
246
+ : data;
247
+ setSuggestions(filteredData);
248
+ }
249
+ };
250
+ const handlePickSuggestions = async (value?: string) => {
251
+ if (getData) {
252
+ setIsLoading(true);
253
+ try {
254
+ const fetchedSuggestions = await getData?.(value);
255
+ setSuggestions(fetchedSuggestions);
256
+ setIsLoading(false);
257
+ if (autoDropdown) {
258
+ setRefetchData(true);
259
+ }
260
+ } catch (error) {
261
+ setIsLoading(false);
262
+ }
263
+ }
264
+ };
265
+
266
+ const onLabelClick = () => {
267
+ if (!isDisabled) {
268
+ inputRef?.current?.focus();
269
+ if (autoFilter && inputValue === '') {
270
+ handleValChange('*');
271
+ } else if (
272
+ (type === 'custom_select' || type === 'custom_search_select') &&
273
+ data &&
274
+ data?.length > 0
275
+ ) {
276
+ handleDropData();
277
+ setDropOpen(true);
278
+ }
279
+ }
280
+ };
281
+ const handleClickOutside = (event: MouseEvent) => {
282
+ if (
283
+ dropdownref.current &&
284
+ !dropdownref.current.contains(event.target as Node)
285
+ ) {
286
+ setDropOpen(false);
287
+ }
288
+ };
289
+ const checkIsEmptyField = (): boolean => {
290
+ if (inputValue === undefined) return true;
291
+ else return (inputValue?.toString().length ?? 0) <= 0 ? true : false;
292
+ };
293
+ const loadStaticData = async () => {
294
+ if (!data || (refetchData && autoDropdown && !isStaticList)) {
295
+ data = [];
296
+ if (getData) {
297
+ try {
298
+ setIsLoading(true);
299
+ //handlePickSuggestions('*');
300
+ const fetchedSuggestions = await getData?.('*');
301
+ fetchedSuggestions?.forEach((element: any) => {
302
+ data?.push(element);
303
+ });
304
+ setSuggestions(fetchedSuggestions);
305
+ setIsLoading(false);
306
+ if (autoDropdown) {
307
+ setRefetchData(false);
308
+ timerRef.current = 1;
309
+ }
310
+ } catch (error) {
311
+ setIsLoading(false);
312
+ }
313
+ }
314
+ }
315
+ };
316
+ useEffect(() => {
317
+ document.addEventListener('click', handleClickOutside);
318
+ return () => {
319
+ document.removeEventListener('click', handleClickOutside);
320
+ };
321
+ }, []);
322
+
323
+ useEffect(() => {
324
+ if (autoFocus) {
325
+ onLabelClick();
326
+ }
327
+ }, [autoFocus]);
328
+ const onInputFocus = () => {
329
+ if (!isDisabled) {
330
+ if (autoFilter && inputValue === '') {
331
+ handleValChange('*');
332
+ } else if (
333
+ (type === 'custom_select' || type === 'custom_search_select') &&
334
+ data &&
335
+ data?.length > 0
336
+ ) {
337
+ handleDropData();
338
+ //if (insideOpen) setDropOpen(true) // commeted this code bcoz added onclick function inside
339
+ }
340
+ }
341
+ };
342
+ useEffect(() => {
343
+ setIsDisabled(disabled);
344
+ }, [disabled]);
345
+ const handleSuggestionClick = (suggestion: valueProps, index: number) => {
346
+ onChange({
347
+ ...suggestion,
348
+ id: suggestion?.id,
349
+ name: suggestion.name,
350
+ ...(suggestion?.label ? { label: suggestion?.label } : {}),
351
+ });
352
+ if (type !== 'custom_select') setInputValue(suggestion.name);
353
+ setDropOpen(false);
354
+ setSelectedIndex(index);
355
+ };
356
+ const handleOpen = (e: any) => {
357
+ if (!suggestions || suggestions?.length === 0) handleDropData();
358
+ if (!isLoading) {
359
+ setDropOpen(!dropOpen);
360
+ if (type !== 'custom_select') setInputValue('');
361
+ }
362
+ };
363
+
364
+ const handleOpenDropdown = (e: any) => {
365
+ if (!suggestions || suggestions?.length === 0 || refetchData) {
366
+ if (autoDropdown && (inputValue === '' || inputValue.trim() === '')) {
367
+ setSuggestions([]);
368
+ loadStaticData();
369
+ } else if (!refetchData) {
370
+ handleDropData();
371
+ }
372
+ }
373
+ if (!isLoading) {
374
+ setDropOpen(!dropOpen);
375
+ }
376
+ };
377
+
378
+ const handleScroll = () => {
379
+ setDropOpen(false);
380
+ };
381
+
382
+ useEffect(() => {
383
+ if (!insideOpen) {
384
+ setSelectedIndex(0);
385
+ window.addEventListener('scroll', handleScroll);
386
+
387
+ const mainElement = document.querySelector('main');
388
+ mainElement?.addEventListener('scroll', handleScroll);
389
+
390
+ const gridElements = document.querySelectorAll(
391
+ '.k-grid-content, .overflow-auto, .overflow-y-auto, .overflow-x-auto'
392
+ );
393
+ gridElements.forEach((gridElement: any) => {
394
+ gridElement.addEventListener('scroll', handleScroll);
395
+ });
396
+
397
+ window.addEventListener('resize', getDropPosition);
398
+ getDropPosition();
399
+ return () => {
400
+ window.removeEventListener('scroll', handleScroll);
401
+ const mainElement = document.querySelector('main');
402
+ mainElement?.removeEventListener('scroll', handleScroll);
403
+ window.addEventListener('resize', getDropPosition);
404
+ const gridElements = document.querySelectorAll('.k-grid-content');
405
+ gridElements.forEach((gridElement: any) => {
406
+ gridElement.removeEventListener('scroll', handleScroll);
407
+ });
408
+ };
409
+ }
410
+ }, [dropOpen]);
411
+
412
+ const handleClear = () => {
413
+ setDropOpen(false);
414
+ setInputValue('');
415
+ onChange({ id: undefined, name: '', from: 3 });
416
+ onLabelClick();
417
+ if (autoDropdown) {
418
+ setRefetchData(true);
419
+ }
420
+ };
421
+
422
+ useEffect(() => {
423
+ setInputValue(value?.name ?? '');
424
+ }, [value?.name]);
425
+
426
+ useEffect(() => {
427
+ setSuggestions(data);
428
+ }, [data]);
429
+
430
+ const getErrors = (err: any) => {
431
+ let errMsg = '';
432
+ if (err.message) {
433
+ errMsg = err?.message;
434
+ } else if (err?.id?.message) {
435
+ errMsg = err?.id?.message;
436
+ } else if (err?.name?.message) {
437
+ errMsg = err?.name?.message;
438
+ }
439
+ return errMsg;
440
+ };
441
+ const generateClassName = (
442
+ type: 'input' | 'label' | 'message' | 'adorement'
443
+ ): string => {
444
+ let className = propsClassName;
445
+ switch (type) {
446
+ case 'input':
447
+ className += ` block text-common text-input-text font-normal px-3.5 w-full text-sm text-gray-900 bg-transparent border appearance-none peer h-10 rounded-[4px] disabled:text-input-disabled bg-white disabled:bg-disabled ${
448
+ label && isModern
449
+ ? 'placeholder-transparent'
450
+ : 'focus:placeholder-grey-secondary placeholder-input-label'
451
+ } focus:placeholder-grey-secondary`;
452
+
453
+ if (errors && errors[name]) {
454
+ className +=
455
+ ' border-[#FDA29B] focus:border-error-[#FDA29B] focus:ring-[#FDA29B] focus:ring-3 focus:outline-[#FDA29B] input-outline';
456
+ } else {
457
+ className +=
458
+ ' text-grey-dark border-input-light focus:border-blue-navy focus:outline-none focus:ring-0';
459
+ }
460
+
461
+ className += getHeight()
462
+
463
+ break;
464
+ case 'label':
465
+ className += ` flex modern-input-label-truncate peer-focus:modern-input-peer-focus-label-size
466
+ ${
467
+ isDisabled
468
+ ? 'cursor-pointer'
469
+ : 'cursor-text peer-focus:cursor-pointer'
470
+ } ${
471
+ isDisabled && !checkIsEmptyField()
472
+ ? 'disabled-input-label-bg'
473
+ : !isDisabled || !checkIsEmptyField()
474
+ ? 'active-input-label-bg'
475
+ : ''
476
+ } absolute duration-300 transform -translate-y-4 top-2 z-1 origin-[0] px-0 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:-translate-y-4 start-[14px] rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto ${
477
+ isDisabled
478
+ ? 'cursor-pointer'
479
+ : 'cursor-text peer-focus:cursor-pointer'
480
+ }
481
+ ${
482
+ checkIsEmptyField()
483
+ ? 'modern-input-label-size'
484
+ : 'modern-input-peer-focus-label-size'
485
+ }`;
486
+ if (errors && errors[name]) {
487
+ className += ' text-error-light ';
488
+ } else {
489
+ className += ' text-grey-dark peer-focus:text-blue-navy';
490
+ }
491
+ break;
492
+ case 'message':
493
+ className = ' text-error-icon ';
494
+ break;
495
+ case 'adorement':
496
+ className += ' absolute right-0 adorement gap-1 flex items-center ';
497
+ break;
498
+ default:
499
+ break;
500
+ }
501
+ return className;
502
+ };
503
+ const handleClose = () => {
504
+ setTimeout(() => {
505
+ setDropOpen(false);
506
+ }, 500);
507
+ };
508
+ const handleClearInputValue = () => {
509
+ // setInputValue(value?.name ?? '')
510
+ };
511
+ const getInnerWidth = () => {
512
+ const innerwidth = adorementRef.current
513
+ ? adorementRef.current.offsetWidth
514
+ : 0;
515
+ return innerwidth;
516
+ };
517
+ // const getPosition = () => {
518
+ // return 'bottom'
519
+ // }
520
+ const filteredData =
521
+ inputValue !== '*' &&
522
+ inputValue !== '' &&
523
+ type !== 'custom_select' &&
524
+ !noLocalFilter
525
+ ? suggestions?.filter((item: valueProps) =>
526
+ checkIncludes(
527
+ item.name,
528
+ inputValue,
529
+ item.param1 ?? '',
530
+ item.param2 ?? '',
531
+ item.param3 ?? '',
532
+ item.param4 ?? ''
533
+ )
534
+ )
535
+ : suggestions;
536
+
537
+ const handleError = (data: any) => {
538
+ if (
539
+ getErrors(data[name]) === 'required' ||
540
+ getErrors(data[name]) === 'Required'
541
+ ) {
542
+ return `${label ?? labelTitle} is ${getErrors(data[name])}`;
543
+ } else {
544
+ return getErrors(data[name]) ?? '';
545
+ }
546
+ };
547
+
548
+ const getDropPosition = (): any => {
549
+ if (!insideOpen && inputRef?.current) {
550
+ const dropdownHeight = 200; // Assume the height of the dropdown is 200px, adjust as needed
551
+ const isMobile = window.innerWidth < 768; // Mobile breakpoint
552
+
553
+ const inputRect = inputRef.current.getBoundingClientRect();
554
+ const spaceBelow =
555
+ window.innerHeight - (inputRect.bottom + window.scrollY);
556
+ const spaceAbove = inputRect.top + window.scrollY;
557
+
558
+ let top;
559
+ let bottom;
560
+ if (spaceBelow < dropdownHeight && spaceAbove > dropdownHeight) {
561
+ bottom = spaceBelow + 43;
562
+ } else {
563
+ top = inputRect.top + window.scrollY + inputRect.height + 2;
564
+ }
565
+
566
+ let left = inputRect.left + window.scrollX;
567
+ let width = inputRect.width + dropdownMinWidth;
568
+ const viewportWidth = window.innerWidth;
569
+ const viewportRight = window.scrollX + viewportWidth;
570
+ const inputRight = inputRect.right + window.scrollX;
571
+
572
+ // Check if dropdown would overflow on the right side
573
+ const dropdownRight = left + width;
574
+ const wouldOverflowRight = dropdownRight > viewportRight;
575
+
576
+ // If dropdown would overflow on the right (last column scenario), align from right to left
577
+ if (wouldOverflowRight) {
578
+ // Align right edge of dropdown with right edge of input
579
+ // Dropdown extends to the left from the input's right edge
580
+ const padding = isMobile ? 8 : 0;
581
+ left = inputRight - width;
582
+
583
+ // Ensure dropdown doesn't go off the left edge
584
+ const viewportLeft = window.scrollX + (isMobile ? 8 : 0);
585
+ if (left < viewportLeft) {
586
+ left = viewportLeft;
587
+ // Adjust width to fit within viewport
588
+ width = Math.min(width, inputRight - left - padding);
589
+ }
590
+ }
591
+
592
+ // Handle mobile view - additional constraints
593
+ if (isMobile) {
594
+ // On mobile, constrain width to viewport with some padding
595
+ const mobilePadding = 16; // 8px padding on each side
596
+ const maxWidth = viewportWidth - mobilePadding;
597
+
598
+ // Use the smaller of: calculated width or max mobile width
599
+ width = Math.min(width, maxWidth);
600
+
601
+ // Adjust left position to prevent overflow
602
+ const rightEdge = left + width;
603
+ const mobileViewportRight = window.scrollX + viewportWidth - mobilePadding / 2;
604
+
605
+ if (rightEdge > mobileViewportRight) {
606
+ // Align to right edge with padding
607
+ left = mobileViewportRight - width;
608
+ }
609
+
610
+ // Ensure dropdown doesn't go off the left edge
611
+ const viewportLeft = window.scrollX + mobilePadding / 2;
612
+ if (left < viewportLeft) {
613
+ left = viewportLeft;
614
+ // Adjust width if needed to fit
615
+ width = Math.min(width, mobileViewportRight - left);
616
+ }
617
+ }
618
+
619
+ setDropPosition({
620
+ left,
621
+ top,
622
+ bottom,
623
+ width,
624
+ });
625
+ }
626
+
627
+ return undefined;
628
+ };
629
+
630
+ const [showNoResults, setShowNoResults] = useState(false);
631
+
632
+ useEffect(() => {
633
+ if (
634
+ (inputValue || autoDropdown) &&
635
+ filteredData?.length === 0 &&
636
+ !isLoading &&
637
+ timerRef.current === 1
638
+ ) {
639
+ const timer = setTimeout(() => {
640
+ setShowNoResults(true);
641
+ }, 500); // Delay in milliseconds
642
+ return () => clearTimeout(timer);
643
+ } else {
644
+ setShowNoResults(false);
645
+ }
646
+
647
+ // Handle keyboard navigation
648
+ const handleKeyDown = (e: KeyboardEvent) => {
649
+ if (!dropOpen) return;
650
+
651
+ const nextIndex =
652
+ e.key === 'ArrowDown'
653
+ ? (selectedIndex + 1) % filteredData.length
654
+ : e.key === 'ArrowUp'
655
+ ? (selectedIndex - 1 + filteredData.length) % filteredData.length
656
+ : selectedIndex;
657
+
658
+ switch (e.key) {
659
+ case 'ArrowDown':
660
+ case 'ArrowUp':
661
+ e.preventDefault();
662
+
663
+ // Update index first
664
+ setSelectedIndex(nextIndex);
665
+
666
+ // Wait for DOM update before scrolling
667
+ requestAnimationFrame(() => {
668
+ const el = itemRefs.current[nextIndex];
669
+ const container = scrollContainerRef.current;
670
+
671
+ if (el && container) {
672
+ const elTop = el.offsetTop;
673
+ const elBottom = elTop + el.offsetHeight;
674
+
675
+ const containerTop = container.scrollTop;
676
+ const containerBottom = containerTop + container.clientHeight;
677
+
678
+ if (elBottom > containerBottom) {
679
+ container.scrollTop = elBottom - container.clientHeight;
680
+ } else if (elTop < containerTop) {
681
+ container.scrollTop = elTop;
682
+ }
683
+ }
684
+ });
685
+ break;
686
+
687
+ case 'Enter':
688
+ e.preventDefault();
689
+ handleSuggestionClick(filteredData[selectedIndex], selectedIndex);
690
+ break;
691
+
692
+ case 'Escape':
693
+ e.preventDefault();
694
+ setDropOpen(false);
695
+ break;
696
+
697
+ default:
698
+ break;
699
+ }
700
+ };
701
+
702
+ if (filteredData?.length > 0 && !isLoading) {
703
+ window.addEventListener('keydown', handleKeyDown);
704
+ return () => {
705
+ window.removeEventListener('keydown', handleKeyDown);
706
+ };
707
+ }
708
+ }, [
709
+ inputValue,
710
+ filteredData,
711
+ isLoading,
712
+ timerRef.current,
713
+ selectedIndex,
714
+ dropOpen,
715
+ ]);
716
+
717
+ const setDropDown = () => {
718
+ return (
719
+ (filteredData?.length > 0 || showNoResults) && (
720
+ <div
721
+ style={dropPosition}
722
+ className="autocomplete-suggections autocomplete-suggections-tableview absolute bg-white shadow-gray-300 shadow-md border border-grey-light z-50 mt-9"
723
+ >
724
+ <ul
725
+ className={`h-auto max-h-40 overflow-auto w-full ${tableView ? '' : 'py-1.5'}`}
726
+ ref={scrollContainerRef}
727
+ >
728
+ {filteredData?.length > 0 ? (
729
+ <>
730
+ {filteredData.map((suggestion: any, index: number) => (
731
+ <li
732
+ className={`${
733
+ value?.id === suggestion?.id
734
+ ? 'bg-blue-navy text-white'
735
+ : `${
736
+ index === selectedIndex ? 'is-selected' : ''
737
+ } hover:bg-table-hover`
738
+ } cursor-pointer text-xxs qbs-autocomplete-suggections-items ${
739
+ tableView
740
+ ? "border-b border-grey-light last:border-b-0"
741
+ : "p-1 ps-3.5 pl-[10px]"
742
+ }`}
743
+ key={suggestion?.id}
744
+ data-testid={suggestion.name}
745
+ onClick={() => handleSuggestionClick(suggestion, index)}
746
+ tabIndex={index}
747
+ ref={(el) => (itemRefs.current[index] = el)}
748
+ >
749
+ <ul className="flex items-stretch w-full list-none p-0 m-0">
750
+ {(() => {
751
+ // Sort columns by order if specified, otherwise maintain array order
752
+ const sortedColumns = additionalColumns
753
+ ? [...additionalColumns].sort((a, b) => {
754
+ const orderA = a.order !== undefined ? a.order : Infinity;
755
+ const orderB = b.order !== undefined ? b.order : Infinity;
756
+ return orderA - orderB;
757
+ })
758
+ : [];
759
+
760
+ // Separate columns before first column (order < 0) and after (order >= 0 or undefined)
761
+ const columnsBefore = sortedColumns.filter(col => col.order !== undefined && col.order < 0);
762
+ const columnsAfter = sortedColumns.filter(col => col.order === undefined || col.order >= 0);
763
+
764
+ // Determine if first column needs a separator
765
+ const hasColumnsAfter = columnsAfter.length > 0;
766
+
767
+ return (
768
+ <>
769
+ {/* Columns before first column */}
770
+ {columnsBefore.map((column, colIndex) => {
771
+ const hasValue = suggestion?.[column.key];
772
+ const isLastBefore = false;
773
+ return hasValue ? (
774
+ <li
775
+ key={column.key}
776
+ className={`${column.width ? 'flex-shrink-0' : 'flex-1'} min-w-0 px-3 ${tableView ? 'py-2' : ''} ${!isLastBefore ? 'border-r border-grey-light' : ''} break-words flex flex-col`}
777
+ style={column.width ? { width: `${column.width}px` } : undefined}
778
+ >
779
+ <span className="block whitespace-normal">
780
+ {suggestion?.[column.key]}
781
+ </span>
782
+ </li>
783
+ ) : null;
784
+ })}
785
+
786
+ {/* First column (label/name) */}
787
+ <li className={`flex-1 min-w-0 px-3 ${tableView ? 'py-2' : ''} ${hasColumnsAfter ? 'border-r border-grey-light' : ''} break-words flex flex-col`}>
788
+ <span className="block whitespace-normal">
789
+ {suggestion?.label ? suggestion?.label : suggestion.name}
790
+ </span>
791
+ </li>
792
+
793
+ {/* Columns after first column */}
794
+ {columnsAfter.map((column, colIndex) => {
795
+ const hasValue = suggestion?.[column.key];
796
+ const isLastColumn = colIndex === columnsAfter.length - 1;
797
+ return hasValue ? (
798
+ <li
799
+ key={column.key}
800
+ className={`${column.width ? 'flex-shrink-0' : 'flex-1'} min-w-0 px-3 ${tableView ? 'py-2' : ''} ${!isLastColumn ? 'border-r border-grey-light' : ''} break-words flex flex-col`}
801
+ style={column.width ? { width: `${column.width}px` } : undefined}
802
+ >
803
+ <span className="block whitespace-normal">
804
+ {suggestion?.[column.key]}
805
+ </span>
806
+ </li>
807
+ ) : null;
808
+ })}
809
+ </>
810
+ );
811
+ })()}
812
+ </ul>
813
+ </li>
814
+ ))}
815
+ </>
816
+ ) : (
817
+ showNoResults &&
818
+ !isLoading &&
819
+ timerRef.current === 1 && (
820
+ <li
821
+ className={`$ cursor-pointer p-1 rounded-sm text-xxs`}
822
+ onClick={handleClose}
823
+ >
824
+ No Results Found
825
+ </li>
826
+ )
827
+ )}
828
+ </ul>
829
+ </div>
830
+ )
831
+ );
832
+ };
833
+ const getBorderRight = () => {
834
+ if (fromPrefix) {
835
+ if (
836
+ (hasParentError && Boolean(errors && errors[name])) ||
837
+ hasParentError
838
+ ) {
839
+ return 0;
840
+ } else if (errors && errors[name]) {
841
+ return undefined;
842
+ }
843
+ }
844
+ return 0;
845
+ };
846
+
847
+ useEffect(() => {
848
+ applyPositionClass(dropdownref, isHovered);
849
+ setDropOpen(false);
850
+ }, [isHovered]);
851
+
852
+ useEffect(() => {
853
+ const shouldShowClose =
854
+ value?.name &&
855
+ (value?.id || String(value?.id) === '0') &&
856
+ !disabled &&
857
+ !readOnly &&
858
+ isClose;
859
+ if (showClose !== shouldShowClose) {
860
+ setShowClose(shouldShowClose);
861
+ }
862
+ }, [value, disabled, readOnly]);
863
+ useEffect(() => {
864
+ // Check if input content overflows
865
+ const checkOverflow = () => {
866
+ if (inputRef.current) {
867
+ const { scrollWidth, clientWidth, value } = inputRef.current;
868
+ // Check for overflow only when there's content
869
+ if (value.trim() !== '') {
870
+ setShowTooltip(scrollWidth > clientWidth + 2);
871
+ } else {
872
+ setShowTooltip(false); // No tooltip for empty input
873
+ }
874
+ }
875
+ };
876
+
877
+ if (inputRef.current) {
878
+ inputRef.current.addEventListener('input', checkOverflow);
879
+ checkOverflow(); // Initial check
880
+ }
881
+
882
+ return () => {
883
+ if (inputRef.current) {
884
+ inputRef.current.removeEventListener('input', checkOverflow);
885
+ }
886
+ };
887
+ }, [inputValue]);
888
+
889
+ const handleMouseEnter = () => {
890
+ setTooltipIsHovered(true);
891
+ };
892
+
893
+ const handleMouseLeave = () => {
894
+ setTooltipIsHovered(false);
895
+ };
896
+
897
+ return (
898
+ <div
899
+ id={
900
+ id ? `autocomplete-container-${id}` : `autocomplete-container-${name}`
901
+ }
902
+ className={` flex-grow ${fullWidth ? 'w-full' : 'w-auto'}`}
903
+ ref={dropdownref}
904
+ >
905
+ {label && !isModern && (
906
+ <div className="mb-3">
907
+ <label className={`text-xs font-medium`}>
908
+ {label}
909
+ {required ? <span className="text-error"> *</span> : <></>}
910
+ </label>
911
+ </div>
912
+ )}
913
+ <div className="tooltip-container">
914
+ {isHovered && errors && errors[name] && (
915
+ <span className="tooltip">{handleError(errors)} </span>
916
+ )}
917
+ {tooltipIsHovered && showToolTip && !dropOpen && inputValue && (
918
+ <div className={`tooltip-info`}>{inputValue}</div>
919
+ )}
920
+
921
+ <div
922
+ className={`flex relative ${fullWidth ? 'w-full' : 'w-auto'}`}
923
+ style={{ width: width }}
924
+ >
925
+ <div
926
+ className="relative w-full"
927
+ onMouseEnter={() => handleMouseEnter()}
928
+ onMouseLeave={() => handleMouseLeave()}
929
+ >
930
+ <input
931
+ type="text"
932
+ readOnly={readOnly ?? type === 'custom_select'}
933
+ value={inputValue ? inputValue : ''}
934
+ onBlur={handleClearInputValue}
935
+ autoComplete="off"
936
+ disabled={disabled}
937
+ ref={inputRef}
938
+ id={
939
+ id ? `custom-autocomplete-${id}` : `custom-autocomplete-${name}`
940
+ }
941
+ data-testid="custom-autocomplete"
942
+ // onBlur={() => {
943
+ // setInputValue("")
944
+ // }}
945
+ aria-describedby={id}
946
+ style={{
947
+ paddingRight: getInnerWidth(),
948
+ ...(fromPrefix
949
+ ? {
950
+ borderRight: getBorderRight(),
951
+ borderTopRightRadius: 0,
952
+ borderBottomRightRadius: 0,
953
+ }
954
+ : {}),
955
+ }}
956
+ className={` ${generateClassName('input')}`}
957
+ onChange={handleChange}
958
+ placeholder={
959
+ type === 'auto_complete'
960
+ ? placeholder && isCustomPlaceholder
961
+ ? placeholder
962
+ : 'Type to search'
963
+ : placeholder ?? '--Select--'
964
+ }
965
+ onFocus={onInputFocus}
966
+ onClick={(e) => {
967
+ if (type === 'custom_select') {
968
+ setDropOpen(!dropOpen);
969
+ handleOpen(e);
970
+ } else {
971
+ if (dropOpen || filteredData?.length > 0)
972
+ setDropOpen(!dropOpen);
973
+ }
974
+ }}
975
+ />
976
+ {isModern && (
977
+ <label
978
+ htmlFor={id}
979
+ onClick={() => onLabelClick()}
980
+ className={generateClassName('label')}
981
+ >
982
+ {label ? <span className="truncate">{label}</span> : ''}
983
+ {required ? <span className="text-error"> *</span> : <></>}
984
+ </label>
985
+ )}
986
+ </div>
987
+ <div className="flex items-center justify-center autocomplete-adorement-wrapper">
988
+ <div
989
+ ref={adorementRef}
990
+ className={`${generateClassName(
991
+ 'adorement'
992
+ )} qbs-autocomplete-adorement auto-dorpdown-adorement mr-[1px] ${
993
+ isLoading ? 'bg-white' : ''
994
+ }`}
995
+ >
996
+ {showClose && (
997
+ <button
998
+ onClick={() => handleClear()}
999
+ className=" text-table-bodyColor text-[#667085]"
1000
+ aria-label="close"
1001
+ type="button"
1002
+ id="autocomplete-close-icon"
1003
+ >
1004
+ <CustomIcons name="close" type="large-m" />
1005
+ </button>
1006
+ )}
1007
+ {isLoading && <Spinner />}
1008
+ {(type !== 'auto_complete' || autoDropdown) &&
1009
+ !disabled &&
1010
+ !readOnly && (
1011
+ <button
1012
+ disabled={disabled ?? readOnly}
1013
+ onClick={(e) => handleOpenDropdown(e)}
1014
+ onBlur={handleClose}
1015
+ className=" text-[#667085] focus-visible:outline-slate-100"
1016
+ data-testid="drop-arrow"
1017
+ type="button"
1018
+ id="autocomplete-drop-icon"
1019
+ ref={dropBtnRef}
1020
+ >
1021
+ {!dropOpen ? (
1022
+ autoDropdown ? (
1023
+ <AllDropArrow
1024
+ type="down"
1025
+ uniqueId="all-dropdow-arrow-icon"
1026
+ className="all-dropdow-arrow-icon"
1027
+ />
1028
+ ) : (
1029
+ <DropArrow uniqueDropArrowId="drop-arrow-icon" />
1030
+ )
1031
+ ) : autoDropdown ? (
1032
+ <AllDropArrow
1033
+ type="up"
1034
+ uniqueId="all-dropdow-arrow-icon"
1035
+ className="all-dropdow-arrow-icon"
1036
+ />
1037
+ ) : (
1038
+ <DropArrow
1039
+ className="rotate-180"
1040
+ uniqueDropArrowId="drop-arrow-icon"
1041
+ />
1042
+ )}
1043
+ </button>
1044
+ )}
1045
+ {errors && errors[name] && (
1046
+ <div
1047
+ className={` text-error-label relative cursor-pointer ${generateClassName(
1048
+ 'message'
1049
+ )}`}
1050
+ onMouseEnter={() => setIsHovered(true)}
1051
+ onMouseLeave={() => setIsHovered(false)}
1052
+ >
1053
+ <CustomIcons name="alert" type="medium" />
1054
+ </div>
1055
+ )}
1056
+ </div>
1057
+ </div>
1058
+ {dropOpen && (!isLoading || filteredData?.length > 0) && (
1059
+ <>{insideOpen ? setDropDown() : <Portal>{setDropDown()}</Portal>}</>
1060
+ )}
1061
+ </div>
1062
+ </div>
1063
+ </div>
1064
+ );
1065
+ };
1066
+
1067
+ export default ReactAutoCompleteTableView;