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.
- package/dist/css/qbs-react-grid.css +1 -1
- package/dist/css/qbs-react-grid.min.css +1 -1
- package/dist/css/qbs-react-grid.min.css.map +1 -1
- package/es/index.d.ts +2 -0
- package/es/index.js +2 -1
- package/es/less/qbs-table.less +58 -4
- package/es/qbsTable/CustomTableCell.js +27 -10
- package/es/qbsTable/QbsTable.js +42 -7
- package/es/qbsTable/TableCardList.js +40 -7
- package/es/qbsTable/Toolbar.js +87 -23
- package/es/qbsTable/commontypes.d.ts +19 -0
- package/es/qbsTable/labels.d.ts +25 -0
- package/es/qbsTable/labels.js +32 -0
- package/es/qbsTable/utilities/ColumShowHide.d.ts +3 -0
- package/es/qbsTable/utilities/ColumShowHide.js +112 -33
- package/es/qbsTable/utilities/VerticalDropDownMenu.d.ts +11 -0
- package/es/qbsTable/utilities/VerticalDropDownMenu.js +182 -0
- package/es/qbsTable/utilities/columnToggleCoordinator.d.ts +5 -0
- package/es/qbsTable/utilities/columnToggleCoordinator.js +9 -0
- package/es/qbsTable/utilities/icons.d.ts +3 -0
- package/es/qbsTable/utilities/icons.js +67 -3
- package/es/qbsTable/utilities/verticalMenuCoordinator.d.ts +5 -0
- package/es/qbsTable/utilities/verticalMenuCoordinator.js +9 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +6 -2
- package/lib/less/qbs-table.less +58 -4
- package/lib/qbsTable/CustomTableCell.js +27 -10
- package/lib/qbsTable/QbsTable.js +42 -7
- package/lib/qbsTable/TableCardList.js +40 -7
- package/lib/qbsTable/Toolbar.js +85 -21
- package/lib/qbsTable/commontypes.d.ts +19 -0
- package/lib/qbsTable/labels.d.ts +25 -0
- package/lib/qbsTable/labels.js +40 -0
- package/lib/qbsTable/utilities/ColumShowHide.d.ts +3 -0
- package/lib/qbsTable/utilities/ColumShowHide.js +112 -32
- package/lib/qbsTable/utilities/VerticalDropDownMenu.d.ts +11 -0
- package/lib/qbsTable/utilities/VerticalDropDownMenu.js +190 -0
- package/lib/qbsTable/utilities/columnToggleCoordinator.d.ts +5 -0
- package/lib/qbsTable/utilities/columnToggleCoordinator.js +15 -0
- package/lib/qbsTable/utilities/icons.d.ts +3 -0
- package/lib/qbsTable/utilities/icons.js +72 -5
- package/lib/qbsTable/utilities/verticalMenuCoordinator.d.ts +5 -0
- package/lib/qbsTable/utilities/verticalMenuCoordinator.js +15 -0
- package/package.json +9 -1
- package/src/index.ts +6 -0
- package/src/less/qbs-table.less +58 -4
- package/src/qbsTable/CustomTableCell.tsx +28 -8
- package/src/qbsTable/QbsTable.tsx +32 -4
- package/src/qbsTable/TableCardList.tsx +30 -4
- package/src/qbsTable/Toolbar.tsx +99 -29
- package/src/qbsTable/commontypes.ts +20 -0
- package/src/qbsTable/labels.ts +55 -0
- package/src/qbsTable/utilities/ColumShowHide.tsx +170 -84
- package/src/qbsTable/utilities/VerticalDropDownMenu.tsx +216 -0
- package/src/qbsTable/utilities/columnToggleCoordinator.ts +14 -0
- package/src/qbsTable/utilities/icons.tsx +76 -3
- 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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
};
|