react-core-ts 2.0.21 → 2.0.22

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,30 @@
1
+ export declare const MIN_DROPDOWN_WIDTH_PX = 300;
2
+ export declare const MAX_DROPDOWN_WIDTH_WITH_TABS_PX = 600;
3
+ /**
4
+ * Measures the "natural" (nowrap) width of a rendered tabs <ul>, including the dropdown's horizontal padding.
5
+ * This avoids tabs wrapping/squishing when the trigger/input is narrower than the tab labels.
6
+ */
7
+ export declare function measureTabListNaturalWidth(tabListEl: HTMLUListElement): number | null;
8
+ /**
9
+ * Keeps a dropdown fully visible within the viewport by clamping width and shifting left/right as needed.
10
+ * All coordinates are page coordinates (i.e., include window.scrollX).
11
+ */
12
+ export declare function clampDropdownToViewport(opts: {
13
+ desiredWidth: number;
14
+ desiredLeft: number;
15
+ }): {
16
+ width: number;
17
+ left: number;
18
+ };
19
+ /**
20
+ * Same as clampDropdownToViewport, but if the dropdown would overflow the right edge, it will prefer
21
+ * right-aligning the dropdown to the provided anchor (typically the input's right edge).
22
+ */
23
+ export declare function clampDropdownToViewportPreferRightAlign(opts: {
24
+ desiredWidth: number;
25
+ desiredLeft: number;
26
+ anchorRight: number;
27
+ }): {
28
+ width: number;
29
+ left: number;
30
+ };
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clampDropdownToViewportPreferRightAlign = exports.clampDropdownToViewport = exports.measureTabListNaturalWidth = exports.MAX_DROPDOWN_WIDTH_WITH_TABS_PX = exports.MIN_DROPDOWN_WIDTH_PX = void 0;
4
+ var DROPDOWN_HORIZONTAL_PADDING_PX = 16; // .qbs-autocomplete-suggestions has padding: 8px
5
+ var VIEWPORT_RIGHT_GUTTER_PX = 8;
6
+ var VIEWPORT_LEFT_GUTTER_PX = 8;
7
+ exports.MIN_DROPDOWN_WIDTH_PX = 300;
8
+ // When tabs exist, cap dropdown width and let the tab strip scroll horizontally if needed.
9
+ exports.MAX_DROPDOWN_WIDTH_WITH_TABS_PX = 600;
10
+ /**
11
+ * Measures the "natural" (nowrap) width of a rendered tabs <ul>, including the dropdown's horizontal padding.
12
+ * This avoids tabs wrapping/squishing when the trigger/input is narrower than the tab labels.
13
+ */
14
+ function measureTabListNaturalWidth(tabListEl) {
15
+ if (!tabListEl || typeof document === 'undefined')
16
+ return null;
17
+ try {
18
+ var container = document.createElement('div');
19
+ container.style.position = 'absolute';
20
+ container.style.top = '-99999px';
21
+ container.style.left = '-99999px';
22
+ container.style.visibility = 'hidden';
23
+ container.style.pointerEvents = 'none';
24
+ container.style.whiteSpace = 'nowrap';
25
+ var clone = tabListEl.cloneNode(true);
26
+ // Force a single row with natural width.
27
+ clone.style.display = 'inline-flex';
28
+ clone.style.flexWrap = 'nowrap';
29
+ clone.style.width = 'max-content';
30
+ clone.style.whiteSpace = 'nowrap';
31
+ // Remove width constraints from items so content can dictate width.
32
+ clone.querySelectorAll('li').forEach(function (li) {
33
+ li.style.flex = '0 0 auto';
34
+ li.style.width = 'auto';
35
+ });
36
+ clone.querySelectorAll('span').forEach(function (span) {
37
+ span.style.width = 'max-content';
38
+ span.style.whiteSpace = 'nowrap';
39
+ });
40
+ container.appendChild(clone);
41
+ document.body.appendChild(container);
42
+ var tabRowWidth = Math.ceil(clone.getBoundingClientRect().width);
43
+ document.body.removeChild(container);
44
+ // Include dropdown padding so the tabs still fit within the content box.
45
+ return tabRowWidth + DROPDOWN_HORIZONTAL_PADDING_PX;
46
+ }
47
+ catch (_a) {
48
+ return null;
49
+ }
50
+ }
51
+ exports.measureTabListNaturalWidth = measureTabListNaturalWidth;
52
+ /**
53
+ * Keeps a dropdown fully visible within the viewport by clamping width and shifting left/right as needed.
54
+ * All coordinates are page coordinates (i.e., include window.scrollX).
55
+ */
56
+ function clampDropdownToViewport(opts) {
57
+ if (typeof window === 'undefined')
58
+ return { width: opts.desiredWidth, left: opts.desiredLeft };
59
+ var viewportLeft = window.scrollX;
60
+ var viewportRight = window.scrollX + window.innerWidth;
61
+ var maxWidth = Math.max(0, Math.floor(viewportRight - viewportLeft - VIEWPORT_LEFT_GUTTER_PX - VIEWPORT_RIGHT_GUTTER_PX));
62
+ var width = maxWidth > 0 ? Math.min(opts.desiredWidth, maxWidth) : opts.desiredWidth;
63
+ var minLeft = viewportLeft + VIEWPORT_LEFT_GUTTER_PX;
64
+ var maxLeft = viewportRight - VIEWPORT_RIGHT_GUTTER_PX - width;
65
+ var left = Math.min(Math.max(opts.desiredLeft, minLeft), maxLeft);
66
+ return { width: width, left: left };
67
+ }
68
+ exports.clampDropdownToViewport = clampDropdownToViewport;
69
+ /**
70
+ * Same as clampDropdownToViewport, but if the dropdown would overflow the right edge, it will prefer
71
+ * right-aligning the dropdown to the provided anchor (typically the input's right edge).
72
+ */
73
+ function clampDropdownToViewportPreferRightAlign(opts) {
74
+ if (typeof window === 'undefined')
75
+ return { width: opts.desiredWidth, left: opts.desiredLeft };
76
+ var viewportLeft = window.scrollX;
77
+ var viewportRight = window.scrollX + window.innerWidth;
78
+ var maxWidth = Math.max(0, Math.floor(viewportRight - viewportLeft - VIEWPORT_LEFT_GUTTER_PX - VIEWPORT_RIGHT_GUTTER_PX));
79
+ var width = maxWidth > 0 ? Math.min(opts.desiredWidth, maxWidth) : opts.desiredWidth;
80
+ var minLeft = viewportLeft + VIEWPORT_LEFT_GUTTER_PX;
81
+ var maxLeft = viewportRight - VIEWPORT_RIGHT_GUTTER_PX - width;
82
+ // Default: left-align to the trigger.
83
+ var left = opts.desiredLeft;
84
+ // If it would overflow right, prefer right-align to the trigger's right edge.
85
+ if (left + width > viewportRight - VIEWPORT_RIGHT_GUTTER_PX) {
86
+ left = opts.anchorRight - width;
87
+ }
88
+ left = Math.min(Math.max(left, minLeft), maxLeft);
89
+ return { width: width, left: left };
90
+ }
91
+ exports.clampDropdownToViewportPreferRightAlign = clampDropdownToViewportPreferRightAlign;
92
+ //# sourceMappingURL=measureTabListNaturalWidth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measureTabListNaturalWidth.js","sourceRoot":"","sources":["../../src/utilities/measureTabListNaturalWidth.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-core-ts",
3
- "version": "2.0.21",
3
+ "version": "2.0.22",
4
4
  "description": "React Components",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,12 @@ import { filterSuggestions } from './utilities/filterSuggestions';
