react-core-ts 2.0.21 → 2.0.23

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.23",
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,26 @@ 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 maxWidthCap = tab.length > 0
145
+ ? Math.max(MAX_DROPDOWN_WIDTH_WITH_TABS_PX, dropdownPosition.width)
146
+ : Number.POSITIVE_INFINITY;
147
+ const widthBeforeClamp = Math.min(
148
+ Math.max(dropdownPosition.width, tabsNaturalWidth ?? 0, MIN_DROPDOWN_WIDTH_PX),
149
+ maxWidthCap
150
+ );
151
+ const { width: desiredWidth, left: desiredLeft } =
152
+ clampDropdownToViewportPreferRightAlign({
153
+ desiredWidth: widthBeforeClamp,
154
+ desiredLeft: dropdownPosition.left,
155
+ anchorRight: inputRect.right + window.scrollX,
156
+ });
157
+ setTabsScrollable((prev) => {
158
+ const next = tab.length > 0 && (tabsNaturalWidth ?? 0) > desiredWidth;
159
+ return prev === next ? prev : next;
160
+ });
134
161
  // Check if there's enough space below the input for the dropdown
135
162
  const spaceBelow = window.innerHeight - inputRect.bottom;
136
163
  const spaceAbove = inputRect.top + window.scrollY;
@@ -184,8 +211,8 @@ const AutoCompleteWithSelectedList = forwardRef<
184
211
  setDropdownStyle((prevStyle) => ({
185
212
  ...prevStyle,
186
213
  transform: `translateY(${dropdownPosition.top}px)`,
187
- left: inputRect.left + window.scrollX,
188
- width: inputRect.width,
214
+ left: desiredLeft,
215
+ width: desiredWidth,
189
216
  }));
190
217
  }
191
218
  };
@@ -383,6 +410,7 @@ const AutoCompleteWithSelectedList = forwardRef<
383
410
 
384
411
  const uniqueDropArrowId = `${name}-drop-arrow-selected-list-icon`;
385
412
  const isKeyboardOpenRef = useRef(false);
413
+ const closeOnScrollTimeoutRef = useRef<number | null>(null);
386
414
 
387
415
  // Keep ref in sync with state for use in event handlers
