react-core-ts 2.0.2 → 2.0.4

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.
@@ -89,6 +89,7 @@ const AutoCompleteWithTabFilter = forwardRef<
89
89
  toolTabClearSwitch = false,
90
90
  onToolTabChange,
91
91
  autoRefreshMainDropdown = true,
92
+ autoTabSelectAll = true
92
93
  },
93
94
  ref
94
95
  ) => {
@@ -113,6 +114,7 @@ const AutoCompleteWithTabFilter = forwardRef<
113
114
  number | undefined
114
115
  >(1);
115
116
  const [visible, setVisible] = useState<boolean>(false);
117
+ const lastEmittedSelectedRef = useRef<ValueProps[] | null>(null);
116
118
  const [activeTab, setActiveTab] = useState<number>(currentTab);
117
119
  const [focusedIndex, setFocusedIndex] = useState(0);
118
120
  const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
@@ -132,10 +134,12 @@ const AutoCompleteWithTabFilter = forwardRef<
132
134
  const [selectAll, setSelectAll] = useState(false);
133
135
  const [refetchData, setRefetchData] = useState(false);
134
136
  const [allDataLoaded, setAllDataLoaded] = useState(false);
137
+ const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
135
138
  const toolsBtnRef = useRef<HTMLButtonElement>(null);
136
139
  const [showToolsTab, setShowToolsTab] = useState(false);
137
140
  // Track previous tabSelectedItems to detect new items
138
141
  const prevTabSelectedItemsRef = useRef<ValueProps[]>([]);
142
+ const [applyTabFilter, setApplyTabFilter] = useState(false);
139
143
 
140
144
  let originalOverflow = '';
141
145
  let hasScrollbar = false;
@@ -187,6 +191,7 @@ const AutoCompleteWithTabFilter = forwardRef<
187
191
  onChange(suggestion);
188
192
  setDropOpen(false);
189
193
  setShowToolsTab(false);
194
+ setAllDataLoaded(false);
190
195
  }
191
196
  },
192
197
  [tab, activeTab, descId, desc, onChange]
@@ -232,6 +237,14 @@ const AutoCompleteWithTabFilter = forwardRef<
232
237
  }
233
238
  }, [isMultiple, descId, desc, onChange]);
234
239
  useEffect(() => {
240
+ // Avoid feedback loops: if props equal last emitted value, skip syncing to state
241
+ if (
242
+ propsSeelctedItems &&
243
+ lastEmittedSelectedRef.current &&
244
+ deepEqual(propsSeelctedItems, lastEmittedSelectedRef.current)
245
+ ) {
246
+ return;
247
+ }
235
248
  // Only update if different
236
249
  if (!deepEqual(selectedItems, propsSeelctedItems)) {
237
250
  setSelectedItems(propsSeelctedItems);
@@ -263,7 +276,7 @@ const AutoCompleteWithTabFilter = forwardRef<
263
276
  setShowToolsTab(false);
264
277
  }
265
278
  }, [handleChangeWithDebounce, tabInlineSearch, onSearchValueChange, typeOnlyFetch]);
266
-
279
+
267
280
  const handleSuggestionChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
268
281
  const { value } = e.target;
269
282
  //setVisibleDrop();
@@ -276,6 +289,7 @@ const AutoCompleteWithTabFilter = forwardRef<
276
289
  const handleClearSelected = useCallback(() => {
277
290
  setSelectedItems([]);
278
291
  setShowAllSelected(false);
292
+ setShowToolsTab(false);
279
293
  if (searchValue && type === 'auto_suggestion' && tabInlineSearch) {
280
294
  setSearchValue('');
281
295
  }
@@ -295,50 +309,124 @@ const AutoCompleteWithTabFilter = forwardRef<
295
309
  }, []);
296
310
 
297
311
  const uniqueDropArrowId = `${name}-drop-arrow-selected-list-icon`;