18
18
  import { AllDropArrow, DropArrow, Search, Spinner } from './utilities/icons';
19
19
  import { default as Tooltip } from './utilities/NewTooltip';
20
20
  import { getKeyValue, safeToLowerString } from './utilities/getKeyValue';
21
+ import {
22
+ clampDropdownToViewportPreferRightAlign,
23
+ MAX_DROPDOWN_WIDTH_WITH_TABS_PX,
24
+ MIN_DROPDOWN_WIDTH_PX,
25
+ measureTabListNaturalWidth,
26
+ } from './utilities/measureTabListNaturalWidth';
21
27
 
22
28
  const AutoCompleteWithSelectedList = forwardRef<
23
29
  HTMLInputElement,
@@ -119,6 +125,7 @@ const AutoCompleteWithSelectedList = forwardRef<
119
125
  const [refetchData, setRefetchData] = useState(false);
120
126
  const [allDataLoaded, setAllDataLoaded] = useState(false);
121
127
  const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
128
+ const [tabsScrollable, setTabsScrollable] = useState(false);
122
129
 
123
130
  let originalOverflow = '';
124
131
  let hasScrollbar = false;
@@ -131,6 +138,23 @@ const AutoCompleteWithSelectedList = forwardRef<
131
138
  width: inputRect.width,