388
416
  useEffect(() => {
@@ -391,7 +419,13 @@ const AutoCompleteWithSelectedList = forwardRef<
391
419
 
392
420
  useEffect(() => {
393
421
  const handleScroll = (event: any) => {
394
- // On mobile: Don't close dropdown on scroll when keyboard is open
422
+ // Don't close if the user is scrolling inside the dropdown itself.
423
+ const scrollTarget = event.target;
424
+ if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
425
+ return;
426
+ }
427
+
428
+ // On mobile: Don't close dropdown on scroll when keyboard is open (typing scenario)
395
429
  // On desktop: Always allow closing on scroll
396
430
  if (isKeyboardOpenRef.current) {
397
431
  // Only prevent closing if keyboard is actually open (mobile scenario)
@@ -406,15 +440,12 @@ const AutoCompleteWithSelectedList = forwardRef<
406
440
  }
407
441
  }
408
442
 
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(() => {
443
+ // Close on any outside scroll (including scrolls on nested containers).
444
+ if (dropOpen) {
445
+ if (closeOnScrollTimeoutRef.current) {
446
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
447
+ }
448
+ closeOnScrollTimeoutRef.current = window.setTimeout(() => {
418
449
  setDropOpen(false);
419
450
  if (!typeOnlyFetch) setSearchValue('');
420
451
  }, 200);
@@ -460,6 +491,8 @@ const AutoCompleteWithSelectedList = forwardRef<
460
491
 
461
492
  document.addEventListener('mousedown', handleClickOutside as any);
462
493
  window.addEventListener('scroll', handleScroll as any);
494
+ // Capture-phase scroll catches scrolls from any scroll container (scroll doesn't bubble).
495
+ document.addEventListener('scroll', handleScroll as any, true);
463
496
 
464
497
  const scrollableDivs = document.querySelectorAll(
465
498
  'div[style*="overflow"], .overflow-auto, .overflow-y-auto, .overflow-x-auto'
@@ -473,9 +506,14 @@ const AutoCompleteWithSelectedList = forwardRef<
473
506
  return () => {
474
507
  document.removeEventListener('mousedown', handleClickOutside as any);
475
508
  window.removeEventListener('scroll', handleScroll as any);
509
+ document.removeEventListener('scroll', handleScroll as any, true);
476
510
  scrollableDivs.forEach((div) =>
477
511
  div.removeEventListener('scroll', handleScroll as any)
478
512
  );
513
+ if (closeOnScrollTimeoutRef.current) {
514
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
515
+ closeOnScrollTimeoutRef.current = null;
516
+ }
479
517
  if (scrollRef && scrollRef.current && scrollRef.current !== null) {
480
518
  scrollRef.current.removeEventListener(
481
519
  'scroll',
@@ -774,7 +812,7 @@ const AutoCompleteWithSelectedList = forwardRef<
774
812
  const getTabItems = () => {
775
813
  return (
776
814
  <ul
777
- className="qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2"
815
+ className={`qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2 ${tabsScrollable ? 'qbs-tab-scroll' : ''}`}
778
816
  ref={tabRef}
779
817
  >
780
818
  {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,25 @@ 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 maxWidthCap = tab.length > 0
482
+ ? Math.max(MAX_DROPDOWN_WIDTH_WITH_TABS_PX, dropdownPosition.width)
483
+ : Number.POSITIVE_INFINITY;
484
+ const widthBeforeClamp = Math.min(
485
+ Math.max(dropdownPosition.width, tabsNaturalWidth ?? 0, MIN_DROPDOWN_WIDTH_PX),
486
+ maxWidthCap
487
+ );
488
+ const { width: desiredWidth, left: desiredLeft } = clampDropdownToViewportPreferRightAlign({
489
+ desiredWidth: widthBeforeClamp,
490
+ desiredLeft: dropdownPosition.left,
491
+ anchorRight: inputRect.right + window.scrollX,
492
+ });
493
+ setTabsScrollable((prev) => {
494
+ const next = tab.length > 0 && (tabsNaturalWidth ?? 0) > desiredWidth;
495
+ return prev === next ? prev : next;
496
+ });
460
497
  // Check if there's enough space below the input for the dropdown
461
498
  const spaceBelow = window.innerHeight - inputRect.bottom;
462
499
  const spaceAbove = inputRect.top + window.scrollY;
@@ -512,8 +549,8 @@ const AutoCompleteWithTabFilter = forwardRef<
512
549
  const newStyle = {
513
550
  ...prevStyle,
514
551
  transform: `translateY(${dropdownPosition.top}px)`,
515
- left: inputRect.left + window.scrollX,
516
- width: inputRect.width,
552
+ left: desiredLeft,
553
+ width: desiredWidth,
517
554
  };
518
555
  // Only update if the style actually changed
519
556
  if (prevStyle.transform !== newStyle.transform ||
@@ -833,7 +870,7 @@ const AutoCompleteWithTabFilter = forwardRef<
833
870
  const getTabItems = useCallback(() => {
834
871
  return (
835
872
  <ul
836
- className="qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2"
873
+ className={`qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2 ${tabsScrollable ? 'qbs-tab-scroll' : ''}`}
837
874
  ref={tabRef}
838
875
  >
839
876
  {tab.map((item: TabPops, idx: number) => (
@@ -851,7 +888,7 @@ const AutoCompleteWithTabFilter = forwardRef<
851
888
  ))}
852
889
  </ul>
853
890
  );
854
- }, [tab, activeTab, handleTabClick]);
891
+ }, [tab, activeTab, handleTabClick, tabsScrollable]);
855
892
 
856
893
  const handleSelectAll = useCallback(() => {
857
894
  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,26 @@ 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 maxWidthCap = hasTabs
186
+ ? Math.max(MAX_DROPDOWN_WIDTH_WITH_TABS_PX, dropdownPosition.width)
187
+ : Number.POSITIVE_INFINITY;
188
+ const widthBeforeClamp = Math.min(
189
+ Math.max(dropdownPosition.width, tabsNaturalWidth ?? 0, MIN_DROPDOWN_WIDTH_PX),
190
+ maxWidthCap
191
+ );
192
+ const { width: desiredWidth, left: desiredLeft } = clampDropdownToViewportPreferRightAlign({
193
+ desiredWidth: widthBeforeClamp,
194
+ desiredLeft: dropdownPosition.left,
195
+ anchorRight: inputRect.right + window.scrollX,
196
+ });
197
+ setTabsScrollable((prev) => {
198
+ const next = hasTabs && (tabsNaturalWidth ?? 0) > desiredWidth;
199
+ return prev === next ? prev : next;
200
+ });
174
201
  // Check if there's enough space below the input for the dropdown
175
202
  const spaceBelow = window.innerHeight - inputRect.bottom;
176
203
  const spaceAbove = inputRect.top + window.scrollY;
@@ -224,8 +251,8 @@ const DropdownFilterTabs = forwardRef<
224
251
  setDropdownStyle((prevStyle) => ({
225
252
  ...prevStyle,
226
253
  transform: `translateY(${dropdownPosition.top}px)`,
227
- left: inputRect.left + window.scrollX,
228
- width: inputRect.width,
254
+ left: desiredLeft,
255
+ width: desiredWidth,
229
256
  }));
230
257
 
231
258
  }
@@ -386,6 +413,7 @@ const DropdownFilterTabs = forwardRef<
386
413
 
387
414
  const uniqueDropArrowId = `${name}-drop-arrow-selected-list-icon`;
388
415
  const isKeyboardOpenRef = useRef(false);
416
+ const closeOnScrollTimeoutRef = useRef<number | null>(null);
389
417
 
390
418
  // Keep ref in sync with state for use in event handlers
391
419
  useEffect(() => {
@@ -412,7 +440,13 @@ const DropdownFilterTabs = forwardRef<
412
440
 
413
441
  useEffect(() => {
414
442
  const handleScroll = (event: any) => {
415
- // On mobile: Don't close dropdown on scroll when keyboard is open
443
+ // Don't close if the user is scrolling inside the dropdown itself.
444
+ const scrollTarget = event.target;
445
+ if (dropRef.current && scrollTarget instanceof Node && dropRef.current.contains(scrollTarget)) {
446
+ return;
447
+ }
448
+
449
+ // On mobile: Don't close dropdown on scroll when keyboard is open (typing scenario)
416
450
  // On desktop: Always allow closing on scroll
417
451
  if (isKeyboardOpenRef.current) {
418
452
  // Only prevent closing if keyboard is actually open (mobile scenario)
@@ -427,15 +461,12 @@ const DropdownFilterTabs = forwardRef<
427
461
  }
428
462
  }
429
463
 
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(() => {
464
+ // Close on any outside scroll (including scrolls on nested containers).
465
+ if (dropOpen) {
466
+ if (closeOnScrollTimeoutRef.current) {
467
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
468
+ }
469
+ closeOnScrollTimeoutRef.current = window.setTimeout(() => {
439
470
  setDropOpen(false);
440
471
  onToolClose?.();
441
472
  if (!typeOnlyFetch) setSearchValue('');
@@ -485,6 +516,8 @@ const DropdownFilterTabs = forwardRef<
485
516
 
486
517
  document.addEventListener('mousedown', handleClickOutside as any);
487
518
  window.addEventListener('scroll', handleScroll as any);
519
+ // Capture-phase scroll catches scrolls from any scroll container (scroll doesn't bubble).
520
+ document.addEventListener('scroll', handleScroll as any, true);
488
521
 
489
522
  const scrollableDivs = document.querySelectorAll(
490
523
  'div[style*="overflow"], .overflow-auto, .overflow-y-auto, .overflow-x-auto'
@@ -498,9 +531,14 @@ const DropdownFilterTabs = forwardRef<
498
531
  return () => {
499
532
  document.removeEventListener('mousedown', handleClickOutside as any);
500
533
  window.removeEventListener('scroll', handleScroll as any);
534
+ document.removeEventListener('scroll', handleScroll as any, true);
501
535
  scrollableDivs.forEach((div) =>
502
536
  div.removeEventListener('scroll', handleScroll as any)
503
537
  );
538
+ if (closeOnScrollTimeoutRef.current) {
539
+ window.clearTimeout(closeOnScrollTimeoutRef.current);
540
+ closeOnScrollTimeoutRef.current = null;
541
+ }
504
542
  if (scrollRef && scrollRef.current && scrollRef.current !== null) {
505
543
  scrollRef.current.removeEventListener(
506
544
  'scroll',
@@ -758,7 +796,7 @@ const DropdownFilterTabs = forwardRef<
758
796
  const allTabs = moreOptionTab ? [...tab, { id: 'more-options', label: moreOptionTabLabel }] : tab;
759
797
  return (
760
798
  <ul
761
- className="qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2"
799
+ className={`qbs-flex qbs-flex-wrap qbs-w-full qbs-tab qbs-mb-2 -qbs-mt-2 ${tabsScrollable ? 'qbs-tab-scroll' : ''}`}
762
800
  ref={tabRef}
763
801
  >
764
802
  {allTabs.map((item: TabPops, idx: number) => {
@@ -794,7 +832,7 @@ const DropdownFilterTabs = forwardRef<
794
832
  })}
795
833
  </ul>
796
834
  );
797
- }, [tab, hasSelectedItemsInTab, activeTab, handleTabClick, moreOptionTab, moreOptionTabLabel, moreOptionTabIcon]);
835
+ }, [tab, hasSelectedItemsInTab, activeTab, handleTabClick, moreOptionTab, moreOptionTabLabel, moreOptionTabIcon, tabsScrollable]);
798
836
 
799
837
  const handleSelectAll = useCallback(() => {
800
838
  if (isMultiple) {
@@ -866,6 +904,8 @@ const DropdownFilterTabs = forwardRef<
866
904
 
867
905
  const handleApplySelected = (values?: any) => {
868
906
  setDropOpen(false);
907
+ setSearchValue('');
908
+ resetSuggections?.();
869
909
  applyTabFilter?.(values);
870
910
  };
871
911
 
@@ -37,6 +37,18 @@
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;
49
+ /* Minimum left/right padding inside each tab */
50
+ padding-left: 8px;
51
+ padding-right: 8px;
40
52
  }
41
53
  .qbs-tab-item:hover {
42
54
  color: #2563eb;
@@ -45,6 +57,36 @@
45
57
  color: #2563eb;
46
58
  border-color: #2563eb;
47
59
  }
60
+
61
+ /* When tabs overflow, keep them in a single row and allow horizontal scroll */
62
+ .qbs-tab.qbs-tab-scroll {
63
+ flex-wrap: nowrap !important; /* override .qbs-flex-wrap */
64
+ overflow-x: auto;
65
+ overflow-y: hidden;
66
+ -webkit-overflow-scrolling: touch;
67
+ }
68
+
69
+ .qbs-tab.qbs-tab-scroll .qbs-tab-items {
70
+ flex: 0 0 auto !important; /* override .qbs-flex-1 */
71
+ }
72
+
73
+ .qbs-tab.qbs-tab-scroll .qbs-tab-item {
74
+ width: max-content !important; /* override .qbs-w-full */
75
+ white-space: nowrap;
76
+ word-break: normal;
77
+ overflow-wrap: normal;
78
+ hyphens: manual;
79
+ overflow: visible;
80
+ text-overflow: clip;
81
+ }
82
+
83
+ /* If the tab label has nested spans (icons + text), ensure they inherit the non-breaking behavior */
84
+ .qbs-tab-item * {
85
+ word-break: inherit;
86
+ overflow-wrap: inherit;
87
+ hyphens: inherit;
88
+ white-space: inherit;
89
+ }
48
90
  .qbs-relative {
49
91
  position: relative;
50
92
  }