react-core-ts 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AutoComplete.js +26 -8
- package/dist/AutoComplete.js.map +1 -1
- package/dist/ReactAutoComplete.js +67 -5
- package/dist/ReactAutoComplete.js.map +1 -1
- package/dist/ReactAutoCompleteDropdown.js +67 -5
- package/dist/ReactAutoCompleteDropdown.js.map +1 -1
- package/dist/styles/styles/components/autocomplete.css +744 -0
- package/dist/styles/styles/components/selectedautocomplete.css +221 -0
- package/dist/styles/styles/components/tooltip.css +497 -0
- package/dist/styles/styles/components/treedropdown.css +97 -0
- package/dist/styles/styles/global.css +8 -0
- package/package.json +1 -1
- package/src/AutoComplete.tsx +45 -13
- package/src/ReactAutoComplete.tsx +69 -4
- package/src/ReactAutoCompleteDropdown.tsx +70 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
.qbs-tree-list-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
}
|
|
5
|
+
.qbs-tree-list-container-sub {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: row;
|
|
8
|
+
align-items: flex-start;
|
|
9
|
+
gap: 7px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.qbs-tree-list-container-sub .no-children {
|
|
13
|
+
margin-left: 1.75rem; /* Corresponds to ml-7 in Tailwind */
|
|
14
|
+
}
|
|
15
|
+
.qbs-tree-list-container-sub-icon {
|
|
16
|
+
margin-top: 0.25rem; /* Corresponds to mt-1 in Tailwind */
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
padding-top: 0.5rem; /* Corresponds to py-2 in Tailwind */
|
|
19
|
+
padding-bottom: 0.5rem;
|
|
20
|
+
transition: transform 0.3s ease; /* Smooth rotation transition */
|
|
21
|
+
color: #003c71;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.qbs-tree-list-container-sub-icon.expanded {
|
|
25
|
+
transform: rotate(90deg); /* Corresponds to rotate-90 in Tailwind */
|
|
26
|
+
}
|
|
27
|
+
.qbs-tree-list-container-sub-container {
|
|
28
|
+
display: flex;
|
|
29
|
+
gap: 15px; /* Corresponds to gap-2 in Tailwind */
|
|
30
|
+
padding: 0.5rem; /* Corresponds to p-2 in Tailwind */
|
|
31
|
+
flex-direction: row; /* Corresponds to flex-row in Tailwind */
|
|
32
|
+
width: 100%; /* Corresponds to w-full in Tailwind */
|
|
33
|
+
color: #3d414a;
|
|
34
|
+
}
|
|
35
|
+
.qbs-tree-list-container-sub-container-hideicon {
|
|
36
|
+
display: flex;
|
|
37
|
+
gap: 6px; /* Corresponds to gap-2 in Tailwind */
|
|
38
|
+
padding: 0.5rem; /* Corresponds to p-2 in Tailwind */
|
|
39
|
+
flex-direction: row; /* Corresponds to flex-row in Tailwind */
|
|
40
|
+
width: 100%; /* Corresponds to w-full in Tailwind */
|
|
41
|
+
color: #3d414a;
|
|
42
|
+
}
|
|
43
|
+
.qbs-tree-list-container-sub-container-hideicon:hover {
|
|
44
|
+
color: #003c71;
|
|
45
|
+
background-color: rgba(141, 194, 240, 0.15) !important;
|
|
46
|
+
}
|
|
47
|
+
.qbs-tree-list-container-sub-container:hover {
|
|
48
|
+
color: #003c71;
|
|
49
|
+
background-color: rgba(141, 194, 240, 0.15) !important;
|
|
50
|
+
}
|
|
51
|
+
.qbs-tree-list-container-sub-container.qbs-expanded-child {
|
|
52
|
+
color: #003c71;
|
|
53
|
+
}
|
|
54
|
+
.qbs-tree-list-container-sub-text.qbs-expanded-child {
|
|
55
|
+
color: #003c71;
|
|
56
|
+
}
|
|
57
|
+
.qbs-tree-list-container-sub-text:hover {
|
|
58
|
+
color: #003c71;
|
|
59
|
+
/* background-color: rgba(141, 194, 240, 0.15) !important; */
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.qbs-tree-list-container-sub-container.checked,
|
|
63
|
+
.qbs-tree-list-container-sub-container-hideicon.checked {
|
|
64
|
+
color: #003c71;
|
|
65
|
+
background-color: rgba(
|
|
66
|
+
141,
|
|
67
|
+
194,
|
|
68
|
+
240,
|
|
69
|
+
0.15
|
|
70
|
+
) !important; /* Corresponds to bg-green-200 in Tailwind */
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.marginLeft {
|
|
74
|
+
margin-left: 28px; /* Corresponds to ml-2 in Tailwind */
|
|
75
|
+
}
|
|
76
|
+
.lightmarginLeft {
|
|
77
|
+
margin-left: 19px; /* Corresponds to ml-2 in Tailwind */
|
|
78
|
+
}
|
|
79
|
+
.qbs-tree-list-container-sub- {
|
|
80
|
+
line-height: 18px; /* Corresponds to leading-[18px] in Tailwind */
|
|
81
|
+
font-size: 0.875rem; /* Corresponds to text-sm in Tailwind (14px) */
|
|
82
|
+
}
|
|
83
|
+
.qbs-tree-list-container-child {
|
|
84
|
+
margin-left: 1rem; /* Corresponds to ml-4 in Tailwind */
|
|
85
|
+
margin-top: 0.5rem; /* Corresponds to mt-2 in Tailwind */
|
|
86
|
+
display: flex; /* Corresponds to flex in Tailwind */
|
|
87
|
+
flex-direction: column; /* Corresponds to flex-col in Tailwind */
|
|
88
|
+
gap: 0.5rem; /* Corresponds to gap-2 in Tailwind */
|
|
89
|
+
}
|
|
90
|
+
.qbs-tree-list-container-sub-text {
|
|
91
|
+
line-height: 18px; /* Corresponds to leading-[18px] in Tailwind */
|
|
92
|
+
font-size: 14px; /* Corresponds to text-sm in Tailwind (14px) */
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
letter-spacing: 0.28px;
|
|
95
|
+
font-family: 'Noto Sans';
|
|
96
|
+
/* color: #3d414a; */
|
|
97
|
+
}
|
package/package.json
CHANGED
package/src/AutoComplete.tsx
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
2
9
|
|
|
3
10
|
import { AutoSuggestionInputProps } from './commontypes';
|
|
4
11
|
import { useSuggestions } from './utilities/autosuggestions';
|
|
@@ -43,7 +50,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
|
43
50
|
notDataMessage,
|
|
44
51
|
onFocus,
|
|
45
52
|
},
|
|
46
|
-
ref
|
|
53
|
+
ref,
|
|
47
54
|
) => {
|
|
48
55
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
49
56
|
// State Hooks Section
|
|
@@ -94,16 +101,17 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
|
94
101
|
inputValue,
|
|
95
102
|
isMultiple,
|
|
96
103
|
setNextPage,
|
|
97
|
-
selectedItems
|
|
104
|
+
selectedItems,
|
|
98
105
|
// nextBlock
|
|
99
106
|
);
|
|
100
107
|
|
|
101
108
|
// Handling the selection of a suggestion
|
|
102
109
|
const handleSuggestionClick = useCallback((suggestion: ValueProps) => {
|
|
110
|
+
isSelectingRef.current = true;
|
|
103
111
|
if (isMultiple) {
|
|
104
112
|
setSelectedItems((prev) => {
|
|
105
113
|
const isAdded = prev.some(
|
|
106
|
-
(item) => item[descId] === suggestion[descId]
|
|
114
|
+
(item) => item[descId] === suggestion[descId],
|
|
107
115
|
);
|
|
108
116
|
if (isAdded) {
|
|
109
117
|
return prev.filter((item) => item[descId] !== suggestion[descId]);
|
|
@@ -134,7 +142,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
|
134
142
|
} else {
|
|
135
143
|
setSelectedItems((prev) => {
|
|
136
144
|
return prev.filter(
|
|
137
|
-
(item, i) => item[descId] !== suggestion[descId]
|
|
145
|
+
(item, i) => item[descId] !== suggestion[descId],
|
|
138
146
|
);
|
|
139
147
|
});
|
|
140
148
|
}
|
|
@@ -156,7 +164,15 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
|
156
164
|
setInputValue(value ?? '');
|
|
157
165
|
}, [value]);
|
|
158
166
|
|
|
167
|
+
const isSelectingRef = useRef(false);
|
|
168
|
+
|
|
159
169
|
const selectExactMatchFromSuggestions = () => {
|
|
170
|
+
// If user is clicking on dropdown, don't execute exact match logic
|
|
171
|
+
if (isSelectingRef.current) {
|
|
172
|
+
isSelectingRef.current = false;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
160
176
|
if (!inputValue) {
|
|
161
177
|
return;
|
|
162
178
|
}
|
|
@@ -260,15 +276,15 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
|
260
276
|
type,
|
|
261
277
|
desc,
|
|
262
278
|
selected,
|
|
263
|
-
async
|
|
279
|
+
async,
|
|
264
280
|
);
|
|
265
281
|
const isSelected = (
|
|
266
282
|
item: ValueProps,
|
|
267
|
-
selectedItems: ValueProps[] | string
|
|
283
|
+
selectedItems: ValueProps[] | string,
|
|
268
284
|
): boolean => {
|
|
269
285
|
if (Array.isArray(selectedItems)) {
|
|
270
286
|
return selectedItems.some(
|
|
271
|
-
(selectedItem) => selectedItem[desc] === item[desc]
|
|
287
|
+
(selectedItem) => selectedItem[desc] === item[desc],
|
|
272
288
|
);
|
|
273
289
|
} else {
|
|
274
290
|
return item[desc] === selectedItems;
|
|
@@ -353,14 +369,26 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
|
353
369
|
: searchValue || inputValue
|
|
354
370
|
}
|
|
355
371
|
onChange={handleChange}
|
|
356
|
-
onBlur={() => {
|
|
357
|
-
|
|
358
|
-
|
|
372
|
+
onBlur={(e) => {
|
|
373
|
+
// Delay to allow click events on dropdown to process first
|
|
374
|
+
setTimeout(() => {
|
|
375
|
+
// Check if focus moved to dropdown or if user is selecting
|
|
376
|
+
const relatedTarget = e.relatedTarget as Node;
|
|
377
|
+
if (
|
|
378
|
+
dropdownRef.current &&
|
|
379
|
+
relatedTarget &&
|
|
380
|
+
dropdownRef.current.contains(relatedTarget)
|
|
381
|
+
) {
|
|
382
|
+
return; // Focus moved to dropdown, don't execute exact match
|
|
383
|
+
}
|
|
384
|
+
handleBlur();
|
|
385
|
+
selectExactMatchFromSuggestions();
|
|
386
|
+
}, 200);
|
|
359
387
|
}}
|
|
360
388
|
onFocus={onFocus}
|
|
361
389
|
onClick={() => handleOnClick()}
|
|
362
390
|
className={generateClassName()}
|
|
363
|
-
placeholder={selectedItems?.length > 0 ? '' : placeholder ?? ''}
|
|
391
|
+
placeholder={selectedItems?.length > 0 ? '' : (placeholder ?? '')}
|
|
364
392
|
readOnly={
|
|
365
393
|
readOnly || type === 'custom_select' || type == 'auto_suggestion'
|
|
366
394
|
}
|
|
@@ -469,6 +497,10 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
|
469
497
|
className={`qbs-autocomplete-suggestions-item ${
|
|
470
498
|
isSelected(suggestion, selected) ? 'is-selected' : ''
|
|
471
499
|
}`}
|
|
500
|
+
onMouseDown={(e) => {
|
|
501
|
+
e.preventDefault(); // Prevent input blur before click
|
|
502
|
+
isSelectingRef.current = true;
|
|
503
|
+
}}
|
|
472
504
|
onClick={() => handleSuggestionClick(suggestion)}
|
|
473
505
|
data-testid={suggestion[desc]}
|
|
474
506
|
>
|
|
@@ -523,7 +555,7 @@ const AutoComplete = forwardRef<HTMLInputElement, AutoSuggestionInputProps>(
|
|
|
523
555
|
)}
|
|
524
556
|
</div>
|
|
525
557
|
);
|
|
526
|
-
}
|
|
558
|
+
},
|
|
527
559
|
);
|
|
528
560
|
|
|
529
561
|
export default React.memo(AutoComplete);
|
|
@@ -106,6 +106,7 @@ const ModernAutoComplete: React.FC<AutoSuggestionInputProps> = ({
|
|
|
106
106
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
107
107
|
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
|
|
108
108
|
const scrollContainerRef = useRef<HTMLUListElement | null>(null);
|
|
109
|
+
const isSelectingRef = useRef(false);
|
|
109
110
|
const [dropPosition, setDropPosition] = useState<any>({
|
|
110
111
|
top: 0,
|
|
111
112
|
left: 0,
|
|
@@ -329,6 +330,7 @@ const ModernAutoComplete: React.FC<AutoSuggestionInputProps> = ({
|
|
|
329
330
|
setIsDisabled(disabled);
|
|
330
331
|
}, [disabled]);
|
|
331
332
|
const handleSuggestionClick = (suggestion: valueProps, index: number) => {
|
|
333
|
+
isSelectingRef.current = true;
|
|
332
334
|
onChange({
|
|
333
335
|
...suggestion,
|
|
334
336
|
id: suggestion?.id,
|
|
@@ -338,6 +340,10 @@ const ModernAutoComplete: React.FC<AutoSuggestionInputProps> = ({
|
|
|
338
340
|
if (type !== 'custom_select') setInputValue(suggestion.name);
|
|
339
341
|
setDropOpen(false);
|
|
340
342
|
setSelectedIndex(index);
|
|
343
|
+
// Reset flag after a short delay to allow blur handler to check it
|
|
344
|
+
setTimeout(() => {
|
|
345
|
+
isSelectingRef.current = false;
|
|
346
|
+
}, 100);
|
|
341
347
|
};
|
|
342
348
|
const handleOpen = (e: any) => {
|
|
343
349
|
if (!suggestions || suggestions?.length === 0) {
|
|
@@ -490,6 +496,12 @@ const ModernAutoComplete: React.FC<AutoSuggestionInputProps> = ({
|
|
|
490
496
|
// return 'bottom'
|
|
491
497
|
// }
|
|
492
498
|
const selectExactMatchFromSuggestions = () => {
|
|
499
|
+
// If user is clicking on dropdown, don't execute exact match logic
|
|
500
|
+
if (isSelectingRef.current) {
|
|
501
|
+
isSelectingRef.current = false;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
493
505
|
if (!inputValue) {
|
|
494
506
|
return;
|
|
495
507
|
}
|
|
@@ -630,7 +642,40 @@ const ModernAutoComplete: React.FC<AutoSuggestionInputProps> = ({
|
|
|
630
642
|
|
|
631
643
|
case 'Enter':
|
|
632
644
|
e.preventDefault();
|
|
633
|
-
|
|
645
|
+
if (filteredData[selectedIndex]) {
|
|
646
|
+
handleSuggestionClick(filteredData[selectedIndex], selectedIndex);
|
|
647
|
+
}
|
|
648
|
+
break;
|
|
649
|
+
|
|
650
|
+
case 'Tab':
|
|
651
|
+
// Tab should check for exact match first, then select highlighted item if no exact match
|
|
652
|
+
if (inputValue && filteredData && filteredData.length > 0) {
|
|
653
|
+
const normalizedInput = inputValue.toString().trim().toLowerCase();
|
|
654
|
+
const exactMatchIndex = filteredData.findIndex((item: valueProps) => {
|
|
655
|
+
const name = item?.name?.toString().trim().toLowerCase();
|
|
656
|
+
const label = item?.label?.toString().trim().toLowerCase();
|
|
657
|
+
return name === normalizedInput || label === normalizedInput;
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
if (exactMatchIndex >= 0) {
|
|
661
|
+
// Exact match found - select it
|
|
662
|
+
isSelectingRef.current = true;
|
|
663
|
+
handleSuggestionClick(filteredData[exactMatchIndex], exactMatchIndex);
|
|
664
|
+
} else if (filteredData[selectedIndex]) {
|
|
665
|
+
// No exact match but has highlighted item - select highlighted item
|
|
666
|
+
isSelectingRef.current = true;
|
|
667
|
+
handleSuggestionClick(filteredData[selectedIndex], selectedIndex);
|
|
668
|
+
} else {
|
|
669
|
+
// No exact match and no highlighted item - trigger invalid value action
|
|
670
|
+
onChange({ id: undefined, name: inputValue, from: 4 });
|
|
671
|
+
setInputValue('');
|
|
672
|
+
}
|
|
673
|
+
} else if (inputValue && (!filteredData || filteredData.length === 0)) {
|
|
674
|
+
// No suggestions available - trigger invalid value action
|
|
675
|
+
onChange({ id: undefined, name: inputValue, from: 4 });
|
|
676
|
+
setInputValue('');
|
|
677
|
+
}
|
|
678
|
+
// Don't prevent default - allow Tab to move focus
|
|
634
679
|
break;
|
|
635
680
|
|
|
636
681
|
case 'Escape':
|
|
@@ -682,6 +727,10 @@ const ModernAutoComplete: React.FC<AutoSuggestionInputProps> = ({
|
|
|
682
727
|
} cursor-pointer p-1 text-xxs ps-3.5 pl-[10px] qbs-autocomplete-suggections-items`}
|
|
683
728
|
key={suggestion?.id}
|
|
684
729
|
data-testid={suggestion.name}
|
|
730
|
+
onMouseDown={(e) => {
|
|
731
|
+
e.preventDefault(); // Prevent input blur before click
|
|
732
|
+
isSelectingRef.current = true;
|
|
733
|
+
}}
|
|
685
734
|
onClick={() => handleSuggestionClick(suggestion, index)}
|
|
686
735
|
tabIndex={index}
|
|
687
736
|
ref={(el) => (itemRefs.current[index] = el)}
|
|
@@ -821,9 +870,25 @@ const ModernAutoComplete: React.FC<AutoSuggestionInputProps> = ({
|
|
|
821
870
|
type="text"
|
|
822
871
|
readOnly={readOnly ?? type === 'custom_select'}
|
|
823
872
|
value={inputValue ? inputValue : ''}
|
|
824
|
-
onBlur={() => {
|
|
825
|
-
|
|
826
|
-
|
|
873
|
+
onBlur={(e) => {
|
|
874
|
+
// Check immediately if user is selecting (Tab or click)
|
|
875
|
+
const wasSelecting = isSelectingRef.current;
|
|
876
|
+
|
|
877
|
+
// Delay to allow click events on dropdown to process first
|
|
878
|
+
setTimeout(() => {
|
|
879
|
+
// Check if focus moved to dropdown or if user is selecting
|
|
880
|
+
const relatedTarget = e.relatedTarget as Node;
|
|
881
|
+
if (
|
|
882
|
+
wasSelecting ||
|
|
883
|
+
(dropdownref.current &&
|
|
884
|
+
relatedTarget &&
|
|
885
|
+
dropdownref.current.contains(relatedTarget))
|
|
886
|
+
) {
|
|
887
|
+
return; // User selected an item or focus moved to dropdown, don't execute exact match
|
|
888
|
+
}
|
|
889
|
+
handleClearInputValue();
|
|
890
|
+
selectExactMatchFromSuggestions();
|
|
891
|
+
}, 200);
|
|
827
892
|
}}
|
|
828
893
|
autoComplete="off"
|
|
829
894
|
disabled={disabled}
|
|
@@ -109,6 +109,8 @@ const ModernAutoCompleteDropdown: React.FC<AutoSuggestionInputProps> = ({
|
|
|
109
109
|
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
|
|
110
110
|
const scrollContainerRef = useRef<HTMLUListElement | null>(null);
|
|
111
111
|
const [refetchData, setRefetchData] = useState(false);
|
|
112
|
+
|
|
113
|
+
const isSelectingRef = useRef(false);
|
|
112
114
|
const [dropPosition, setDropPosition] = useState<any>({
|
|
113
115
|
top: 0,
|
|
114
116
|
left: 0,
|
|
@@ -337,6 +339,7 @@ const ModernAutoCompleteDropdown: React.FC<AutoSuggestionInputProps> = ({
|
|
|
337
339
|
setIsDisabled(disabled);
|
|
338
340
|
}, [disabled]);
|
|
339
341
|
const handleSuggestionClick = (suggestion: valueProps, index: number) => {
|
|
342
|
+
isSelectingRef.current = true;
|
|
340
343
|
onChange({
|
|
341
344
|
...suggestion,
|
|
342
345
|
id: suggestion?.id,
|
|
@@ -346,6 +349,10 @@ const ModernAutoCompleteDropdown: React.FC<AutoSuggestionInputProps> = ({
|
|
|
346
349
|
if (type !== 'custom_select') setInputValue(suggestion.name);
|
|
347
350
|
setDropOpen(false);
|
|
348
351
|
setSelectedIndex(index);
|
|
352
|
+
// Reset flag after a short delay to allow blur handler to check it
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
isSelectingRef.current = false;
|
|
355
|
+
}, 100);
|
|
349
356
|
};
|
|
350
357
|
const handleOpen = (e: any) => {
|
|
351
358
|
if (!suggestions || suggestions?.length === 0) handleDropData();
|
|
@@ -512,6 +519,12 @@ const ModernAutoCompleteDropdown: React.FC<AutoSuggestionInputProps> = ({
|
|
|
512
519
|
// return 'bottom'
|
|
513
520
|
// }
|
|
514
521
|
const selectExactMatchFromSuggestions = () => {
|
|
522
|
+
// If user is clicking on dropdown, don't execute exact match logic
|
|
523
|
+
if (isSelectingRef.current) {
|
|
524
|
+
isSelectingRef.current = false;
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
515
528
|
if (!inputValue) {
|
|
516
529
|
return;
|
|
517
530
|
}
|
|
@@ -652,7 +665,40 @@ const ModernAutoCompleteDropdown: React.FC<AutoSuggestionInputProps> = ({
|
|
|
652
665
|
|
|
653
666
|
case 'Enter':
|
|
654
667
|
e.preventDefault();
|
|
655
|
-
|
|
668
|
+
if (filteredData[selectedIndex]) {
|
|
669
|
+
handleSuggestionClick(filteredData[selectedIndex], selectedIndex);
|
|
670
|
+
}
|
|
671
|
+
break;
|
|
672
|
+
|
|
673
|
+
case 'Tab':
|
|
674
|
+
// Tab should check for exact match first, then select highlighted item if no exact match
|
|
675
|
+
if (inputValue && filteredData && filteredData.length > 0) {
|
|
676
|
+
const normalizedInput = inputValue.toString().trim().toLowerCase();
|
|
677
|
+
const exactMatchIndex = filteredData.findIndex((item: valueProps) => {
|
|
678
|
+
const name = item?.name?.toString().trim().toLowerCase();
|
|
679
|
+
const label = item?.label?.toString().trim().toLowerCase();
|
|
680
|
+
return name === normalizedInput || label === normalizedInput;
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
if (exactMatchIndex >= 0) {
|
|
684
|
+
// Exact match found - select it
|
|
685
|
+
isSelectingRef.current = true;
|
|
686
|
+
handleSuggestionClick(filteredData[exactMatchIndex], exactMatchIndex);
|
|
687
|
+
} else if (filteredData[selectedIndex]) {
|
|
688
|
+
// No exact match but has highlighted item - select highlighted item
|
|
689
|
+
isSelectingRef.current = true;
|
|
690
|
+
handleSuggestionClick(filteredData[selectedIndex], selectedIndex);
|
|
691
|
+
} else {
|
|
692
|
+
// No exact match and no highlighted item - trigger invalid value action
|
|
693
|
+
onChange({ id: undefined, name: inputValue, from: 4 });
|
|
694
|
+
setInputValue('');
|
|
695
|
+
}
|
|
696
|
+
} else if (inputValue && (!filteredData || filteredData.length === 0)) {
|
|
697
|
+
// No suggestions available - trigger invalid value action
|
|
698
|
+
onChange({ id: undefined, name: inputValue, from: 4 });
|
|
699
|
+
setInputValue('');
|
|
700
|
+
}
|
|
701
|
+
// Don't prevent default - allow Tab to move focus
|
|
656
702
|
break;
|
|
657
703
|
|
|
658
704
|
case 'Escape':
|
|
@@ -704,6 +750,10 @@ const ModernAutoCompleteDropdown: React.FC<AutoSuggestionInputProps> = ({
|
|
|
704
750
|
} cursor-pointer p-1 text-xxs ps-3.5 pl-[10px] qbs-autocomplete-suggections-items`}
|
|
705
751
|
key={suggestion?.id}
|
|
706
752
|
data-testid={suggestion.name}
|
|
753
|
+
onMouseDown={(e) => {
|
|
754
|
+
e.preventDefault(); // Prevent input blur before click
|
|
755
|
+
isSelectingRef.current = true;
|
|
756
|
+
}}
|
|
707
757
|
onClick={() => handleSuggestionClick(suggestion, index)}
|
|
708
758
|
tabIndex={index}
|
|
709
759
|
ref={(el) => (itemRefs.current[index] = el)}
|
|
@@ -844,9 +894,25 @@ const ModernAutoCompleteDropdown: React.FC<AutoSuggestionInputProps> = ({
|
|
|
844
894
|
type="text"
|
|
845
895
|
readOnly={readOnly ?? type === 'custom_select'}
|
|
846
896
|
value={inputValue ? inputValue : ''}
|
|
847
|
-
onBlur={() => {
|
|
848
|
-
|
|
849
|
-
|
|
897
|
+
onBlur={(e) => {
|
|
898
|
+
// Check immediately if user is selecting (Tab or click)
|
|
899
|
+
const wasSelecting = isSelectingRef.current;
|
|
900
|
+
|
|
901
|
+
// Delay to allow click events on dropdown to process first
|
|
902
|
+
setTimeout(() => {
|
|
903
|
+
// Check if focus moved to dropdown or if user is selecting
|
|
904
|
+
const relatedTarget = e.relatedTarget as Node;
|
|
905
|
+
if (
|
|
906
|
+
wasSelecting ||
|
|
907
|
+
(dropdownref.current &&
|
|
908
|
+
relatedTarget &&
|
|
909
|
+
dropdownref.current.contains(relatedTarget))
|
|
910
|
+
) {
|
|
911
|
+
return; // User selected an item or focus moved to dropdown, don't execute exact match
|
|
912
|
+
}
|
|
913
|
+
handleClearInputValue();
|
|
914
|
+
selectExactMatchFromSuggestions();
|
|
915
|
+
}, 200);
|
|
850
916
|
}}
|
|
851
917
|
autoComplete="off"
|
|
852
918
|
disabled={disabled}
|