132
139
  top: 0,
133
140
  };
141
+ const tabsNaturalWidth = tabRef.current
142
+ ? measureTabListNaturalWidth(tabRef.current)
143
+ : null;
144
+ const widthBeforeClamp = Math.min(
145
+ Math.max(dropdownPosition.width, tabsNaturalWidth ?? 0, MIN_DROPDOWN_WIDTH_PX),
146
+ tab.length > 0 ? MAX_DROPDOWN_WIDTH_WITH_TABS_PX : Number.POSITIVE_INFINITY
147
+ );
148
+ const { width: desiredWidth, left: desiredLeft } =
149
+ clampDropdownToViewportPreferRightAlign({
150
+ desiredWidth: widthBeforeClamp,
151
+ desiredLeft: dropdownPosition.left,
152
+ anchorRight: inputRect.right + window.scrollX,
153
+ });
154
+ setTabsScrollable((prev) => {
155
+ const next = tab.length > 0 && (tabsNaturalWidth ?? 0) > desiredWidth;
156
+ return prev === next ? prev : next;
157
+ });
134
158
  // Check if there's enough space below the input for the dropdown
135
159
  const spaceBelow = window.innerHeight - inputRect.bottom;
136
160
  const spaceAbove = inputRect.top + window.scrollY;
@@ -184,8 +208,8 @@ const AutoCompleteWithSelectedList = forwardRef<
184
208
  setDropdownStyle((prevStyle) => ({
185
209
  ...prevStyle,
186
210
  transform: `translateY(${dropdownPosition.top}px)`,
187
- left: inputRect.left + window.scrollX,
188
- width: inputRect.width,
211
+ left: desiredLeft,
212
+ width: desiredWidth,
189
213
  }));
190
214
  }
191
215
  };
@@ -383,6 +407,7 @@ const AutoCompleteWithSelectedList = forwardRef<
383
407
 
384
408
  const uniqueDropArrowId = `${name}-drop-arrow-selected-list-icon`;
385
409
  const isKeyboardOpenRef = useRef(false);
410
+ const closeOnScrollTimeoutRef = useRef<number | null>(null);
386
411
 
387
412
  // Keep ref in sync with state for use in event handlers
