qbs-react-grid 2.2.1 → 2.2.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.
@@ -387,71 +387,98 @@
387
387
  .qbs-table-tooltip {
388
388
  cursor: pointer;
389
389
  position: relative;
390
- width: auto;
391
- display: flex;
390
+ display: inline-flex;
392
391
  }
393
392
 
394
393
  .qbs-table-tooltip .tooltiptext {
395
394
  visibility: hidden;
396
- background-color: black;
397
- color: white;
395
+ background-color: var(--tooltip-bg, #222);
396
+ color: var(--tooltip-text, #fff);
398
397
  text-align: center;
399
- padding: 6px;
400
- border-radius: 4px;
398
+ padding: 6px 10px;
399
+ border-radius: 6px;
401
400
  position: absolute;
402
- z-index: 9999;
401
+ z-index: 10050;
403
402
  opacity: 0;
404
- transition: opacity 0.3s;
403
+ transition: opacity 0.15s ease;
405
404
  font-size: 12px;
406
- font-style: normal;
407
- font-weight: 400;
405
+ font-weight: 500;
408
406
  line-height: 16px;
409
- min-width: 100px;
410
- max-width: 200px;
407
+ white-space: nowrap;
408
+ max-width: min(280px, 90vw);
409
+ pointer-events: none;
410
+ inset-inline-start: 50%;
411
+ translate: -50% 0;
411
412
  }
412
413
 
413
414
  .qbs-table-tooltip.up .tooltiptext {
414
- bottom: calc(100% + 10px);
415
- right: -8px;
416
- left: unset;
415
+ bottom: calc(100% + 8px);
416
+ top: auto;
417
417
  }
418
418
 
419
419
  .qbs-table-tooltip.down .tooltiptext {
420
- right: -10px;
421
420
  top: calc(100% + 8px);
421
+ bottom: auto;
422
422
  }
423
423
 
424
- // .qbs-table-tooltip:hover .tooltiptext {
425
- // visibility: visible;
426
- // opacity: 1;
427
- // }
428
-
429
424
  .qbs-table-tooltip .tooltiptext::after {
430
425
  content: '';
431
426
  position: absolute;
432
- border-width: 5px;
433
- border-style: solid;
427
+ inset-inline-start: 50%;
428
+ translate: -50% 0;
429
+ border: 5px solid transparent;
434
430
  }
435
431
 
436
432
  .qbs-table-tooltip.up .tooltiptext::after {
437
- border-color: black transparent transparent !important;
438
- right: 12px !important;
439
- margin-left: -5px !important;
440
- top: 100% !important;
441
- left: unset !important;
433
+ top: 100%;
434
+ border-top-color: var(--tooltip-bg, #222);
442
435
  }
443
436
 
444
437
  .qbs-table-tooltip.down .tooltiptext::after {
445
- border-color: transparent transparent black;
446
- bottom: 100% !important;
447
- right: 12px !important;
448
- margin-left: -5px !important;
449
- left: unset !important;
438
+ bottom: 100%;
439
+ border-bottom-color: var(--tooltip-bg, #222);
450
440
  }
451
- .qbs-table-tooltip.down .tooltiptext {
452
- top: 145% !important;
453
- right: -10px !important;
454
- left: auto !important;
441
+
442
+ .qbs-table-tooltip-floating.tooltiptext {
443
+ position: fixed;
444
+ z-index: 10050;
445
+ visibility: hidden;
446
+ opacity: 0;
447
+ pointer-events: none;
448
+ background-color: var(--tooltip-bg, #222);
449
+ color: var(--tooltip-text, #fff);
450
+ text-align: center;
451
+ padding: 6px 10px;
452
+ border-radius: 6px;
453
+ font-size: 12px;
454
+ font-weight: 500;
455
+ line-height: 16px;
456
+ white-space: nowrap;
457
+ max-width: min(280px, 90vw);
458
+ transition: opacity 0.15s ease;
459
+ }
460
+
461
+ .qbs-table-tooltip-floating.tooltiptext.is-positioned {
462
+ visibility: visible;
463
+ opacity: 1;
464
+ }
465
+
466
+ .qbs-table-tooltip-floating.tooltiptext::after {
467
+ content: '';
468
+ position: absolute;
469
+ left: var(--tooltip-arrow-offset, 50%);
470
+ translate: -50% 0;
471
+ border: 5px solid transparent;
472
+ }
473
+
474
+ .qbs-table-tooltip-floating--down.tooltiptext::after {
475
+ bottom: 100%;
476
+ border-bottom-color: var(--tooltip-bg, #222);
477
+ }
478
+
479
+ .qbs-table-tooltip-floating--up.tooltiptext::after {
480
+ top: 100%;
481
+ border-top-color: var(--tooltip-bg, #222);
455
482
  }
456
483
  .rs-table-row {
457
484
  overflow: visible !important;
@@ -81,7 +81,7 @@ const CardMenuDropdown: React.FC<Props> = ({
81
81
  return (
82
82
  <div className="dropdown text-black dark:text-white dark:bg-[#424242] bg-white" ref={menuRef}>
83
83
  <button className="dropdown-toggle" onClick={toggleMenu} ref={menuButtonRef}>
84
- <TooltipComponent title={labels.actions} enabled={false} ref={menuButtonRef}>
84
+ <TooltipComponent title={labels.actions} enabled={false}>
85
85
  <ThreeDotIcon />
86
86
  </TooltipComponent>
87
87
  </button>
@@ -1,88 +1,149 @@
1
- // import React, { useRef, useState } from 'react';
2
-
3
- // const TooltipComponent: React.FC<any> = ({ title, children, tableBodyRef }) => {
4
- // const [dropdownPosition, setDropdownPosition] = useState('bottom-position');
5
- // const dropRef = useRef(null);
6
- // const menuButtonRef = useRef<HTMLElement>(null);
7
- // const adjustDropdownPosition = () => {
8
- // if (menuButtonRef.current && tableBodyRef?.current) {
9
- // const inputBoxRect = menuButtonRef.current?.getBoundingClientRect();
10
- // const tableRect = tableBodyRef.current.getBoundingClientRect();
11
- // // Calculate positions relative to the table
12
- // const spaceAbove = inputBoxRect.top - tableRect.top;
13
- // const spaceBelow = tableRect.bottom - inputBoxRect.bottom;
14
-
15
- // if (spaceAbove > spaceBelow) {
16
- // setDropdownPosition('top-position');
17
- // } else {
18
- // setDropdownPosition('bottom-position');
19
- // }
20
- // }
21
- // };
22
-
23
- // return (
24
- // <div className={`qbs-table-tooltip ${dropdownPosition == 'bottom-position' ? 'down' : 'up'} `}>
25
- // <span
26
- // ref={menuButtonRef}
27
- // style={{ display: 'flex' }}
28
- // onMouseEnter={() => adjustDropdownPosition()}
29
- // >
30
- // {children}
31
- // </span>
32
- // <span ref={dropRef} className={'tooltiptext'}>
33
- // {title}
34
- // </span>
35
- // </div>
36
- // );
37
- // };
38
-
39
- // export default TooltipComponent;
40
- import React, { useRef, useState } from 'react';
41
-
42
- const TooltipComponent: React.FC<any> = ({ title, children, tableBodyRef }) => {
43
- const [dropdownPosition, setDropdownPosition] = useState<'up' | 'down'>('down');
44
- const menuButtonRef = useRef<HTMLElement>(null);
45
-
46
- const adjustDropdownPosition = () => {
47
- if (menuButtonRef.current && tableBodyRef?.current) {
48
- const triggerRect = menuButtonRef.current.getBoundingClientRect();
49
- const tableRect = tableBodyRef.current.getBoundingClientRect();
50
-
51
- const spaceAbove = triggerRect.top - tableRect.top;
52
- const spaceBelow = tableRect.bottom - triggerRect.bottom;
53
-
54
- setDropdownPosition(spaceAbove > spaceBelow ? 'up' : 'down');
1
+ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+
4
+ type TooltipComponentProps = {
5
+ title?: React.ReactNode;
6
+ children: React.ReactNode;
7
+ tableBodyRef?: React.RefObject<HTMLDivElement | null>;
8
+ /** When false, renders children only (no tooltip). */
9
+ enabled?: boolean;
10
+ };
11
+
12
+ const VIEWPORT_PADDING = 8;
13
+ const TOOLTIP_GAP = 8;
14
+
15
+ const TooltipComponent: React.FC<TooltipComponentProps> = ({
16
+ title,
17
+ children,
18
+ tableBodyRef,
19
+ enabled = true,
20
+ }) => {
21
+ const [visible, setVisible] = useState(false);
22
+ const [positioned, setPositioned] = useState(false);
23
+ const [placement, setPlacement] = useState<'up' | 'down'>('down');
24
+ const [coords, setCoords] = useState({ top: 0, left: 0 });
25
+ const [arrowOffset, setArrowOffset] = useState(0);
26
+ const triggerRef = useRef<HTMLSpanElement>(null);
27
+ const tooltipRef = useRef<HTMLSpanElement>(null);
28
+
29
+ const updatePosition = useCallback(() => {
30
+ const trigger = triggerRef.current;
31
+ const tooltip = tooltipRef.current;
32
+ if (!trigger || !tooltip) {
33
+ return;
34
+ }
35
+
36
+ const triggerRect = trigger.getBoundingClientRect();
37
+ const tooltipRect = tooltip.getBoundingClientRect();
38
+ const boundaryRect =
39
+ tableBodyRef?.current?.getBoundingClientRect() ??
40
+ trigger.closest('.qbs-table')?.getBoundingClientRect();
41
+
42
+ const spaceAbove = boundaryRect
43
+ ? triggerRect.top - boundaryRect.top
44
+ : triggerRect.top;
45
+ const spaceBelow = boundaryRect
46
+ ? boundaryRect.bottom - triggerRect.bottom
47
+ : window.innerHeight - triggerRect.bottom;
48
+
49
+ const nextPlacement =
50
+ spaceBelow >= tooltipRect.height + TOOLTIP_GAP || spaceBelow >= spaceAbove ? 'down' : 'up';
51
+
52
+ const triggerCenter = triggerRect.left + triggerRect.width / 2;
53
+ let left = triggerCenter - tooltipRect.width / 2;
54
+
55
+ if (left < VIEWPORT_PADDING) {
56
+ left = VIEWPORT_PADDING;
57
+ } else if (left + tooltipRect.width > window.innerWidth - VIEWPORT_PADDING) {
58
+ left = window.innerWidth - VIEWPORT_PADDING - tooltipRect.width;
55
59
  }
60
+
61
+ const top =
62
+ nextPlacement === 'down'
63
+ ? triggerRect.bottom + TOOLTIP_GAP
64
+ : triggerRect.top - tooltipRect.height - TOOLTIP_GAP;
65
+
66
+ setPlacement(nextPlacement);
67
+ setCoords({ top, left });
68
+ setArrowOffset(triggerCenter - left);
69
+ setPositioned(true);
70
+ }, [tableBodyRef]);
71
+
72
+ const showTooltip = () => {
73
+ setPositioned(false);
74
+ setVisible(true);
75
+ };
76
+
77
+ const hideTooltip = () => {
78
+ setVisible(false);
79
+ setPositioned(false);
56
80
  };
57
81
 
82
+ useLayoutEffect(() => {
83
+ if (!visible) {
84
+ return;
85
+ }
86
+
87
+ updatePosition();
88
+ const frame = window.requestAnimationFrame(updatePosition);
89
+ return () => window.cancelAnimationFrame(frame);
90
+ }, [visible, title, updatePosition]);
91
+
92
+ useEffect(() => {
93
+ if (!visible) {
94
+ return;
95
+ }
96
+
97
+ const handleReposition = () => updatePosition();
98
+ window.addEventListener('resize', handleReposition);
99
+ window.addEventListener('scroll', handleReposition, true);
100
+ return () => {
101
+ window.removeEventListener('resize', handleReposition);
102
+ window.removeEventListener('scroll', handleReposition, true);
103
+ };
104
+ }, [visible, updatePosition]);
105
+
106
+ if (!title || enabled === false) {
107
+ return <>{children}</>;
108
+ }
109
+
110
+ const portalRoot = typeof document !== 'undefined' ? document.body : null;
111
+
58
112
  return (
59
- <div
60
- className={`qbs-table-tooltip ${dropdownPosition}`}
61
- onMouseEnter={adjustDropdownPosition}
62
- onMouseLeave={() => {
63
- const tooltip = menuButtonRef?.current?.querySelector('.tooltiptext') as HTMLElement;
64
- if (tooltip) {
65
- tooltip.style.visibility = 'hidden';
66
- tooltip.style.opacity = '0';
67
- }
68
- }}
69
- >
113
+ <>
70
114
  <span
71
- ref={menuButtonRef}
72
- style={{ display: 'flex' }}
73
- onMouseEnter={() => {
74
- adjustDropdownPosition();
75
- const tooltip = menuButtonRef?.current?.querySelector('.tooltiptext') as HTMLElement;
76
- if (tooltip) {
77
- tooltip.style.visibility = 'visible';
78
- tooltip.style.opacity = '1';
79
- }
80
- }}
115
+ ref={triggerRef}
116
+ className="qbs-table-tooltip-trigger"
117
+ style={{ display: 'inline-flex' }}
118
+ onMouseEnter={showTooltip}
119
+ onMouseLeave={hideTooltip}
120
+ onFocus={showTooltip}
121
+ onBlur={hideTooltip}
81
122
  >
82
123
  {children}
83
- <span className="tooltiptext">{title}</span>
84
124
  </span>
85
- </div>
125
+ {visible &&
126
+ portalRoot &&
127
+ ReactDOM.createPortal(
128
+ <span
129
+ ref={tooltipRef}
130
+ role="tooltip"
131
+ className={`qbs-table-tooltip-floating tooltiptext qbs-table-tooltip-floating--${placement} ${
132
+ positioned ? 'is-positioned' : ''
133
+ }`}
134
+ style={
135
+ {
136
+ top: coords.top,
137
+ left: coords.left,
138
+ '--tooltip-arrow-offset': `${arrowOffset}px`,
139
+ } as React.CSSProperties
140
+ }
141
+ >
142
+ {title}
143
+ </span>,
144
+ portalRoot,
145
+ )}
146
+ </>
86
147
  );
87
148
  };
88
149
 
@@ -10,7 +10,7 @@ type Props = {
10
10
  handleMenuActions?: (slug: ActionProps, rowData?: any) => void;
11
11
  rowData?: any;
12
12
  dataTheme?: string;
13
- tableBodyRef: React.RefObject<HTMLDivElement>;
13
+ tableBodyRef: React.RefObject<HTMLDivElement | null>;
14
14
  rowIndex?: number;
15
15
  wheelWrapperRef?: React.RefObject<HTMLDivElement>;
16
16
  };
@@ -87,42 +87,33 @@ const VerticalMenuDropdown: React.FC<Props> = ({
87
87
  const toggleMenu = () => {
88
88
  if (!openMenu && menuButtonRef.current) {
89
89
  const rect = menuButtonRef.current.getBoundingClientRect();
90
- const windowHeight = window.innerHeight;
91
- const menuHeight =
92
- actionDropDown?.filter(item => !item.hidden && !item?.hide?.(rowData, rowIndex)).length *
93
- 40; // 40px per menu item
94
-
95
- // Get table boundaries for RTL positioning
96
- const tableRect = tableBodyRef.current?.getBoundingClientRect();
90
+ const viewportPadding = 8;
91
+ const menuGap = 4;
97
92
  const dropdownWidth = 200;
93
+ const visibleItems =
94
+ actionDropDown?.filter(item => !item.hidden && !item?.hide?.(rowData, rowIndex)) ?? [];
95
+ const menuHeight = visibleItems.length * 40;
98
96
 
99
- // Check if there's enough space below
100
- const spaceBelow = windowHeight - rect.bottom;
97
+ const spaceBelow = window.innerHeight - rect.bottom;
98
+ const openBelow = spaceBelow >= menuHeight + menuGap;
101
99
 
102
- let leftPosition = rect.left - dropdownWidth;
100
+ // Anchor to trigger; prefer opening toward inline-start (left in LTR).
101
+ let left = rect.right - dropdownWidth;
103
102
 
104
- // For RTL, adjust positioning to stay within table bounds
105
- if (document.documentElement.dir === 'rtl' && tableRect) {
106
- // Calculate the right edge position for RTL
107
- const rightEdge = rect.right;
108
- leftPosition = Math.min(rightEdge, tableRect.right - dropdownWidth);
109
- // Ensure it doesn't go beyond the left edge of the table
110
- leftPosition = Math.max(leftPosition, tableRect.left);
103
+ if (left < viewportPadding) {
104
+ left = rect.left;
111
105
  }
112
-
113
- if (spaceBelow >= menuHeight) {
114
- // Open below
115
- setPosition({
116
- top: rect.bottom + window.scrollY - rect.height,
117
- left: leftPosition
118
- });
119
- } else {
120
- // Open above
121
- setPosition({
122
- top: rect.top + window.scrollY - menuHeight,
123
- left: leftPosition
124
- });
106
+ if (left + dropdownWidth > window.innerWidth - viewportPadding) {
107
+ left = Math.max(viewportPadding, rect.left - dropdownWidth);
125
108
  }
109
+ if (left + dropdownWidth > window.innerWidth - viewportPadding) {
110
+ left = window.innerWidth - viewportPadding - dropdownWidth;
111
+ }
112
+
113
+ setPosition({
114
+ top: openBelow ? rect.bottom + menuGap : rect.top - menuHeight - menuGap,
115
+ left,
116
+ });
126
117
  }
127
118
  setTimeout(() => {
128
119
  setOpenMenu(prev => !prev);
@@ -132,13 +123,13 @@ const VerticalMenuDropdown: React.FC<Props> = ({
132
123
  const portalTarget = document.getElementById('portal-root');
133
124
  const dropdownContent = (
134
125
  <div
135
- className="absolute z-50 min-w-48 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 vertical-menu-dropdown-content"
126
+ className="absolute z-[60] min-w-48 rounded-md vertical-menu-dropdown-content"
136
127
  ref={menuRef}
137
128
  style={{
138
129
  width: 200,
139
130
  top: position.top,
140
131
  left: position.left,
141
- position: 'absolute'
132
+ position: 'fixed',
142
133
  }}
143
134
  >
144
135
  <div className="py-1">
@@ -146,7 +137,7 @@ const VerticalMenuDropdown: React.FC<Props> = ({
146
137
  !item?.hidden && !item?.hide?.(rowData, rowIndex) ? (
147
138
  <div
148
139
  key={item.title}
149
- className="vertical-menu-item px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer flex items-center gap-2 transition-colors"
140
+ className="vertical-menu-item px-4 py-2 text-sm text-base-black hover:bg-gray-light-1 cursor-pointer flex items-center gap-2 transition-colors"
150
141
  onClick={e => {
151
142
  e.preventDefault();
152
143
  item.action?.(item);
@@ -171,7 +162,7 @@ const VerticalMenuDropdown: React.FC<Props> = ({
171
162
  <div className="inline-block vertical-menu-dropdown-wrapper">
172
163
  {handleShowHideMenu() > 0 && (
173
164
  <button
174
- className="vertical-menu-trigger-button p-2 rounded hover:bg-gray-100 transition-colors"
165
+ className="vertical-menu-trigger-button p-2 rounded text-base-gray hover:bg-gray-light-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
175
166
  onClick={toggleMenu}
176
167
  ref={menuButtonRef}
177
168
  >
@@ -5,7 +5,7 @@ export const ThreeDotIcon: React.FC<any> = () => {
5
5
  <svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
6
6
  <path
7
7
  d="M2 2.16665L2 2.17498M2 7.99998L2 8.00831M2 13.8333L2 13.8416M2 2.99998C1.53976 2.99998 1.16667 2.62688 1.16667 2.16665C1.16667 1.70641 1.53976 1.33331 2 1.33331C2.46024 1.33331 2.83333 1.70641 2.83333 2.16665C2.83333 2.62688 2.46024 2.99998 2 2.99998ZM2 8.83331C1.53976 8.83331 1.16667 8.46022 1.16667 7.99998C1.16667 7.53974 1.53976 7.16665 2 7.16665C2.46024 7.16665 2.83333 7.53974 2.83333 7.99998C2.83333 8.46022 2.46024 8.83331 2 8.83331ZM2 14.6666C1.53976 14.6666 1.16666 14.2935 1.16666 13.8333C1.16666 13.3731 1.53976 13 2 13C2.46024 13 2.83333 13.3731 2.83333 13.8333C2.83333 14.2935 2.46024 14.6666 2 14.6666Z"
8
- stroke="#313131"
8
+ stroke="currentColor"
9
9
  strokeWidth="1.5"
10
10
  strokeLinecap="round"
11
11
  strokeLinejoin="round"