312
+ const isKeyboardOpenRef = useRef(false);
313
+
314
+ // Keep ref in sync with state for use in event handlers
298
315
  useEffect(() => {
316
+ isKeyboardOpenRef.current = isKeyboardOpen;
317
+ }, [isKeyboardOpen]);
318
+
319
+ useEffect(() => {
320
+ const handleScroll = (event: any) => {
321
+ // On mobile: Don't close dropdown on scroll when keyboard is open
322
+ // On desktop: Always allow closing on scroll
323
+ if (isKeyboardOpenRef.current) {
324
+ // Only prevent closing if keyboard is actually open (mobile scenario)
325
+ // Also check if input is focused to avoid closing while typing
326
+ const activeElement = document.activeElement;
327
+ if (activeElement && (
328
+ activeElement === inputRef.current ||
329
+ activeElement === inputSearchRef.current ||
330
+ (dropRef.current && dropRef.current.contains(activeElement))
331
+ )) {
332
+ return;
333
+ }
334
+ }
335
+
336
+ // Check if scroll is happening within the dropdown itself - if so, don't close
337
+ const scrollTarget = event.target;
338
+ if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
339
+ return;
340
+ }
341
+
342
+ // On desktop (keyboard not open), close on scroll
343
+ if (dropRef.current && dropOpen) {
344
+ setTimeout(() => {
345
+ setDropOpen(false);
346
+ setShowToolsTab(false);
347
+ setAllDataLoaded(false);
348
+ if (!typeOnlyFetch) setSearchValue('');
349
+ }, 200);
350
+ }
351
+ };
352
+
299
353
  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
- ) {
354
+ const target = event.target as HTMLElement;
355
+
356
+ // Always check if click is inside component container first
357
+ const insideComponent = dropdownRef.current ? dropdownRef.current.contains(target) : false;
358
+
359
+ // If clicking inside component, don't close (let onClick handlers handle it)
360
+ if (insideComponent) {
361
+ return;
362
+ }
363
+
364
+ // When keyboard is open, be more lenient - only close on clear outside clicks
365
+ if (isKeyboardOpenRef.current) {
366
+ // Only close if it's a clear click outside, not on input/textarea elements
367
+ if (
368
+ dropRef.current &&
369
+ target instanceof Node &&
370
+ !dropRef.current.contains(target) &&
371
+ target?.id !== uniqueDropArrowId &&
372
+ !target?.closest('.tools-drop-btn') &&
373
+ event.type === 'mousedown' &&
374
+ target.closest &&
375
+ !target.closest('input') &&
376
+ !target.closest('textarea')
377
+ ) {
378
+ setTimeout(() => {
379
+ setDropOpen(false);
380
+ setShowToolsTab(false);
381
+ setAllDataLoaded(false);
382
+ if (!typeOnlyFetch) setSearchValue('');
383
+ }, 200);
384
+ }
385
+ return;
386
+ }
387
+
388
+ // Normal behavior when keyboard is not open
389
+ const clickedToolsBtn = !!target?.closest?.('.tools-drop-btn');
390
+ const insideMainDropdown = dropRef.current ? dropRef.current.contains(target) : false;
391
+ const insideAnyDropdown = !!target?.closest?.('.qbs-autocomplete-selected-suggestions');
392
+
393
+ // Close only when click is outside both dropdowns and not on tools button
394
+ if (!insideMainDropdown && !insideAnyDropdown && !clickedToolsBtn) {
307
395
  setTimeout(() => {
308
396
  setDropOpen(false);
309
397
  setShowToolsTab(false);
398
+ setAllDataLoaded(false);
310
399
  if (!typeOnlyFetch) setSearchValue('');
311
400
  }, 200);
312
- //setSearchValue("");
313
401
  }
314
402
  };
315
403
 
316
404
  document.addEventListener('mousedown', handleClickOutside as any);
317
- window.addEventListener('scroll', handleClickOutside as any);
405
+ window.addEventListener('scroll', handleScroll as any);
318
406
 
319
407
  const scrollableDivs = document.querySelectorAll(
320
408
  'div[style*="overflow"], .overflow-auto, .overflow-y-auto, .overflow-x-auto'
321
409
  );
322
410
  scrollableDivs.forEach((div) =>
323
- div.addEventListener('scroll', handleClickOutside as any)
411
+ div.addEventListener('scroll', handleScroll as any)
324
412
  );
325
413
  if (scrollRef && scrollRef.current && scrollRef.current !== null) {
326
- scrollRef.current.addEventListener('scroll', handleClickOutside as any);
414
+ scrollRef.current.addEventListener('scroll', handleScroll as any);
327
415
  }
