qbs-react-grid 2.2.5 → 2.2.9

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.
Files changed (57) hide show
  1. package/dist/css/qbs-react-grid.css +1 -1
  2. package/dist/css/qbs-react-grid.min.css +1 -1
  3. package/dist/css/qbs-react-grid.min.css.map +1 -1
  4. package/es/index.d.ts +2 -0
  5. package/es/index.js +2 -1
  6. package/es/less/qbs-table.less +58 -4
  7. package/es/qbsTable/CustomTableCell.js +27 -10
  8. package/es/qbsTable/QbsTable.js +42 -7
  9. package/es/qbsTable/TableCardList.js +40 -7
  10. package/es/qbsTable/Toolbar.js +87 -23
  11. package/es/qbsTable/commontypes.d.ts +19 -0
  12. package/es/qbsTable/labels.d.ts +25 -0
  13. package/es/qbsTable/labels.js +32 -0
  14. package/es/qbsTable/utilities/ColumShowHide.d.ts +3 -0
  15. package/es/qbsTable/utilities/ColumShowHide.js +112 -33
  16. package/es/qbsTable/utilities/VerticalDropDownMenu.d.ts +11 -0
  17. package/es/qbsTable/utilities/VerticalDropDownMenu.js +182 -0
  18. package/es/qbsTable/utilities/columnToggleCoordinator.d.ts +5 -0
  19. package/es/qbsTable/utilities/columnToggleCoordinator.js +9 -0
  20. package/es/qbsTable/utilities/icons.d.ts +3 -0
  21. package/es/qbsTable/utilities/icons.js +67 -3
  22. package/es/qbsTable/utilities/verticalMenuCoordinator.d.ts +5 -0
  23. package/es/qbsTable/utilities/verticalMenuCoordinator.js +9 -0
  24. package/lib/index.d.ts +2 -0
  25. package/lib/index.js +6 -2
  26. package/lib/less/qbs-table.less +58 -4
  27. package/lib/qbsTable/CustomTableCell.js +27 -10
  28. package/lib/qbsTable/QbsTable.js +42 -7
  29. package/lib/qbsTable/TableCardList.js +40 -7
  30. package/lib/qbsTable/Toolbar.js +85 -21
  31. package/lib/qbsTable/commontypes.d.ts +19 -0
  32. package/lib/qbsTable/labels.d.ts +25 -0
  33. package/lib/qbsTable/labels.js +40 -0
  34. package/lib/qbsTable/utilities/ColumShowHide.d.ts +3 -0
  35. package/lib/qbsTable/utilities/ColumShowHide.js +112 -32
  36. package/lib/qbsTable/utilities/VerticalDropDownMenu.d.ts +11 -0
  37. package/lib/qbsTable/utilities/VerticalDropDownMenu.js +190 -0
  38. package/lib/qbsTable/utilities/columnToggleCoordinator.d.ts +5 -0
  39. package/lib/qbsTable/utilities/columnToggleCoordinator.js +15 -0
  40. package/lib/qbsTable/utilities/icons.d.ts +3 -0
  41. package/lib/qbsTable/utilities/icons.js +72 -5
  42. package/lib/qbsTable/utilities/verticalMenuCoordinator.d.ts +5 -0
  43. package/lib/qbsTable/utilities/verticalMenuCoordinator.js +15 -0
  44. package/package.json +9 -1
  45. package/src/index.ts +6 -0
  46. package/src/less/qbs-table.less +58 -4
  47. package/src/qbsTable/CustomTableCell.tsx +28 -8
  48. package/src/qbsTable/QbsTable.tsx +32 -4
  49. package/src/qbsTable/TableCardList.tsx +30 -4
  50. package/src/qbsTable/Toolbar.tsx +99 -29
  51. package/src/qbsTable/commontypes.ts +20 -0
  52. package/src/qbsTable/labels.ts +55 -0
  53. package/src/qbsTable/utilities/ColumShowHide.tsx +170 -84
  54. package/src/qbsTable/utilities/VerticalDropDownMenu.tsx +216 -0
  55. package/src/qbsTable/utilities/columnToggleCoordinator.ts +14 -0
  56. package/src/qbsTable/utilities/icons.tsx +76 -3
  57. package/src/qbsTable/utilities/verticalMenuCoordinator.ts +14 -0
