qantler-flaz-dataview 1.0.0
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/package.json +11 -0
- package/src/FlazDataView.css +4423 -0
- package/src/FlazDataView.jsx +2943 -0
- package/src/FlazDataViewList.jsx +787 -0
- package/src/MdtActiveFilterBar.jsx +263 -0
- package/src/MdtBulkSelectionBar.jsx +91 -0
- package/src/MdtColumnPickerPopup.jsx +59 -0
- package/src/MdtColumnVisibilityList.jsx +38 -0
- package/src/MdtCreateViewModal.jsx +280 -0
- package/src/MdtDatetimeValueEditor.jsx +259 -0
- package/src/MdtDeleteViewConfirmModal.jsx +83 -0
- package/src/MdtDisplayPopup.jsx +170 -0
- package/src/MdtEmptyState.jsx +20 -0
- package/src/MdtFilterColumnPicker.jsx +66 -0
- package/src/MdtFilterOperatorDropdown.jsx +58 -0
- package/src/MdtFilterSection.jsx +73 -0
- package/src/MdtFilterToolbar.jsx +81 -0
- package/src/MdtInlineField.css +221 -0
- package/src/MdtInlineField.jsx +187 -0
- package/src/MdtInlineFieldRow.jsx +71 -0
- package/src/MdtNoDataIllustration.jsx +64 -0
- package/src/MdtPopupChecklist.jsx +70 -0
- package/src/MdtRowActionsMenu.jsx +281 -0
- package/src/MdtSidebarNav.jsx +26 -0
- package/src/MdtTabsRow.jsx +354 -0
- package/src/MdtTabsSection.jsx +35 -0
- package/src/MdtTruncateWithTooltip.jsx +82 -0
- package/src/MdtValueEditorPopup.jsx +950 -0
- package/src/filterDisplayUtils.jsx +96 -0
- package/src/mdtClientSideFilter.js +68 -0
- package/src/mdtColumnEditUtils.js +403 -0
- package/src/mdtDateFilterUtils.js +135 -0
- package/src/mdtExportUtils.js +234 -0
- package/src/mdtFilterUtils.js +56 -0
- package/src/mdtInlineFieldDisplay.js +66 -0
- package/src/mdtMobilePopupPosition.js +107 -0
- package/src/mdtMultiSelectDisplay.jsx +279 -0
- package/src/mdtSavedViewFilterMatch.js +154 -0
- package/src/mdtTabChangeUtils.js +12 -0
- package/src/mdtTabStatusRules.js +111 -0
- package/src/mdtTableConstants.js +2 -0
- package/src/mdtTooltipFormat.js +44 -0
- package/src/renderInlineFieldValue.jsx +183 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
const MdtSidebarNav = ({ items = [], activeKey = null, onItemClick }) => {
|
|
4
|
+
return (
|
|
5
|
+
<nav className="mdt-sidebar-nav" aria-label="Master data navigation">
|
|
6
|
+
{items.map((item) => {
|
|
7
|
+
const key = item?.key;
|
|
8
|
+
const isActive = key === activeKey;
|
|
9
|
+
return (
|
|
10
|
+
<button
|
|
11
|
+
key={String(key)}
|
|
12
|
+
type="button"
|
|
13
|
+
className={`mdt-sidebar-nav-item${isActive ? " mdt-sidebar-nav-item--active" : ""}`}
|
|
14
|
+
onClick={() => onItemClick && onItemClick(item)}
|
|
15
|
+
disabled={Boolean(item?.disabled)}
|
|
16
|
+
title={item?.label || ""}
|
|
17
|
+
>
|
|
18
|
+
<span className="mdt-sidebar-nav-item-label">{item?.label}</span>
|
|
19
|
+
</button>
|
|
20
|
+
);
|
|
21
|
+
})}
|
|
22
|
+
</nav>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default MdtSidebarNav;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { MoreHorizontal, Pencil } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
const TAB_GAP = 8;
|
|
6
|
+
const MORE_BTN_FALLBACK = 40;
|
|
7
|
+
|
|
8
|
+
const MdtTabsRow = ({
|
|
9
|
+
tabs = [],
|
|
10
|
+
activeTabId,
|
|
11
|
+
onTabClick,
|
|
12
|
+
onEditSavedView,
|
|
13
|
+
tabsConfig = {},
|
|
14
|
+
leadingSlot = null,
|
|
15
|
+
rowClassName = "",
|
|
16
|
+
}) => {
|
|
17
|
+
const {
|
|
18
|
+
showCounts = true,
|
|
19
|
+
showSavedViewEdit = true,
|
|
20
|
+
moreButtonAriaLabel = "More views",
|
|
21
|
+
className: tabsClassName = "",
|
|
22
|
+
} = tabsConfig;
|
|
23
|
+
|
|
24
|
+
const currentUserId = React.useMemo(() => {
|
|
25
|
+
try {
|
|
26
|
+
const userInfo = JSON.parse(localStorage.getItem("UserInfoRMS") || "{}");
|
|
27
|
+
return userInfo?.userId ?? null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const isAdmin = React.useMemo(() => {
|
|
34
|
+
try {
|
|
35
|
+
const roles = localStorage.getItem("rmsRoles") || "";
|
|
36
|
+
return roles.includes("Admin");
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const canEditTab = React.useCallback(
|
|
43
|
+
(tab) => {
|
|
44
|
+
if (!tab.isSavedView) return false;
|
|
45
|
+
if (isAdmin && tab.isSystem) return true;
|
|
46
|
+
return currentUserId != null && tab.createdBy === currentUserId;
|
|
47
|
+
},
|
|
48
|
+
[currentUserId, isAdmin],
|
|
49
|
+
);
|
|
50
|
+
const tabsRowRef = useRef(null);
|
|
51
|
+
const tabsMeasureRef = useRef(null);
|
|
52
|
+
const moreBtnRef = useRef(null);
|
|
53
|
+
const overflowRef = useRef(null);
|
|
54
|
+
const measureRetryRef = useRef(0);
|
|
55
|
+
const [visibleCount, setVisibleCount] = useState(tabs.length);
|
|
56
|
+
const [overflowOpen, setOverflowOpen] = useState(false);
|
|
57
|
+
const [moreBtnWidth, setMoreBtnWidth] = useState(MORE_BTN_FALLBACK);
|
|
58
|
+
const [menuPosition, setMenuPosition] = useState(null);
|
|
59
|
+
|
|
60
|
+
const getAvailableWidth = useCallback(() => {
|
|
61
|
+
const row = tabsRowRef.current;
|
|
62
|
+
if (!row) return 0;
|
|
63
|
+
const style = window.getComputedStyle(row);
|
|
64
|
+
const padding =
|
|
65
|
+
parseFloat(style.paddingLeft || "0") + parseFloat(style.paddingRight || "0");
|
|
66
|
+
return Math.max(0, row.clientWidth - padding);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const measureTabs = useCallback(() => {
|
|
70
|
+
const measure = tabsMeasureRef.current;
|
|
71
|
+
if (!measure || tabs.length === 0) {
|
|
72
|
+
setVisibleCount(tabs.length);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const available = getAvailableWidth();
|
|
77
|
+
if (available <= 0) return;
|
|
78
|
+
|
|
79
|
+
const entries = measure.querySelectorAll("[data-tab-measure]");
|
|
80
|
+
const widths = Array.from(entries).map((el) => el.getBoundingClientRect().width);
|
|
81
|
+
|
|
82
|
+
if (widths.length !== tabs.length || widths.some((w) => w <= 0)) {
|
|
83
|
+
if (measureRetryRef.current < 8) {
|
|
84
|
+
measureRetryRef.current += 1;
|
|
85
|
+
requestAnimationFrame(measureTabs);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
measureRetryRef.current = 0;
|
|
90
|
+
|
|
91
|
+
const reserve = (moreBtnRef.current?.offsetWidth ?? moreBtnWidth) + TAB_GAP;
|
|
92
|
+
|
|
93
|
+
let sum = 0;
|
|
94
|
+
for (let i = 0; i < widths.length; i++) {
|
|
95
|
+
sum += widths[i] + (i > 0 ? TAB_GAP : 0);
|
|
96
|
+
}
|
|
97
|
+
if (sum <= available) {
|
|
98
|
+
setVisibleCount(tabs.length);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
sum = 0;
|
|
103
|
+
let count = 0;
|
|
104
|
+
for (let i = 0; i < widths.length; i++) {
|
|
105
|
+
const w = widths[i] + (i > 0 ? TAB_GAP : 0);
|
|
106
|
+
if (sum + w + reserve > available) break;
|
|
107
|
+
sum += w;
|
|
108
|
+
count = i + 1;
|
|
109
|
+
}
|
|
110
|
+
setVisibleCount(Math.max(1, count));
|
|
111
|
+
}, [tabs, getAvailableWidth, moreBtnWidth]);
|
|
112
|
+
|
|
113
|
+
const scheduleMeasure = useCallback(() => {
|
|
114
|
+
requestAnimationFrame(() => {
|
|
115
|
+
requestAnimationFrame(measureTabs);
|
|
116
|
+
});
|
|
117
|
+
}, [measureTabs]);
|
|
118
|
+
|
|
119
|
+
useLayoutEffect(() => {
|
|
120
|
+
measureRetryRef.current = 0;
|
|
121
|
+
scheduleMeasure();
|
|
122
|
+
}, [scheduleMeasure, activeTabId, tabs]);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
scheduleMeasure();
|
|
126
|
+
}, [moreBtnWidth, scheduleMeasure]);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const row = tabsRowRef.current;
|
|
130
|
+
if (!row) return undefined;
|
|
131
|
+
|
|
132
|
+
const ro = new ResizeObserver(() => scheduleMeasure());
|
|
133
|
+
ro.observe(row);
|
|
134
|
+
|
|
135
|
+
window.addEventListener("resize", scheduleMeasure);
|
|
136
|
+
return () => {
|
|
137
|
+
ro.disconnect();
|
|
138
|
+
window.removeEventListener("resize", scheduleMeasure);
|
|
139
|
+
};
|
|
140
|
+
}, [scheduleMeasure]);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (moreBtnRef.current) {
|
|
144
|
+
setMoreBtnWidth(moreBtnRef.current.offsetWidth || MORE_BTN_FALLBACK);
|
|
145
|
+
}
|
|
146
|
+
}, [overflowOpen, tabs.length]);
|
|
147
|
+
|
|
148
|
+
const updateMenuPosition = useCallback(() => {
|
|
149
|
+
const btn = moreBtnRef.current;
|
|
150
|
+
if (!btn) return;
|
|
151
|
+
const rect = btn.getBoundingClientRect();
|
|
152
|
+
setMenuPosition({
|
|
153
|
+
top: rect.bottom + 4,
|
|
154
|
+
left: rect.right,
|
|
155
|
+
});
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
const { visibleTabs, overflowTabs } = useMemo(() => {
|
|
159
|
+
if (tabs.length === 0) {
|
|
160
|
+
return { visibleTabs: [], overflowTabs: [] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const activeIdx = tabs.findIndex((t) => t.id === activeTabId);
|
|
164
|
+
const hasOverflow = visibleCount < tabs.length;
|
|
165
|
+
let visible = tabs.slice(0, visibleCount);
|
|
166
|
+
|
|
167
|
+
if (hasOverflow && activeIdx >= visibleCount) {
|
|
168
|
+
visible = [
|
|
169
|
+
...tabs.slice(0, Math.max(0, visibleCount - 1)),
|
|
170
|
+
tabs[activeIdx],
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const visibleIds = new Set(visible.map((t) => t.id));
|
|
175
|
+
const overflow = tabs.filter((t) => !visibleIds.has(t.id));
|
|
176
|
+
return { visibleTabs: visible, overflowTabs: overflow };
|
|
177
|
+
}, [tabs, visibleCount, activeTabId]);
|
|
178
|
+
|
|
179
|
+
useLayoutEffect(() => {
|
|
180
|
+
if (!overflowOpen) {
|
|
181
|
+
setMenuPosition(null);
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
updateMenuPosition();
|
|
185
|
+
window.addEventListener("resize", updateMenuPosition);
|
|
186
|
+
window.addEventListener("scroll", updateMenuPosition, true);
|
|
187
|
+
return () => {
|
|
188
|
+
window.removeEventListener("resize", updateMenuPosition);
|
|
189
|
+
window.removeEventListener("scroll", updateMenuPosition, true);
|
|
190
|
+
};
|
|
191
|
+
}, [overflowOpen, updateMenuPosition, overflowTabs.length]);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!overflowOpen) return undefined;
|
|
195
|
+
const onOutside = (e) => {
|
|
196
|
+
const inWrap = overflowRef.current?.contains(e.target);
|
|
197
|
+
const inMenu = e.target.closest?.(".mdt-tabs-overflow-menu--portal");
|
|
198
|
+
if (!inWrap && !inMenu) setOverflowOpen(false);
|
|
199
|
+
};
|
|
200
|
+
document.addEventListener("mousedown", onOutside);
|
|
201
|
+
return () => document.removeEventListener("mousedown", onOutside);
|
|
202
|
+
}, [overflowOpen]);
|
|
203
|
+
|
|
204
|
+
const renderTabEntry = (tab, { forMeasure = false } = {}) => {
|
|
205
|
+
const isActive = activeTabId === tab.id;
|
|
206
|
+
return (
|
|
207
|
+
<div
|
|
208
|
+
key={forMeasure ? `measure-${tab.id}` : tab.id}
|
|
209
|
+
data-tab-measure={forMeasure ? "true" : undefined}
|
|
210
|
+
className={`mdt-tab-entry${isActive ? " mdt-tab-entry--active" : ""}`}
|
|
211
|
+
>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
className={`mdt-tab-item${isActive ? " mdt-tab-active" : ""}${tab.isSavedView && isActive ? " mdt-tab-item--editable" : ""}`}
|
|
215
|
+
onClick={() => !forMeasure && onTabClick(tab)}
|
|
216
|
+
>
|
|
217
|
+
<span className="mdt-tab-item-inner">
|
|
218
|
+
{tab.label}
|
|
219
|
+
{showCounts && tab.count !== undefined && tab.count !== null && (
|
|
220
|
+
<span className="mdt-tab-count">({tab.count})</span>
|
|
221
|
+
)}
|
|
222
|
+
{showSavedViewEdit && tab.isSavedView && isActive && !forMeasure && canEditTab(tab) && (
|
|
223
|
+
<span
|
|
224
|
+
role="button"
|
|
225
|
+
tabIndex={0}
|
|
226
|
+
className="mdt-tab-edit-btn"
|
|
227
|
+
aria-label={`Edit view ${tab.label}`}
|
|
228
|
+
onClick={(e) => {
|
|
229
|
+
e.stopPropagation();
|
|
230
|
+
onEditSavedView?.(tab.savedViewId);
|
|
231
|
+
}}
|
|
232
|
+
onKeyDown={(e) => {
|
|
233
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
e.stopPropagation();
|
|
236
|
+
onEditSavedView?.(tab.savedViewId);
|
|
237
|
+
}
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
<Pencil size={11} />
|
|
241
|
+
</span>
|
|
242
|
+
)}
|
|
243
|
+
{showSavedViewEdit && tab.isSavedView && isActive && forMeasure && canEditTab(tab) && (
|
|
244
|
+
<span className="mdt-tab-edit-btn mdt-tab-edit-btn--measure" aria-hidden="true">
|
|
245
|
+
<Pencil size={11} />
|
|
246
|
+
</span>
|
|
247
|
+
)}
|
|
248
|
+
</span>
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const overflowMenu =
|
|
255
|
+
overflowOpen && overflowTabs.length > 0 && menuPosition ? (
|
|
256
|
+
<div
|
|
257
|
+
className="mdt-tabs-overflow-menu mdt-tabs-overflow-menu--portal"
|
|
258
|
+
role="menu"
|
|
259
|
+
style={{
|
|
260
|
+
position: "fixed",
|
|
261
|
+
top: menuPosition.top,
|
|
262
|
+
left: menuPosition.left,
|
|
263
|
+
transform: "translateX(-100%)",
|
|
264
|
+
}}
|
|
265
|
+
>
|
|
266
|
+
{overflowTabs.map((tab) => {
|
|
267
|
+
const isActive = activeTabId === tab.id;
|
|
268
|
+
return (
|
|
269
|
+
<button
|
|
270
|
+
key={tab.id}
|
|
271
|
+
type="button"
|
|
272
|
+
role="menuitem"
|
|
273
|
+
className={`mdt-tabs-overflow-item${isActive ? " mdt-tabs-overflow-item--active" : ""}`}
|
|
274
|
+
onClick={() => {
|
|
275
|
+
onTabClick(tab);
|
|
276
|
+
setOverflowOpen(false);
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
<span className="mdt-tabs-overflow-label">{tab.label}</span>
|
|
280
|
+
{showCounts && tab.count !== undefined && tab.count !== null && (
|
|
281
|
+
<span className="mdt-tabs-overflow-count">({tab.count})</span>
|
|
282
|
+
)}
|
|
283
|
+
</button>
|
|
284
|
+
);
|
|
285
|
+
})}
|
|
286
|
+
</div>
|
|
287
|
+
) : null;
|
|
288
|
+
|
|
289
|
+
if (tabs.length === 0) return null;
|
|
290
|
+
|
|
291
|
+
const showOverflow = overflowTabs.length > 0;
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div
|
|
295
|
+
className={[
|
|
296
|
+
"mdt-tabs-row",
|
|
297
|
+
leadingSlot ? "mdt-tabs-row--with-leading" : "",
|
|
298
|
+
tabsClassName,
|
|
299
|
+
rowClassName,
|
|
300
|
+
]
|
|
301
|
+
.filter(Boolean)
|
|
302
|
+
.join(" ")}
|
|
303
|
+
ref={tabsRowRef}
|
|
304
|
+
>
|
|
305
|
+
{leadingSlot}
|
|
306
|
+
<div className="mdt-tabs-strip">
|
|
307
|
+
<div className="mdt-tabs-visible">
|
|
308
|
+
{visibleTabs.map((tab) => renderTabEntry(tab))}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
{showOverflow && (
|
|
312
|
+
<div className="mdt-tabs-overflow-wrap" ref={overflowRef}>
|
|
313
|
+
<button
|
|
314
|
+
ref={moreBtnRef}
|
|
315
|
+
type="button"
|
|
316
|
+
className={`mdt-tabs-more-btn${overflowOpen ? " mdt-tabs-more-btn--open" : ""}`}
|
|
317
|
+
aria-label={moreButtonAriaLabel}
|
|
318
|
+
aria-expanded={overflowOpen}
|
|
319
|
+
onClick={() => {
|
|
320
|
+
setOverflowOpen((v) => {
|
|
321
|
+
const next = !v;
|
|
322
|
+
if (next) requestAnimationFrame(updateMenuPosition);
|
|
323
|
+
return next;
|
|
324
|
+
});
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
<MoreHorizontal size={16} />
|
|
328
|
+
</button>
|
|
329
|
+
{overflowMenu && createPortal(overflowMenu, document.body)}
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
|
|
333
|
+
{/* Hidden more button for width measurement before overflow renders */}
|
|
334
|
+
{!showOverflow && tabs.length > 1 && (
|
|
335
|
+
<button
|
|
336
|
+
ref={moreBtnRef}
|
|
337
|
+
type="button"
|
|
338
|
+
className="mdt-tabs-more-btn mdt-tabs-more-btn--measure"
|
|
339
|
+
tabIndex={-1}
|
|
340
|
+
aria-hidden="true"
|
|
341
|
+
>
|
|
342
|
+
<MoreHorizontal size={16} />
|
|
343
|
+
</button>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div className="mdt-tabs-measure" aria-hidden="true" ref={tabsMeasureRef}>
|
|
348
|
+
{tabs.map((tab) => renderTabEntry(tab, { forMeasure: true }))}
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export default MdtTabsRow;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import MdtTabsRow from "./MdtTabsRow";
|
|
3
|
+
import { resolveTabsConfig } from "./mdtFilterUtils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configurable tabs row wrapper (status tabs + saved views).
|
|
7
|
+
*/
|
|
8
|
+
const MdtTabsSection = ({
|
|
9
|
+
tabs = [],
|
|
10
|
+
activeTabId,
|
|
11
|
+
onTabClick,
|
|
12
|
+
onEditSavedView,
|
|
13
|
+
tabsConfig: tabsConfigProp,
|
|
14
|
+
show = true,
|
|
15
|
+
leadingSlot = null,
|
|
16
|
+
rowClassName = "",
|
|
17
|
+
}) => {
|
|
18
|
+
const config = resolveTabsConfig(tabsConfigProp);
|
|
19
|
+
|
|
20
|
+
if (!show || tabs.length === 0) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<MdtTabsRow
|
|
24
|
+
tabs={tabs}
|
|
25
|
+
activeTabId={activeTabId}
|
|
26
|
+
onTabClick={onTabClick}
|
|
27
|
+
onEditSavedView={onEditSavedView}
|
|
28
|
+
tabsConfig={config}
|
|
29
|
+
leadingSlot={leadingSlot}
|
|
30
|
+
rowClassName={rowClassName}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default MdtTabsSection;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { useRef, useState, useEffect, useCallback } from "react";
|
|
2
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
3
|
+
|
|
4
|
+
const TOOLTIP_SLOT_PROPS = {
|
|
5
|
+
tooltip: {
|
|
6
|
+
sx: {
|
|
7
|
+
bgcolor: "#ffffff",
|
|
8
|
+
color: "#18181b",
|
|
9
|
+
border: "1px solid #e4e4e7",
|
|
10
|
+
borderRadius: 0,
|
|
11
|
+
boxShadow:
|
|
12
|
+
"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
|
13
|
+
padding: "6px 10px",
|
|
14
|
+
maxWidth: 420,
|
|
15
|
+
fontSize: "12px",
|
|
16
|
+
lineHeight: 1.4,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Single-line ellipsis; shows tooltip only when content overflows.
|
|
23
|
+
*/
|
|
24
|
+
const MdtTruncateWithTooltip = ({
|
|
25
|
+
children,
|
|
26
|
+
title,
|
|
27
|
+
className = "",
|
|
28
|
+
as: Component = "span",
|
|
29
|
+
...rest
|
|
30
|
+
}) => {
|
|
31
|
+
const ref = useRef(null);
|
|
32
|
+
const [overflow, setOverflow] = useState(false);
|
|
33
|
+
|
|
34
|
+
const tooltipTitle =
|
|
35
|
+
title != null && String(title).trim() !== ""
|
|
36
|
+
? String(title)
|
|
37
|
+
: typeof children === "string" || typeof children === "number"
|
|
38
|
+
? String(children)
|
|
39
|
+
: "";
|
|
40
|
+
|
|
41
|
+
const measure = useCallback(() => {
|
|
42
|
+
const el = ref.current;
|
|
43
|
+
if (!el) return;
|
|
44
|
+
setOverflow(el.scrollWidth > el.clientWidth + 1);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
measure();
|
|
49
|
+
const el = ref.current;
|
|
50
|
+
if (!el || typeof ResizeObserver === "undefined") return undefined;
|
|
51
|
+
const ro = new ResizeObserver(measure);
|
|
52
|
+
ro.observe(el);
|
|
53
|
+
return () => ro.disconnect();
|
|
54
|
+
}, [children, title, measure]);
|
|
55
|
+
|
|
56
|
+
const inner = (
|
|
57
|
+
<Component
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={["mdt-cell-truncate", className].filter(Boolean).join(" ")}
|
|
60
|
+
{...rest}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</Component>
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (!overflow || !tooltipTitle) {
|
|
67
|
+
return <span className="mdt-cell-truncate-host">{inner}</span>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Tooltip
|
|
72
|
+
title={tooltipTitle}
|
|
73
|
+
arrow
|
|
74
|
+
placement="top"
|
|
75
|
+
slotProps={TOOLTIP_SLOT_PROPS}
|
|
76
|
+
>
|
|
77
|
+
<span className="mdt-cell-truncate-host">{inner}</span>
|
|
78
|
+
</Tooltip>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default MdtTruncateWithTooltip;
|