328
416
  return () => {
329
417
  document.removeEventListener('mousedown', handleClickOutside as any);
330
- window.removeEventListener('scroll', handleClickOutside as any);
418
+ window.removeEventListener('scroll', handleScroll as any);
331
419
  scrollableDivs.forEach((div) =>
332
- div.removeEventListener('scroll', handleClickOutside as any)
420
+ div.removeEventListener('scroll', handleScroll as any)
333
421
  );
334
422
  if (scrollRef && scrollRef.current && scrollRef.current !== null) {
335
423
  scrollRef.current.removeEventListener(
336
424
  'scroll',
337
- handleClickOutside as any
425
+ handleScroll as any
338
426
  );
339
427
  }
340
428
  };
341
- }, []);
429
+ }, [dropOpen, typeOnlyFetch, uniqueDropArrowId]);
342
430
 
343
431
  // Filtering suggestions based on type and search value
344
432
  const selected: any = isMultiple ? selectedItems : inputValue;
@@ -440,11 +528,30 @@ const AutoCompleteWithTabFilter = forwardRef<
440
528
  };
441
529
  }, [adjustDropdownPosition]);
442
530
 
531
+ // Detect keyboard open on mobile using Visual Viewport API
532
+ useEffect(() => {
533
+ const vv = (window as any).visualViewport;
534
+ if (!vv) return;
535
+ const onResize = () => {
536
+ // Heuristic: if visual viewport height shrinks a lot, keyboard is likely open
537
+ const open = vv.height < window.innerHeight - 100;
538
+ setIsKeyboardOpen(open);
539
+ };
540
+ vv.addEventListener("resize", onResize);
541
+ vv.addEventListener("scroll", onResize); // iOS sometimes fires scroll instead
542
+ onResize();
543
+ return () => {
544
+ vv.removeEventListener("resize", onResize);
545
+ vv.removeEventListener("scroll", onResize);
546
+ };
547
+ }, []);
548
+
443
549
  useEffect(() => {
444
550
  if (isInitialRender) {
445
551
  setIsInitialRender(false);
446
552
  } else {
447
553
  onChange(selectedItems);
554
+ lastEmittedSelectedRef.current = selectedItems;
448
555
  }
449
556
  adjustDropdownPosition();
450
557
  setTimeout(() => {
@@ -506,6 +613,7 @@ const AutoCompleteWithTabFilter = forwardRef<
506
613
  case 'Escape':
507
614
  e.preventDefault();
508
615
  setDropOpen(false);
616
+ setAllDataLoaded(false);
509
617
  break;
510
618
 
511
619
  default:
@@ -591,13 +699,21 @@ const AutoCompleteWithTabFilter = forwardRef<
591
699
  .join(', ')
592
700
  : '';
593
701
 
594
- const setVisibleDrop = () => {
702
+ const setVisibleDrop = (skipAnimation = false) => {
595
703
  hasScrollbar = document.body.scrollHeight > window.innerHeight;
596
704
  if (!originalOverflow && !hasScrollbar) {
597
705
  originalOverflow = document.body.style.overflow;
598
706
  document.body.style.overflow = 'hidden';
599
707
  }
600
708
 
709
+ // If skipAnimation is true (dropdown already open but not visible), just make it visible
710
+ if (skipAnimation && dropOpen) {
711
+ setVisible(true);
712
+ setFocusedIndex(0);
713
+ return;
714
+ }
715
+
716
+ // Normal opening animation
601
717
  setVisible(false);
602
718
  if (!dropOpen) {
603
719
  setDropOpen(true);
@@ -612,7 +728,7 @@ const AutoCompleteWithTabFilter = forwardRef<
612
728
  document.body.style.overflow = originalOverflow || '';
613
729
  }
614
730
  originalOverflow = '';
615
- }, 200);
731
+ }, 100);
616
732
  };
617
733
 
618
734
  const handleTabClick = useCallback((index: number) => {
@@ -746,7 +862,7 @@ const AutoCompleteWithTabFilter = forwardRef<
746
862
  // Add only items from current tab - batch all updates in single setState
747
863
  setSelectedItems((prev) => {
748
864
  const newItems: ValueProps[] = [];
749
-
865
+
750
866
  filteredData.forEach((suggestion: ValueProps) => {
751
867
  const isAdded = prev.some(
752
868
  (item) =>
@@ -757,7 +873,7 @@ const AutoCompleteWithTabFilter = forwardRef<
757
873
  newItems.push(suggestion);
758
874
  }
759
875
  });
760
-
876
+
761
877
  return [...prev, ...newItems];
762
878
  });
763
879
  }
@@ -765,6 +881,22 @@ const AutoCompleteWithTabFilter = forwardRef<
765
881
  }, [isMultiple, selectAll, filteredData, descId]);