@@ -0,0 +1,55 @@
1
+ export type QbsTableLabels = {
2
+ search?: string;
3
+ searchAriaLabel?: string;
4
+ clear?: string;
5
+ selectedItems?: string;
6
+ switchToDefaultView?: string;
7
+ switchToRelaxedView?: string;
8
+ switchToFullScreen?: string;
9
+ switchToTableView?: string;
10
+ switchToCardView?: string;
11
+ noDataFound?: string;
12
+ showingRange?: (start: number, end: number, total: number) => string;
13
+ itemsPerPage?: string;
14
+ fixedColumns?: string;
15
+ visibleColumns?: string;
16
+ availableColumns?: string;
17
+ resetToDefault?: string;
18
+ save?: string;
19
+ viewMore?: string;
20
+ viewLess?: string;
21
+ actions?: string;
22
+ };
23
+
24
+ export const defaultQbsTableLabels: QbsTableLabels = {
25
+ search: 'Search',
26
+ searchAriaLabel: 'Search',
27
+ clear: 'Clear',
28
+ selectedItems: 'Selected items',
29
+ switchToDefaultView: 'Switch to Default View',
30
+ switchToRelaxedView: 'Switch to Relaxed View',
31
+ switchToFullScreen: 'Switch to Full Screen',
32
+ switchToTableView: 'Switch to Table View',
33
+ switchToCardView: 'Switch to Card View',
34
+ noDataFound: 'No Data Found',
35
+ showingRange: (start, end, total) => `Showing ${start} to ${end} of ${total}`,
36
+ itemsPerPage: 'Items per page',
37
+ fixedColumns: 'FIXED COLUMNS',
38
+ visibleColumns: 'VISIBLE COLUMNS',
39
+ availableColumns: 'AVAILABLE COLUMNS',
40
+ resetToDefault: 'Reset to default',
41
+ save: 'Save',
42
+ viewMore: 'View More',
43
+ viewLess: 'View Less',
44
+ actions: 'Actions',
45
+ };
46
+
47
+ export const mergeQbsTableLabels = (overrides?: QbsTableLabels): QbsTableLabels => ({
48
+ ...defaultQbsTableLabels,
49
+ ...overrides,
50
+ });
51
+
52
+ export const formatSelectedItems = (count: number, labels?: QbsTableLabels): string => {
53
+ const merged = mergeQbsTableLabels(labels);
54
+ return `${merged.selectedItems} (${count})`;
55
+ };
@@ -1,6 +1,14 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { useCallback, useEffect, useId, useRef, useState } from 'react';
2
+ import ReactDOM from 'react-dom';
2
3
 
3
4
  import { QbsColumnProps } from '../commontypes';
5
+ import type { QbsTableLabels } from '../labels';
6
+ import { mergeQbsTableLabels } from '../labels';
7
+ import {
8
+ closeOtherColumnToggles,
9
+ COLUMN_TOGGLE_CLOSE_OTHERS,
10
+ type ColumnToggleCloseDetail,
11
+ } from './columnToggleCoordinator';
4
12
  import { SettingsIcon } from './icons';
5
13
 