388
413
  useEffect(() => {
@@ -391,7 +416,13 @@ const AutoCompleteWithSelectedList = forwardRef<
391
416
 
392
417
  useEffect(() => {
393
418
  const handleScroll = (event: any) => {
394
- // On mobile: Don't close dropdown on scroll when keyboard is open
419
+ // Don't close if the user is scrolling inside the dropdown itself.
420
+ const scrollTarget = event.target;
421
+ if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
422
+ return;
423
+ }
424
+
425
+ // On mobile: Don't close dropdown on scroll when keyboard is open (typing scenario)
395
426
  // On desktop: Always allow closing on scroll
396
427
  if (isKeyboardOpenRef.current) {
397
428
  // Only prevent closing if keyboard is actually open (mobile scenario)
@@ -406,15 +437,12 @@ const AutoCompleteWithSelectedList = forwardRef<
406
437
  }
407
438
  }
408
439
 
409
- // Check if scroll is happening within the dropdown itself - if so, don't close
410
- const scrollTarget = event.target;
411
- if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
412
- return;
413
- }
414
-
415
- // On desktop (keyboard not open), close on scroll
416
- if (dropRef.current && dropOpen) {
417
- setTimeout(() => {
440
+ // Close on any outside scroll (including scrolls on nested containers).
441
+ if (dropOpen) {
442
+ if (closeOnScrollTimeoutRef.current) {
443
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
444
+ }
445
+ closeOnScrollTimeoutRef.current = window.setTimeout(() => {
418
446
  setDropOpen(false);
419
447
  if (!typeOnlyFetch) setSearchValue('');
420
448
  }, 200);
@@ -460,6 +488,8 @@ const AutoCompleteWithSelectedList = forwardRef<
460
488
 
461
489
  document.addEventListener('mousedown', handleClickOutside as any);
462
490
  window.addEventListener('scroll', handleScroll as any);
491
+ // Capture-phase scroll catches scrolls from any scroll container (scroll doesn't bubble).
492
+ document.addEventListener('scroll', handleScroll as any, true);
463
493
 
464
494
  const scrollableDivs = document.querySelectorAll(
465
495
  'div[style*="overflow"], .overflow-auto, .overflow-y-auto, .overflow-x-auto'
@@ -473,9 +503,14 @@ const AutoCompleteWithSelectedList = forwardRef<
473
503
  return () => {
474
504
  document.removeEventListener('mousedown', handleClickOutside as any);
475
505
  window.removeEventListener('scroll', handleScroll as any);
506
+ document.removeEventListener('scroll', handleScroll as any, true);
476
507
  scrollableDivs.forEach((div) =>
477
508
  div.removeEventListener('scroll', handleScroll as any)
478
509
  );
510
+ if (closeOnScrollTimeoutRef.current) {
511
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
512
+ closeOnScrollTimeoutRef.current = null;
513
+ }
479
514
  if (scrollRef && scrollRef.current && scrollRef.current !== null) {
480
515
  scrollRef.current.removeEventListener(
481
516
  'scroll',
@@ -774,7 +809,7 @@ const AutoCompleteWithSelectedList = forwardRef<
774
809
  const getTabItems = () => {
775
810
  return (
776
811
  <ul
777
- className="qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2"
812
+ className={`qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2 ${tabsScrollable ? 'qbs-tab-scroll' : ''}`}
778
813
  ref={tabRef}
779
814
  >
780
815
  {tab.map((item: TabPops, idx: number) => (
@@ -17,6 +17,12 @@ import { filterSuggestions } from './utilities/filterSuggestions';
17
17
  import { AllDropArrow, DropArrow, Search, Spinner, Tools } from './utilities/icons';
18
18
  import { default as Tooltip } from './utilities/NewTooltip';
19
19
  import { getKeyValue, safeToLowerString } from './utilities/getKeyValue';
20
+ import {
21
+ clampDropdownToViewportPreferRightAlign,
22
+ MAX_DROPDOWN_WIDTH_WITH_TABS_PX,
23
+ MIN_DROPDOWN_WIDTH_PX,
24
+ measureTabListNaturalWidth,
25
+ } from './utilities/measureTabListNaturalWidth';
20
26
  import DropdownFilterTabs from './dropdownFilterTabs';
21
27
 
22
28
  const AutoCompleteWithTabFilter = forwardRef<
@@ -140,6 +146,7 @@ const AutoCompleteWithTabFilter = forwardRef<
140
146
  const [refetchData, setRefetchData] = useState(false);
141
147
  const [allDataLoaded, setAllDataLoaded] = useState(false);
142
148
  const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
149
+ const [tabsScrollable, setTabsScrollable] = useState(false);
143
150
  const toolsBtnRef = useRef<HTMLButtonElement>(null);
144
151
  const [showToolsTab, setShowToolsTab] = useState(false);
145
152
  // Track previous tabSelectedItems to detect new items
@@ -317,6 +324,7 @@ const AutoCompleteWithTabFilter = forwardRef<
317
324
 
318
325
  const uniqueDropArrowId = `${name}-drop-arrow-selected-list-icon`;
319
326
  const isKeyboardOpenRef = useRef(false);
327
+ const closeOnScrollTimeoutRef = useRef<number | null>(null);
320
328
 
321
329
  // Keep ref in sync with state for use in event handlers
322
330
  useEffect(() => {
@@ -325,7 +333,13 @@ const AutoCompleteWithTabFilter = forwardRef<
325
333
 
326
334
  useEffect(() => {
327
335
  const handleScroll = (event: any) => {
328
- // On mobile: Don't close dropdown on scroll when keyboard is open
336
+ // Don't close if the user is scrolling inside the dropdown itself.
337
+ const scrollTarget = event.target;
338
+ if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
339
+ return;
340
+ }
341
+
342
+ // On mobile: Don't close dropdown on scroll when keyboard is open (typing scenario)
329
343
  // On desktop: Always allow closing on scroll
330
344
  if (isKeyboardOpenRef.current) {
331
345
  // Only prevent closing if keyboard is actually open (mobile scenario)
@@ -340,15 +354,12 @@ const AutoCompleteWithTabFilter = forwardRef<
340
354
  }
341
355
  }
342
356
 
343
- // Check if scroll is happening within the dropdown itself - if so, don't close
344
- const scrollTarget = event.target;
345
- if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
346
- return;
347
- }
348
-
349
- // On desktop (keyboard not open), close on scroll
350
- if (dropRef.current && dropOpen) {
351
- setTimeout(() => {
357
+ // Close on any outside scroll (including scrolls on nested containers).
358
+ if (dropOpen) {
359
+ if (closeOnScrollTimeoutRef.current) {
360
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
361
+ }
362
+ closeOnScrollTimeoutRef.current = window.setTimeout(() => {
352
363
  setDropOpen(false);
353
364
  setShowToolsTab(false);
354
365
  setAllDataLoaded(false);
@@ -410,6 +421,8 @@ const AutoCompleteWithTabFilter = forwardRef<
410
421
 
411
422
  document.addEventListener('mousedown', handleClickOutside as any);
412
423
  window.addEventListener('scroll', handleScroll as any);
424
+ // Capture-phase scroll catches scrolls from any scroll container (scroll doesn't bubble).
425
+ document.addEventListener('scroll', handleScroll as any, true);
413
426
 
414
427
  const scrollableDivs = document.querySelectorAll(
415
428
  'div[style*="overflow"], .overflow-auto, .overflow-y-auto, .overflow-x-auto'
@@ -423,9 +436,14 @@ const AutoCompleteWithTabFilter = forwardRef<
423
436
  return () => {
424
437
  document.removeEventListener('mousedown', handleClickOutside as any);
425
438
  window.removeEventListener('scroll', handleScroll as any);
439
+ document.removeEventListener('scroll', handleScroll as any, true);
426
440
  scrollableDivs.forEach((div) =>
427
441
  div.removeEventListener('scroll', handleScroll as any)
428
442
  );
443
+ if (closeOnScrollTimeoutRef.current) {
444
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
445
+ closeOnScrollTimeoutRef.current = null;
446
+ }
429
447
  if (scrollRef && scrollRef.current && scrollRef.current !== null) {
430
448
  scrollRef.current.removeEventListener(
431
449
  'scroll',
@@ -457,6 +475,22 @@ const AutoCompleteWithTabFilter = forwardRef<
457
475
  width: inputRect.width,
458
476
  top: 0,
459
477
  };
478
+ const tabsNaturalWidth = tabRef.current
479
+ ? measureTabListNaturalWidth(tabRef.current)
480
+ : null;
481
+ const widthBeforeClamp = Math.min(
482
+ Math.max(dropdownPosition.width, tabsNaturalWidth ?? 0, MIN_DROPDOWN_WIDTH_PX),
483
+ tab.length > 0 ? MAX_DROPDOWN_WIDTH_WITH_TABS_PX : Number.POSITIVE_INFINITY
484
+ );
485
+ const { width: desiredWidth, left: desiredLeft } = clampDropdownToViewportPreferRightAlign({
486
+ desiredWidth: widthBeforeClamp,
487
+ desiredLeft: dropdownPosition.left,
488
+ anchorRight: inputRect.right + window.scrollX,
489
+ });
490
+ setTabsScrollable((prev) => {
491
+ const next = tab.length > 0 && (tabsNaturalWidth ?? 0) > desiredWidth;
492
+ return prev === next ? prev : next;
493
+ });
460
494
  // Check if there's enough space below the input for the dropdown
461
495
  const spaceBelow = window.innerHeight - inputRect.bottom;
462
496
  const spaceAbove = inputRect.top + window.scrollY;
@@ -512,8 +546,8 @@ const AutoCompleteWithTabFilter = forwardRef<
512
546
  const newStyle = {
513
547
  ...prevStyle,
514
548
  transform: `translateY(${dropdownPosition.top}px)`,
515
- left: inputRect.left + window.scrollX,
516
- width: inputRect.width,
549
+ left: desiredLeft,
550
+ width: desiredWidth,
517
551
  };
518
552
  // Only update if the style actually changed
519
553
  if (prevStyle.transform !== newStyle.transform ||
@@ -833,7 +867,7 @@ const AutoCompleteWithTabFilter = forwardRef<
833
867
  const getTabItems = useCallback(() => {
834
868
  return (
835
869
  <ul
836
- className="qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2"
870
+ className={`qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2 ${tabsScrollable ? 'qbs-tab-scroll' : ''}`}
837
871
  ref={tabRef}
838
872
  >
839
873
  {tab.map((item: TabPops, idx: number) => (
@@ -851,7 +885,7 @@ const AutoCompleteWithTabFilter = forwardRef<
851
885
  ))}
852
886
  </ul>
853
887
  );
854
- }, [tab, activeTab, handleTabClick]);
888
+ }, [tab, activeTab, handleTabClick, tabsScrollable]);
855
889
 
856
890
  const handleSelectAll = useCallback(() => {
857
891
  if (isMultiple) {
@@ -18,6 +18,12 @@ import { filterSuggestions } from './utilities/filterSuggestions';
18
18
  import { AllDropArrow, DropArrow, Search, Spinner } from './utilities/icons';
19
19
  import { default as Tooltip } from './utilities/NewTooltip';
20
20
  import { getKeyValue, safeToLowerString } from './utilities/getKeyValue';
21
+ import {
22
+ clampDropdownToViewportPreferRightAlign,
23
+ MAX_DROPDOWN_WIDTH_WITH_TABS_PX,
24
+ MIN_DROPDOWN_WIDTH_PX,
25
+ measureTabListNaturalWidth,
26
+ } from './utilities/measureTabListNaturalWidth';
21
27
 
22
28
  const DropdownFilterTabs = forwardRef<
23
29
  HTMLInputElement,
@@ -137,6 +143,7 @@ const DropdownFilterTabs = forwardRef<
137
143
  const [allDataLoaded, setAllDataLoaded] = useState(false);
138
144
  const [isKeyboardOpen, setIsKeyboardOpen] = useState(false);
139
145
  const [hasMoreOptionValues, setHasMoreOptionValues] = useState(false);
146
+ const [tabsScrollable, setTabsScrollable] = useState(false);
140
147
 
141
148
  // Use external reset key if provided, otherwise default to 0
142
149
  const currentResetKey = externalResetKey !== undefined ? externalResetKey : 0;
@@ -171,6 +178,23 @@ const DropdownFilterTabs = forwardRef<
171
178
  width: inputRect.width,
172
179
  top: 0,
173
180
  };
181
+ const hasTabs = tab.length > 0 || !!moreOptionTab;
182
+ const tabsNaturalWidth = tabRef.current
183
+ ? measureTabListNaturalWidth(tabRef.current)
184
+ : null;
185
+ const widthBeforeClamp = Math.min(
186
+ Math.max(dropdownPosition.width, tabsNaturalWidth ?? 0, MIN_DROPDOWN_WIDTH_PX),
187
+ hasTabs ? MAX_DROPDOWN_WIDTH_WITH_TABS_PX : Number.POSITIVE_INFINITY
188
+ );
189
+ const { width: desiredWidth, left: desiredLeft } = clampDropdownToViewportPreferRightAlign({
190
+ desiredWidth: widthBeforeClamp,
191
+ desiredLeft: dropdownPosition.left,
192
+ anchorRight: inputRect.right + window.scrollX,
193
+ });
194
+ setTabsScrollable((prev) => {
195
+ const next = hasTabs && (tabsNaturalWidth ?? 0) > desiredWidth;
196
+ return prev === next ? prev : next;
197
+ });
174
198
  // Check if there's enough space below the input for the dropdown
175
199
  const spaceBelow = window.innerHeight - inputRect.bottom;
176
200
  const spaceAbove = inputRect.top + window.scrollY;
@@ -224,8 +248,8 @@ const DropdownFilterTabs = forwardRef<
224
248
  setDropdownStyle((prevStyle) => ({
225
249
  ...prevStyle,
226
250
  transform: `translateY(${dropdownPosition.top}px)`,
227
- left: inputRect.left + window.scrollX,
228
- width: inputRect.width,
251
+ left: desiredLeft,
252
+ width: desiredWidth,
229
253
  }));
230
254
 
231
255
  }
@@ -386,6 +410,7 @@ const DropdownFilterTabs = forwardRef<
386
410
 
387
411
  const uniqueDropArrowId = `${name}-drop-arrow-selected-list-icon`;
388
412
  const isKeyboardOpenRef = useRef(false);
413
+ const closeOnScrollTimeoutRef = useRef<number | null>(null);
389
414
 
390
415
  // Keep ref in sync with state for use in event handlers
391
416
  useEffect(() => {
@@ -412,7 +437,13 @@ const DropdownFilterTabs = forwardRef<
412
437
 
413
438
  useEffect(() => {
414
439
  const handleScroll = (event: any) => {
415
- // On mobile: Don't close dropdown on scroll when keyboard is open
440
+ // Don't close if the user is scrolling inside the dropdown itself.
441
+ const scrollTarget = event.target;
442
+ if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
443
+ return;
444
+ }
445
+
446
+ // On mobile: Don't close dropdown on scroll when keyboard is open (typing scenario)
416
447
  // On desktop: Always allow closing on scroll
417
448
  if (isKeyboardOpenRef.current) {
418
449
  // Only prevent closing if keyboard is actually open (mobile scenario)
@@ -427,15 +458,12 @@ const DropdownFilterTabs = forwardRef<
427
458
  }
428
459
  }
429
460
 
430
- // Check if scroll is happening within the dropdown itself - if so, don't close
431
- const scrollTarget = event.target;
432
- if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
433
- return;
434
- }
435
-
436
- // On desktop (keyboard not open), close on scroll
437
- if (dropRef.current && dropOpen) {
438
- setTimeout(() => {
461
+ // Close on any outside scroll (including scrolls on nested containers).
462
+ if (dropOpen) {
463
+ if (closeOnScrollTimeoutRef.current) {
464
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
465
+ }
466
+ closeOnScrollTimeoutRef.current = window.setTimeout(() => {
439
467
  setDropOpen(false);
440
468
  onToolClose?.();
441
469
  if (!typeOnlyFetch) setSearchValue('');
@@ -485,6 +513,8 @@ const DropdownFilterTabs = forwardRef<
485
513
 
486
514
  document.addEventListener('mousedown', handleClickOutside as any);
487
515
  window.addEventListener('scroll', handleScroll as any);
516
+ // Capture-phase scroll catches scrolls from any scroll container (scroll doesn't bubble).
517
+ document.addEventListener('scroll', handleScroll as any, true);
488
518
 
489
519
  const scrollableDivs = document.querySelectorAll(
490
520
  'div[style*="overflow"], .overflow-auto, .overflow-y-auto, .overflow-x-auto'
@@ -498,9 +528,14 @@ const DropdownFilterTabs = forwardRef<
498
528
  return () => {
499
529
  document.removeEventListener('mousedown', handleClickOutside as any);
500
530
  window.removeEventListener('scroll', handleScroll as any);
531
+ document.removeEventListener('scroll', handleScroll as any, true);
501
532
  scrollableDivs.forEach((div) =>
502
533
  div.removeEventListener('scroll', handleScroll as any)
503
534
  );
535
+ if (closeOnScrollTimeoutRef.current) {
536
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
537
+ closeOnScrollTimeoutRef.current = null;
538
+ }
504
539
  if (scrollRef && scrollRef.current && scrollRef.current !== null) {
505
540
  scrollRef.current.removeEventListener(
506
541
  'scroll',
@@ -758,7 +793,7 @@ const DropdownFilterTabs = forwardRef<
758
793
  const allTabs = moreOptionTab ? [...tab, { id: 'more-options', label: moreOptionTabLabel }] : tab;
759
794
  return (
760
795
  <ul
761
- className="qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2"
796
+ className={`qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2 ${tabsScrollable ? 'qbs-tab-scroll' : ''}`}
762
797
  ref={tabRef}
763
798
  >
764
799
  {allTabs.map((item: TabPops, idx: number) => {
@@ -794,7 +829,7 @@ const DropdownFilterTabs = forwardRef<
794
829
  })}
795
830
  </ul>
796
831
  );
797
- }, [tab, hasSelectedItemsInTab, activeTab, handleTabClick, moreOptionTab, moreOptionTabLabel, moreOptionTabIcon]);
832
+ }, [tab, hasSelectedItemsInTab, activeTab, handleTabClick, moreOptionTab, moreOptionTabLabel, moreOptionTabIcon, tabsScrollable]);
798
833
 
799
834
  const handleSelectAll = useCallback(() => {
800
835
  if (isMultiple) {
@@ -866,6 +901,8 @@ const DropdownFilterTabs = forwardRef<
866
901
 
867
902
  const handleApplySelected = (values?: any) => {
868
903
  setDropOpen(false);
904
+ setSearchValue('');
905
+ resetSuggections?.();
869
906
  applyTabFilter?.(values);
870
907
  };
871
908
 
@@ -37,6 +37,15 @@
37
37
  .qbs-tab-item {
38
38
  color: #4b5563;
39
39
  border-color: #d1d5db;
40
+ /* Prevent mid-word breaking from any inherited/global rules */
41
+ word-break: normal;
42
+ overflow-wrap: normal;
43
+ hyphens: manual;
44
+ /* Prevent breaking at spaces too (single-line label) */
45
+ white-space: nowrap;
46
+ /* In non-scroll layout, keep text tidy if it's forced into a narrow cell */
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
40
49
  }
41
50
  .qbs-tab-item:hover {
42
51
  color: #2563eb;
@@ -45,6 +54,36 @@
45
54
  color: #2563eb;
46
55
  border-color: #2563eb;
47
56
  }
57
+
58
+ /* When tabs overflow, keep them in a single row and allow horizontal scroll */
59
+ .qbs-tab.qbs-tab-scroll {
60
+ flex-wrap: nowrap !important; /* override .qbs-flex-wrap */
61
+ overflow-x: auto;
62
+ overflow-y: hidden;
63
+ -webkit-overflow-scrolling: touch;
64
+ }
65
+
66
+ .qbs-tab.qbs-tab-scroll .qbs-tab-items {
67
+ flex: 0 0 auto !important; /* override .qbs-flex-1 */
68
+ }
69
+
70
+ .qbs-tab.qbs-tab-scroll .qbs-tab-item {
71
+ width: max-content !important; /* override .qbs-w-full */
72
+ white-space: nowrap;
73
+ word-break: normal;
74
+ overflow-wrap: normal;
75
+ hyphens: manual;
76
+ overflow: visible;
77
+ text-overflow: clip;
78
+ }
79
+
80
+ /* If the tab label has nested spans (icons + text), ensure they inherit the non-breaking behavior */
81
+ .qbs-tab-item * {
82
+ word-break: inherit;
83
+ overflow-wrap: inherit;
84
+ hyphens: inherit;
85
+ white-space: inherit;
86
+ }
48
87
  .qbs-relative {
49
88
  position: relative;
50
89
  }