766
882
 
767
883
  const handleOpenDropdown = useCallback((e: any) => {
884
+ if (!tabInlineSearch) {
885
+ // Toggle the main dropdown when not using inline search
886
+ if (dropOpen && visible && allDataLoaded) {
887
+ // If dropdown is open and visible, close it
888
+ setDropOpen(false);
889
+ setVisible(false);
890
+ setAllDataLoaded(false);
891
+ return
892
+ } else if (!dropOpen) {
893
+ // If dropdown is closed, open it
894
+ setVisibleDrop(false);
895
+ } else if (dropOpen && !visible) {
896
+ // If dropdown is open but not visible, make it visible
897
+ setVisible(true);
898
+ }
899
+ }
768
900
  if (!suggestions || suggestions?.length === 0 || refetchData) {
769
901
  if (autoDropdown && (inputValue === '' || inputValue.trim() === '')) {
770
902
  if (tabInlineSearch && tab.length > 0) {
@@ -788,13 +920,53 @@ const AutoCompleteWithTabFilter = forwardRef<
788
920
  setAllDataLoaded(false);
789
921
  }
790
922
  }
791
- }, [suggestions, refetchData, autoDropdown, inputValue, tabInlineSearch, tab, activeTab, handlePickSuggestions]);
923
+ }, [suggestions, refetchData, autoDropdown, inputValue, tabInlineSearch, tab, activeTab, handlePickSuggestions, dropOpen, visible]);
924
+
925
+ useEffect(() => {
926
+ if (applyTabFilter && !isLoading && suggestions) {
927
+ // Auto select all currently loaded suggestions (dedup by descId)
928
+ if (isMultiple && Array.isArray(suggestions) && autoTabSelectAll) {
929
+ setSelectedItems((prev) => {
930
+ const existingIds = new Set(
931
+ (prev || []).map((item) => getKeyValue(item, descId, 'id'))
932
+ );
933
+ const toAdd: ValueProps[] = [];
934
+ suggestions.forEach((s: ValueProps) => {
935
+ const sid = getKeyValue(s, descId, 'id');
936
+ if (!existingIds.has(sid)) {
937
+ toAdd.push(s);
938
+ }
939
+ });
940
+ return toAdd.length > 0 ? [...prev, ...toAdd] : prev;
941
+ });
942
+ }
943
+
944
+ // Clear tabSelectedItems after applying filter and data is loaded
945
+ onToolTabChange?.([]);
946
+ setShowToolsTab(false);
947
+ // Open dropdown immediately when data is ready (tools tab was open so dropdown is closed)
948
+ if (!dropOpen) {
949
+ setVisibleDrop(false); // Full animation for closed dropdown
950
+ } else if (dropOpen && !visible) {
951
+ // If for some reason dropdown is open but not visible, just make it visible
952
+ setVisible(true);
953
+ }
954
+ setApplyTabFilter(false);
955
+ }
956
+ }, [applyTabFilter, isLoading, suggestions, onToolTabChange, isMultiple, descId, dropOpen, visible]);
792
957
 
793
958
  const handleOpenToolsTab = useCallback((e: any) => {
794
959
  setShowToolsTab(showToolsTab ? false : true);
795
960
  setDropOpen(false);
961
+ setAllDataLoaded(false);
796
962
  }, [showToolsTab]);
797
963
 