6
14
  interface ColumnToggleProps {
@@ -12,6 +20,8 @@ interface ColumnToggleProps {
12
20
  handleColumnToggle?: (columns: QbsColumnProps[]) => void;
13
21
  handleResetColumns?: () => void;
14
22
  tableHeight?: number;
23
+ labels?: QbsTableLabels;
24
+ rtl?: boolean;
15
25
  }
16
26
 
17
27
  const ColumnToggle: React.FC<ColumnToggleProps> = ({
@@ -22,17 +32,64 @@ const ColumnToggle: React.FC<ColumnToggleProps> = ({
22
32
  setIsOpen,
23
33
  handleResetColumns,
24
34
  handleColumnToggle,
25
- tableHeight = 450
35
+ tableHeight = 450,
36
+ labels: labelsProp,
37
+ rtl = false,
26
38
  }) => {
39
+ const labels = mergeQbsTableLabels(labelsProp);
40
+ const toggleId = useId();
27
41
  const [draggedItem, setDraggedItem] = useState<number | null>(null);
28
42
  const popupRef = useRef<HTMLDivElement | null>(null);
43
+ const settingsBtnRef = useRef<HTMLButtonElement | null>(null);
29
44
  const [dragOverPosition, setDragOverPosition] = useState<number | null>();
45
+ const [position, setPosition] = useState({ top: 0, left: 0 });
46
+
47
+ const updatePopupPosition = useCallback(() => {
48
+ if (!settingsBtnRef.current) return;
49
+ const rect = settingsBtnRef.current.getBoundingClientRect();
50
+ const viewportPadding = 8;
51
+ const popupWidth = popupRef.current?.offsetWidth || 272;
52
+ const popupHeight = popupRef.current?.offsetHeight || 320;
53
+
54
+ let left = rtl ? rect.left : rect.right - popupWidth;
55
+ if (left + popupWidth > window.innerWidth - viewportPadding) {
56
+ left = Math.max(viewportPadding, window.innerWidth - popupWidth - viewportPadding);
57
+ }
58
+ if (left < viewportPadding) {
59
+ left = viewportPadding;
60
+ }
61
+
62
+ let top = rect.bottom + 4;
63
+ if (top + popupHeight > window.innerHeight - viewportPadding) {
64
+ top = Math.max(viewportPadding, rect.top - popupHeight - 4);
65
+ }
66
+
67
+ setPosition({ top, left });
68
+ }, [rtl]);
69
+
70
+ useEffect(() => {
71
+ const handleCloseOthers = (event: Event) => {
72
+ const detail = (event as CustomEvent<ColumnToggleCloseDetail>).detail;
73
+ if (detail?.exceptId !== toggleId) {
74
+ setIsOpen(false);
75
+ }
76
+ };
77
+ document.addEventListener(COLUMN_TOGGLE_CLOSE_OTHERS, handleCloseOthers);
78
+ return () => document.removeEventListener(COLUMN_TOGGLE_CLOSE_OTHERS, handleCloseOthers);
79
+ }, [setIsOpen, toggleId]);
80
+
81
+ useEffect(() => {
82
+ if (!isOpen) return;
83
+ updatePopupPosition();
84
+ const frame = requestAnimationFrame(() => updatePopupPosition());
85
+ return () => cancelAnimationFrame(frame);
86
+ }, [isOpen, updatePopupPosition]);
30
87
 
31
88
  const handleToggle = useCallback(
32
89
  (columnName: string) => {
33
90
  onToggle(columnName);
34
91
  },
35
- [onToggle]
92
+ [onToggle],
36
93
  );
37
94
 
38
95
  const onDragStart = useCallback((e: React.DragEvent, index: number) => {
@@ -59,22 +116,28 @@ const ColumnToggle: React.FC<ColumnToggleProps> = ({
59
116
  }
60
117
  setDraggedItem(null);
61
118
  },
62
- [columns, draggedItem]
119
+ [columns, draggedItem, onReorder],
63
120
  );
121
+
64
122
  const handleClickOutside = useCallback(
65
123
  (event: MouseEvent) => {
66
- if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
67
- setIsOpen(false);
124
+ const target = event.target as Node;
125
+ if (
126
+ popupRef.current?.contains(target) ||
127
+ settingsBtnRef.current?.contains(target)
128
+ ) {
129
+ return;
68
130
  }
131
+ setIsOpen(false);
69
132
  },
70
- [setIsOpen]
133
+ [setIsOpen],
71
134
  );
135
+
72
136
  useEffect(() => {
137
+ if (!isOpen) return;
73
138
  document.addEventListener('mousedown', handleClickOutside);
74
- return () => {
75
- document.removeEventListener('mousedown', handleClickOutside);
76
- };
77
- }, [handleClickOutside]);
139
+ return () => document.removeEventListener('mousedown', handleClickOutside);
140
+ }, [handleClickOutside, isOpen]);
78
141
 
79
142
  const renderFixedColumn = (column: QbsColumnProps, index: number) => (
80
143
  <div
@@ -101,13 +164,14 @@ const ColumnToggle: React.FC<ColumnToggleProps> = ({
101
164
  <path
102
165
  d="M0 3.21739L2.89883 6L8 1.06994L6.89494 0L2.89883 3.86768L1.09728 2.14745L0 3.21739Z"
103
166
  fill="white"
104
- ></path>
167
+ />
105
168
  </svg>
106
169
  </label>
107
170
  </div>
108
171
  <div className="qbs-table-popup-value">{column.title}</div>
109
172
  </div>
110
173
  );
174
+
111
175
  const renderColumn = (column: QbsColumnProps, index: number) => (
112
176
  <div
113
177
  key={column.title}
@@ -137,7 +201,7 @@ const ColumnToggle: React.FC<ColumnToggleProps> = ({
137
201
  <path
138
202
  d="M0 3.21739L2.89883 6L8 1.06994L6.89494 0L2.89883 3.86768L1.09728 2.14745L0 3.21739Z"
139
203
  fill="white"
140
- ></path>
204
+ />
141
205
  </svg>
142
206
  </label>
143
207
  </div>
@@ -162,88 +226,110 @@ const ColumnToggle: React.FC<ColumnToggleProps> = ({
162
226
  )}
163
227
  </div>
164
228
  );
165
- const handleAvailableColumns = () => {
166
- return columns.filter(item => !item.isVisible)?.length > 0 ? true : false;
167
- };
229
+
230
+ const handleAvailableColumns = () =>
231
+ columns.filter(item => !item.isVisible)?.length > 0;
232
+
168
233
  const handleColToggle = () => {
169
234
  setIsOpen(false);
170
235
  handleColumnToggle?.(columns);
171
236
  };
172
- return (
173
- <div>
174
- <button onClick={() => setIsOpen(!isOpen)}>
175
- <SettingsIcon />
176
- </button>
177
- {isOpen && (
178
- <div>
179
- <div
180
- className="qbs-table-column-popup"
181
- style={{ maxHeight: tableHeight - 40 }}
182
- ref={popupRef}
183
- >
184
- <div className="qbs-table-popup-container">
185
- <div className="qbs-table-popup-item">
186
- <div className="qbs-table-popup-label">FIXED COLUMNS</div>
187
- <div className="qbs-table-columns-container">
188
- <div className="qbs-table-column">
189
- {columns.map((column, index) =>
190
- column.fixed ? renderFixedColumn(column, index) : ''
191
- )}
192
- </div>
193
- </div>
194
- </div>
195
- <div className="qbs-table-divider"></div>
196
- <div className="qbs-table-popup-item">
197
- <div className="qbs-table-popup-label">VISIBLE COLUMNS</div>
198
- <div className="qbs-table-columns-container">
199
- <div className="qbs-table-column">
200
- {columns.map((column, index) =>
201
- column.isVisible && !column.fixed ? renderColumn(column, index) : ''
202
- )}
203
- </div>
204
- </div>
205
- </div>
206
- {handleAvailableColumns() && (
207
- <>
208
- <div className="qbs-table-divider"></div>
209
- <div className="qbs-table-popup-item">
210
- <div className="qbs-table-popup-label">AVAILABLE COLUMNS</div>
211
- <div className="qbs-table-columns-container">
212
- <div className="qbs-table-column">
213
- {columns.map((column, index) =>
214
- !column.isVisible && !column.fixed ? renderFixedColumn(column, index) : ''
215
- )}
216
- </div>
217
- </div>
218
- </div>
219
- </>
237
+
238
+ const portalTarget = document.getElementById('portal-root') ?? document.body;
239
+
240
+ const popupContent = (
241
+ <div
242
+ className={`qbs-table-column-popup${rtl ? ' qbs-table-column-popup--rtl' : ''}`}
243
+ style={{
244
+ position: 'fixed',
245
+ top: position.top,
246
+ left: position.left,
247
+ maxHeight: tableHeight - 40,
248
+ zIndex: 10060,
249
+ }}
250
+ ref={popupRef}
251
+ dir={rtl ? 'rtl' : 'ltr'}
252
+ >
253
+ <div className="qbs-table-popup-container">
254
+ <div className="qbs-table-popup-item">
255
+ <div className="qbs-table-popup-label">{labels.fixedColumns}</div>
256
+ <div className="qbs-table-columns-container">
257
+ <div className="qbs-table-column">
258
+ {columns.map((column, index) =>
259
+ column.fixed ? renderFixedColumn(column, index) : null,
260
+ )}
261
+ </div>
262
+ </div>
263
+ </div>
264
+ <div className="qbs-table-divider" />
265
+ <div className="qbs-table-popup-item">
266
+ <div className="qbs-table-popup-label">{labels.visibleColumns}</div>
267
+ <div className="qbs-table-columns-container">
268
+ <div className="qbs-table-column">
269
+ {columns.map((column, index) =>
270
+ column.isVisible && !column.fixed ? renderColumn(column, index) : null,
220
271
  )}
221
272
  </div>
222
- {handleResetColumns && (
223
- <>
224
- <div className="qbs-table-divider"></div>
225
- <div
226
- className="qbs-table-popup-item"
227
- style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}
228
- >
229
- <a
230
- className="qbs-table-reset-link"
231
- href="#"
232
- onClick={() => handleResetColumns?.()}
233
- >
234
- Reset to default
235
- </a>
236
- <a className="qbs-table-reset-link" href="#" onClick={() => handleColToggle()}>
237
- Save
238
- </a>
239
- </div>
240
- </>
241
- )}
242
273
  </div>
243
274
  </div>
275
+ {handleAvailableColumns() && (
276
+ <>
277
+ <div className="qbs-table-divider" />
278
+ <div className="qbs-table-popup-item">
279
+ <div className="qbs-table-popup-label">{labels.availableColumns}</div>
280
+ <div className="qbs-table-columns-container">
281
+ <div className="qbs-table-column">
282
+ {columns.map((column, index) =>
283
+ !column.isVisible && !column.fixed ? renderFixedColumn(column, index) : null,
284
+ )}
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </>
289
+ )}
290
+ </div>
291
+ {handleResetColumns && (
292
+ <>
293
+ <div className="qbs-table-divider" />
294
+ <div className="qbs-table-popup-footer">
295
+ <button
296
+ type="button"
297
+ className="qbs-table-reset-link"
298
+ onClick={() => handleResetColumns?.()}
299
+ >
300
+ {labels.resetToDefault}
301
+ </button>
302
+ <button type="button" className="qbs-table-reset-link" onClick={() => handleColToggle()}>
303
+ {labels.save}
304
+ </button>
305
+ </div>
306
+ </>
244
307
  )}
245
308
  </div>
246
309
  );
310
+
311
+ return (
312
+ <div className="qbs-table-settings-wrapper">
313
+ <button
314
+ type="button"
315
+ className="qbs-table-settings-btn"
316
+ ref={settingsBtnRef}
317
+ onClick={event => {
318
+ event.stopPropagation();
319
+ if (isOpen) {
320
+ setIsOpen(false);
321
+ return;
322
+ }
323
+ closeOtherColumnToggles(toggleId);
324
+ updatePopupPosition();
325
+ setIsOpen(true);
326
+ }}
327
+ >
328
+ <SettingsIcon />
329
+ </button>
330
+ {isOpen && portalTarget && ReactDOM.createPortal(popupContent, portalTarget)}
331
+ </div>
332
+ );
247
333
  };
248
334
 
249
335
  export default ColumnToggle;
@@ -0,0 +1,216 @@
1
+ import React, { useEffect, useId, useRef, useState } from 'react';
2
+ import ReactDOM from 'react-dom';
3
+
4
+ import { ThreeDotIcon } from './icons';
5
+ import TooltipComponent from './ToolTip';
6
+ import type { ActionProps } from '../commontypes';
7
+ import {
8
+ closeOtherVerticalMenus,
9
+ VERTICAL_MENU_CLOSE_OTHERS,
10
+ type VerticalMenuCloseDetail,
11
+ } from './verticalMenuCoordinator';
12
+
13
+ type VerticalMenuDropdownProps = {
14
+ actionDropDown?: readonly ActionProps[];
15
+ handleMenuActions?: (actions: ActionProps, rowData: any) => void;
16
+ rowData: any;
17
+ tableBodyRef?: React.RefObject<HTMLDivElement>;
18
+ rowIndex?: number;
19
+ };
20
+
21
+ const VerticalMenuDropdown: React.FC<VerticalMenuDropdownProps> = ({
22
+ actionDropDown,
23
+ handleMenuActions,
24
+ rowData,
25
+ tableBodyRef,
26
+ rowIndex,
27
+ }) => {
28
+ const [openMenu, setOpenMenu] = useState(false);
29
+ const [position, setPosition] = useState({ top: 0, left: 0 });
30
+ const menuId = useId();
31
+ const menuButtonRef = useRef<HTMLButtonElement>(null);
32
+ const menuRef = useRef<HTMLDivElement>(null);
33
+
34
+ useEffect(() => {
35
+ const handleCloseOthers = (event: Event) => {
36
+ const detail = (event as CustomEvent<VerticalMenuCloseDetail>).detail;
37
+ if (detail?.exceptId !== menuId) {
38
+ setOpenMenu(false);
39
+ }
40
+ };
41
+
42
+ document.addEventListener(VERTICAL_MENU_CLOSE_OTHERS, handleCloseOthers);
43
+ return () => document.removeEventListener(VERTICAL_MENU_CLOSE_OTHERS, handleCloseOthers);
44
+ }, [menuId]);
45
+
46
+ const updateMenuPosition = () => {
47
+ if (!menuButtonRef.current) return;
48
+
49
+ const rect = menuButtonRef.current.getBoundingClientRect();
50
+ const viewportPadding = 8;
51
+ const menuGap = 4;
52
+ const visibleItems =
53
+ actionDropDown?.filter(
54
+ item =>
55
+ !item.hidden && !(item.hide?.call(item, rowData, rowIndex) ?? false),
56
+ ) ?? [];
57
+ const menuWidth =
58
+ menuRef.current && menuRef.current.offsetWidth > 0
59
+ ? menuRef.current.offsetWidth
60
+ : Math.max(120, visibleItems.length * 48);
61
+ const menuHeight = visibleItems.length * 40;
62
+ const spaceBelow = window.innerHeight - rect.bottom;
63
+ const openBelow = spaceBelow >= menuHeight + menuGap;
64
+
65
+ let left = rect.left;
66
+ if (left + menuWidth > window.innerWidth - viewportPadding) {
67
+ left = Math.max(viewportPadding, rect.right - menuWidth);
68
+ }
69
+
70
+ setPosition({
71
+ top: openBelow ? rect.bottom + menuGap : rect.top - menuHeight - menuGap,
72
+ left,
73
+ });
74
+ };
75
+
76
+ useEffect(() => {
77
+ if (!openMenu) return;
78
+ updateMenuPosition();
79
+ const frame = requestAnimationFrame(() => updateMenuPosition());
80
+ const resizeObserver =
81
+ menuRef.current && typeof ResizeObserver !== 'undefined'
82
+ ? new ResizeObserver(() => updateMenuPosition())
83
+ : null;
84
+ if (resizeObserver && menuRef.current) {
85
+ resizeObserver.observe(menuRef.current);
86
+ }
87
+ return () => {
88
+ cancelAnimationFrame(frame);
89
+ resizeObserver?.disconnect();
90
+ };
91
+ }, [openMenu]);
92
+
93
+ useEffect(() => {
94
+ if (!openMenu) return;
95
+
96
+ const handleClickOutside = (event: MouseEvent) => {
97
+ const target = event.target as Node;
98
+ if (
99
+ menuRef.current?.contains(target) ||
100
+ menuButtonRef.current?.contains(target)
101
+ ) {
102
+ return;
103
+ }
104
+ setOpenMenu(false);
105
+ };
106
+ const handleScroll = () => setOpenMenu(false);
107
+
108
+ document.addEventListener('click', handleClickOutside);
109
+ window.addEventListener('scroll', handleScroll, true);
110
+
111
+ return () => {
112
+ document.removeEventListener('click', handleClickOutside);
113
+ window.removeEventListener('scroll', handleScroll, true);
114
+ };
115
+ }, [openMenu]);
116
+
117
+ useEffect(() => {
118
+ const scrollbarHandle = document.querySelector('.rs-table-scrollbar-handle');
119
+ if (!scrollbarHandle) return;
120
+
121
+ const observer = new MutationObserver(mutations => {
122
+ for (const mutation of mutations) {
123
+ if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
124
+ setOpenMenu(false);
125
+ }
126
+ }
127
+ });
128
+
129
+ observer.observe(scrollbarHandle, {
130
+ attributes: true,
131
+ attributeFilter: ['style'],
132
+ });
133
+
134
+ return () => observer.disconnect();
135
+ }, [openMenu]);
136
+
137
+ const handleMenuItemClick = (slug: ActionProps) => {
138
+ handleMenuActions?.(slug, rowData);
139
+ slug.action?.(rowData);
140
+ setOpenMenu(false);
141
+ };
142
+
143
+ const visibleCount =
144
+ actionDropDown?.filter(
145
+ item => !item.hidden && !(item.hide?.call(item, rowData, rowIndex) ?? false),
146
+ ).length ?? 0;
147
+
148
+ const portalTarget =
149
+ document.getElementById('portal-root') ?? document.body;
150
+
151
+ const dropdownContent = (
152
+ <div
153
+ className="absolute z-[60] min-w-48 rounded-md vertical-menu-dropdown-content"
154
+ ref={menuRef}
155
+ style={{
156
+ top: position.top,
157
+ left: position.left,
158
+ position: 'fixed',
159
+ minWidth: 120,
160
+ width: 'max-content',
161
+ }}
162
+ >
163
+ <div className="py-1">
164
+ {actionDropDown?.map(item =>
165
+ !item.hidden && !(item.hide?.call(item, rowData, rowIndex) ?? false) ? (
166
+ <div
167
+ key={item.title}
168
+ 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"
169
+ onClick={e => {
170
+ e.preventDefault();
171
+ item.action?.(item);
172
+ handleMenuItemClick(item);
173
+ }}
174
+ >
175
+ <TooltipComponent title={item.toolTip} tableBodyRef={tableBodyRef}>
176
+ <div className="vertical-menu-icon-title flex items-center gap-2">
177
+ {item.icon && <span className="vertical-menu-icon">{item.icon}</span>}
178
+ <span className="vertical-menu-title">{item.title}</span>
179
+ </div>
180
+ </TooltipComponent>
181
+ </div>
182
+ ) : null,
183
+ )}
184
+ </div>
185
+ </div>
186
+ );
187
+
188
+ return (
189
+ <>
190
+ <div className="inline-block vertical-menu-dropdown-wrapper">
191
+ {visibleCount > 0 && (
192
+ <button
193
+ type="button"
194
+ 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"
195
+ onClick={event => {
196
+ event.stopPropagation();
197
+ if (openMenu) {
198
+ setOpenMenu(false);
199
+ return;
200
+ }
201
+ closeOtherVerticalMenus(menuId);
202
+ updateMenuPosition();
203
+ setOpenMenu(true);
204
+ }}
205
+ ref={menuButtonRef}
206
+ >
207
+ <ThreeDotIcon />
208
+ </button>
209
+ )}
210
+ </div>
211
+ {openMenu && portalTarget && ReactDOM.createPortal(dropdownContent, portalTarget)}
212
+ </>
213
+ );
214
+ };
215
+
216
+ export default VerticalMenuDropdown;
@@ -0,0 +1,14 @@
1
+ export const COLUMN_TOGGLE_CLOSE_OTHERS = 'qbs-column-toggle-close-others';
2
+
3
+ export type ColumnToggleCloseDetail = {
4
+ exceptId: string;
5
+ };
6
+
7
+ export const closeOtherColumnToggles = (exceptId: string) => {
8
+ if (typeof document === 'undefined') return;
9
+ document.dispatchEvent(
10
+ new CustomEvent<ColumnToggleCloseDetail>(COLUMN_TOGGLE_CLOSE_OTHERS, {
11
+ detail: { exceptId },
12
+ }),
13
+ );
14
+ };