964
+ const handleApplyTabFilter = useCallback(() => {
965
+ // Set flag to trigger effect when data loads
966
+ setApplyTabFilter(true);
967
+ handlePickSuggestions(searchValue ? searchValue : '*', 1);
968
+ }, [searchValue, handlePickSuggestions]);
969
+
798
970
  useEffect(() => {
799
971
  const allSelected =
800
972
  filteredData?.length > 0 && selectedItems?.length > 0 &&
@@ -830,14 +1002,14 @@ const AutoCompleteWithTabFilter = forwardRef<
830
1002
  // Check if there are changes by comparing with previous state
831
1003
  const hasChanges = tabSelectedItems.length !== prevTabSelectedItemsRef.current.length ||
832
1004
  !deepEqual(tabSelectedItems, prevTabSelectedItemsRef.current);
833
-
1005
+
834
1006
  // Trigger refresh if there are changes (items added, removed, or modified)
835
1007
  if (hasChanges) {
836
1008
  if (tabInlineSearch && toolTabs.length > 0 && searchValue) {
837
1009
  handlePickSuggestions(searchValue, 1);
838
1010
  }
839
1011
  }
840
-
1012
+
841
1013
  // Update the previous state
842
1014
  prevTabSelectedItemsRef.current = tabSelectedItems;
843
1015
  }
@@ -872,12 +1044,13 @@ const AutoCompleteWithTabFilter = forwardRef<
872
1044
  className={`qbs-relative qbs-autocomplete-selected-comp ${expandable ? 'qbs-expandable-container' : 'qbs-container'
873
1045
  }`}
874
1046
  >
1047
+
875
1048
  {(selectedItems?.length > 0 || !tabInlineSearch) && (
876
1049
  <>
877
1050
  {!countOnly ? (
878
1051
  getSelectedItems(false)
879
1052
  ) : !tabInlineSearch ? (
880
- <div className="selected-items-counter-container qbs-text-sm qbs-gap-1">
1053
+ <div className={`selected-items-counter-container selected-item-inline-counter qbs-text-sm qbs-gap-1 ${!tabInlineSearch && autoDropdown ? 'selected-item-tool-direct-counter' : ''}`}>
881
1054
  <Tooltip title={allSelectedTooltipContent} enabled={selectedItems?.length > 0 ? true : false}>
882
1055
  <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
1056
  {selectedItems?.length}
@@ -907,6 +1080,28 @@ const AutoCompleteWithTabFilter = forwardRef<
907
1080
  </>
908
1081
  )}
909
1082
 
1083
+ <>
1084
+ {autoDropdown && !tabInlineSearch && !disabled && !readOnly && (
1085
+ <div className='qbs-inline-all-dropdown-btn'>
1086
+ <button
1087
+ disabled={disabled ?? readOnly}
1088
+ onClick={(e) => handleOpenDropdown(e)}
1089
+ className="text-[#667085] focus-visible:outline-slate-100 absolute right-2 qbs-all-dropdown-btn"
1090
+ data-testid="drop-arrow"
1091
+ type="button"
1092
+ id="autocomplete-drop-icon"
1093
+ ref={dropBtnRef}
1094
+ >
1095
+ <AllDropArrow
1096
+ type={!allDataLoaded ? 'down' : 'up'}
1097
+ uniqueId="all-dropdow-arrow-icon"
1098
+ className="all-dropdow-arrow-icon"
1099
+ />
1100
+ </button>
1101
+ </div>
1102
+ )}
1103
+ </>
1104
+
910
1105
  <div
911
1106
  className={`qbs-textfield-expandable ${!expandable ? 'qbs-normal' : ''
912
1107
  }`}
@@ -1164,6 +1359,7 @@ const AutoCompleteWithTabFilter = forwardRef<
1164
1359
  }`}
1165
1360
  onClick={() => {
1166
1361
  setDropOpen(false);
1362
+ setAllDataLoaded(false);
1167
1363
  }}
1168
1364
  >
1169
1365
  Done
@@ -1223,6 +1419,8 @@ const AutoCompleteWithTabFilter = forwardRef<
1223
1419
  open={showToolsTab}
1224
1420
  data={tabData}
1225
1421
  enableSelectAll={enableSelectAll}
1422
+ applyTabFilter={handleApplyTabFilter}
1423
+ onToolClose={() => setShowToolsTab(false)}
1226
1424
  />)
1227
1425
  }
1228
1426
  </div>