@bento-core/tab 1.0.0 → 1.0.1-ccdiintegrated.1
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/Tabs.js +268 -18
- package/dist/assets/icons/more-vertical.svg +17 -0
- package/package.json +16 -8
- package/src/Tabs.js +289 -28
- package/src/assets/icons/more-vertical.svg +17 -0
package/dist/Tabs.js
CHANGED
|
@@ -4,58 +4,308 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
-
var _react =
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
8
|
var _core = require("@material-ui/core");
|
|
9
|
+
var _toolTip = _interopRequireDefault(require("@bento-core/tool-tip"));
|
|
9
10
|
var _defaultTheme = require("./defaultTheme");
|
|
11
|
+
var _moreVertical = _interopRequireDefault(require("./assets/icons/more-vertical.svg"));
|
|
10
12
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
13
|
+
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
|
14
|
+
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
15
|
+
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
11
16
|
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
12
17
|
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
13
18
|
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
14
19
|
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
|
|
15
20
|
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
|
|
21
|
+
// Calculate default window width for SSR (ensures max tabs)
|
|
22
|
+
const getDefaultWindowWidth = responsiveBreakpoints => {
|
|
23
|
+
if (!responsiveBreakpoints || !Array.isArray(responsiveBreakpoints.breakpoints) || responsiveBreakpoints.breakpoints.length === 0) {
|
|
24
|
+
return 1800; // Fallback if no config provided
|
|
25
|
+
}
|
|
26
|
+
// Use width above the highest breakpoint to ensure default tab limit
|
|
27
|
+
const {
|
|
28
|
+
breakpoints
|
|
29
|
+
} = responsiveBreakpoints;
|
|
30
|
+
const highestBreakpoint = breakpoints[breakpoints.length - 1];
|
|
31
|
+
return highestBreakpoint.maxWidth + 100;
|
|
32
|
+
};
|
|
16
33
|
const TabItems = _ref => {
|
|
17
34
|
let {
|
|
18
35
|
tabItems,
|
|
19
36
|
handleTabChange,
|
|
20
37
|
currentTab,
|
|
21
38
|
orientation,
|
|
22
|
-
customTheme = {}
|
|
39
|
+
customTheme = {},
|
|
40
|
+
maxVisibleTabs = 6,
|
|
41
|
+
enableGrouping = false,
|
|
42
|
+
responsiveBreakpoints = null
|
|
23
43
|
} = _ref;
|
|
24
|
-
const
|
|
44
|
+
const [currentGroup, setCurrentGroup] = (0, _react.useState)(0);
|
|
45
|
+
const [showMorePopup, setShowMorePopup] = (0, _react.useState)(false);
|
|
46
|
+
const [moreButtonAnchor, setMoreButtonAnchor] = (0, _react.useState)(null);
|
|
47
|
+
const [containerWidth, setContainerWidth] = (0, _react.useState)(getDefaultWindowWidth(responsiveBreakpoints));
|
|
48
|
+
const containerRef = (0, _react.useRef)(null);
|
|
49
|
+
|
|
50
|
+
// Calculate tab limit based on screen width breakpoints
|
|
51
|
+
// We are now using the div container width instead of window width
|
|
52
|
+
// This is to support the facet kickout feature so that the tabs respond
|
|
53
|
+
// to the available space in the container div
|
|
54
|
+
// These breakpoints are calculated by multiplying the width of each tab
|
|
55
|
+
// including the padding/margin (203px)
|
|
56
|
+
// and counting the more button as a tab (203px)
|
|
57
|
+
// We will have enough space for tabs + more button + empty tab space
|
|
58
|
+
// e.g. 2 tabs: (203 * 2) + 203 + 203 = 812px
|
|
59
|
+
const getTabLimitByWidth = width => {
|
|
60
|
+
if (!responsiveBreakpoints) {
|
|
61
|
+
// Fallback to original hardcoded values if no config provided
|
|
62
|
+
if (width < 812) return 2;
|
|
63
|
+
if (width < 1015) return 3;
|
|
64
|
+
if (width < 1281) return 4;
|
|
65
|
+
if (width < 1421) return 5;
|
|
66
|
+
return 6; // >= 1700px
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Use configuration-based breakpoints
|
|
70
|
+
for (let i = 0; i < responsiveBreakpoints.breakpoints.length; i += 1) {
|
|
71
|
+
const {
|
|
72
|
+
maxWidth,
|
|
73
|
+
tabLimit
|
|
74
|
+
} = responsiveBreakpoints.breakpoints[i];
|
|
75
|
+
if (width <= maxWidth) {
|
|
76
|
+
return tabLimit;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return responsiveBreakpoints.defaultTabLimit;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Grouping logic with responsive breakpoints
|
|
83
|
+
const tabLimit = enableGrouping ? getTabLimitByWidth(containerWidth) : maxVisibleTabs;
|
|
84
|
+
const shouldShowMoreButton = enableGrouping && tabItems.length > tabLimit;
|
|
85
|
+
|
|
86
|
+
// ResizeObserver to monitor container div width for responsive breakpoints
|
|
87
|
+
(0, _react.useEffect)(() => {
|
|
88
|
+
if (!enableGrouping || !containerRef.current) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const resizeObserver = new ResizeObserver(entries => {
|
|
92
|
+
if (entries.length > 0) {
|
|
93
|
+
const newWidth = entries[0].contentRect.width;
|
|
94
|
+
setContainerWidth(newWidth);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
resizeObserver.observe(containerRef.current);
|
|
98
|
+
|
|
99
|
+
// Set initial width
|
|
100
|
+
setContainerWidth(containerRef.current.offsetWidth);
|
|
101
|
+
return () => {
|
|
102
|
+
resizeObserver.disconnect();
|
|
103
|
+
};
|
|
104
|
+
}, [enableGrouping]);
|
|
105
|
+
|
|
106
|
+
// Consolidated effect: handle tab limit changes and active tab group recalculation
|
|
107
|
+
(0, _react.useEffect)(() => {
|
|
108
|
+
if (!enableGrouping) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const newActiveTabGroup = Math.floor(currentTab / tabLimit);
|
|
112
|
+
if (newActiveTabGroup !== currentGroup) {
|
|
113
|
+
setCurrentGroup(newActiveTabGroup);
|
|
114
|
+
}
|
|
115
|
+
}, [tabLimit, currentTab, currentGroup, enableGrouping]);
|
|
116
|
+
|
|
117
|
+
// Get visible tabs for current group
|
|
118
|
+
const getVisibleTabs = () => {
|
|
119
|
+
if (!enableGrouping) {
|
|
120
|
+
return tabItems;
|
|
121
|
+
}
|
|
122
|
+
const startIndex = currentGroup * tabLimit;
|
|
123
|
+
const endIndex = Math.min(startIndex + tabLimit, tabItems.length);
|
|
124
|
+
return tabItems.slice(startIndex, endIndex);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Get popup tabs with wrap-around logic (memoized for performance)
|
|
128
|
+
const popupTabs = (0, _react.useMemo)(() => {
|
|
129
|
+
if (!enableGrouping || !shouldShowMoreButton) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const visibleStart = currentGroup * tabLimit;
|
|
133
|
+
const visibleEnd = Math.min(visibleStart + tabLimit, tabItems.length);
|
|
134
|
+
|
|
135
|
+
// Create hidden tabs with their original indices to avoid O(n²) findIndex
|
|
136
|
+
const hiddenTabsWithIndex = [];
|
|
137
|
+
|
|
138
|
+
// Add tabs after visible range
|
|
139
|
+
for (let i = visibleEnd; i < tabItems.length; i += 1) {
|
|
140
|
+
const tab = tabItems[i];
|
|
141
|
+
if (tab) {
|
|
142
|
+
hiddenTabsWithIndex.push({
|
|
143
|
+
tab,
|
|
144
|
+
originalIndex: i
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add tabs before visible range (wrap-around)
|
|
150
|
+
for (let i = 0; i < visibleStart; i += 1) {
|
|
151
|
+
const tab = tabItems[i];
|
|
152
|
+
if (tab) {
|
|
153
|
+
hiddenTabsWithIndex.push({
|
|
154
|
+
tab,
|
|
155
|
+
originalIndex: i
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return hiddenTabsWithIndex;
|
|
160
|
+
}, [enableGrouping, shouldShowMoreButton, currentGroup, tabLimit, tabItems]);
|
|
161
|
+
const handleMoreButtonClick = event => {
|
|
162
|
+
setMoreButtonAnchor(event.currentTarget);
|
|
163
|
+
setShowMorePopup(true);
|
|
164
|
+
};
|
|
165
|
+
const handlePopupClose = () => {
|
|
166
|
+
setShowMorePopup(false);
|
|
167
|
+
setMoreButtonAnchor(null);
|
|
168
|
+
};
|
|
169
|
+
const handlePopupTabClick = tabIndex => {
|
|
170
|
+
const newGroup = Math.floor(tabIndex / tabLimit);
|
|
171
|
+
setCurrentGroup(newGroup);
|
|
172
|
+
handleTabChange(null, tabIndex);
|
|
173
|
+
handlePopupClose();
|
|
174
|
+
};
|
|
175
|
+
const getTabLabel = _ref2 => {
|
|
25
176
|
let {
|
|
26
177
|
name,
|
|
27
178
|
count,
|
|
28
179
|
clsName,
|
|
29
180
|
index
|
|
30
181
|
} = _ref2;
|
|
31
|
-
return /*#__PURE__*/_react.default.createElement(
|
|
32
|
-
|
|
33
|
-
|
|
182
|
+
return /*#__PURE__*/_react.default.createElement("div", {
|
|
183
|
+
style: {
|
|
184
|
+
display: 'flex',
|
|
185
|
+
flexDirection: 'row',
|
|
186
|
+
alignItems: 'center'
|
|
187
|
+
}
|
|
188
|
+
}, /*#__PURE__*/_react.default.createElement("span", {
|
|
189
|
+
style: {
|
|
190
|
+
display: 'flex',
|
|
191
|
+
flexDirection: 'column'
|
|
192
|
+
}
|
|
193
|
+
}, name.split(' ').map((word, index2) => /*#__PURE__*/_react.default.createElement("span", {
|
|
194
|
+
key: index2
|
|
195
|
+
}, word))), count && /*#__PURE__*/_react.default.createElement("span", {
|
|
196
|
+
className: "index_".concat(index, " ").concat(clsName, "_count"),
|
|
197
|
+
style: {
|
|
198
|
+
paddingLeft: '4px'
|
|
199
|
+
}
|
|
200
|
+
}, count));
|
|
34
201
|
};
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
202
|
+
const visibleTabs = getVisibleTabs();
|
|
203
|
+
const TABs = visibleTabs.map((tab, visibleIndex) => {
|
|
204
|
+
// Calculate the actual tab index in the full tabItems array
|
|
205
|
+
const actualIndex = enableGrouping ? currentGroup * tabLimit + visibleIndex : visibleIndex;
|
|
206
|
+
return tab.hasToolTip ? /*#__PURE__*/_react.default.createElement(_toolTip.default, _extends({}, tab.tooltipStyles, {
|
|
207
|
+
title: tab.toolTipText || '.',
|
|
208
|
+
arrow: true,
|
|
209
|
+
placement: "top",
|
|
210
|
+
key: actualIndex
|
|
211
|
+
}), /*#__PURE__*/_react.default.createElement(_core.Tab, {
|
|
212
|
+
index: actualIndex,
|
|
213
|
+
label: getTabLabel(_objectSpread(_objectSpread({}, tab), {}, {
|
|
214
|
+
index: actualIndex
|
|
215
|
+
})),
|
|
216
|
+
className: tab.clsName,
|
|
217
|
+
disableRipple: true
|
|
218
|
+
})) : /*#__PURE__*/_react.default.createElement(_core.Tab, {
|
|
219
|
+
index: actualIndex,
|
|
220
|
+
label: getTabLabel(_objectSpread(_objectSpread({}, tab), {}, {
|
|
221
|
+
index: actualIndex
|
|
222
|
+
})),
|
|
223
|
+
key: actualIndex,
|
|
224
|
+
className: tab.clsName,
|
|
225
|
+
disableRipple: true
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Add More button if needed
|
|
230
|
+
if (shouldShowMoreButton) {
|
|
231
|
+
const hiddenTabsCount = popupTabs.length;
|
|
232
|
+
TABs.push( /*#__PURE__*/_react.default.createElement(_core.Button, {
|
|
233
|
+
key: "more-button",
|
|
234
|
+
onClick: handleMoreButtonClick,
|
|
235
|
+
className: "more-button"
|
|
236
|
+
}, /*#__PURE__*/_react.default.createElement("span", null, /*#__PURE__*/_react.default.createElement("img", {
|
|
237
|
+
src: _moreVertical.default,
|
|
238
|
+
alt: "More options"
|
|
239
|
+
}), "More(".concat(hiddenTabsCount, ")"))));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Adjust currentTab value for visible tabs when grouping is enabled
|
|
243
|
+
const adjustedCurrentTab = enableGrouping ? currentTab - currentGroup * tabLimit : currentTab;
|
|
44
244
|
const themeConfig = (0, _core.createTheme)({
|
|
45
245
|
overrides: _objectSpread(_objectSpread({}, (0, _defaultTheme.defaultTheme)()), customTheme)
|
|
46
246
|
});
|
|
47
247
|
return /*#__PURE__*/_react.default.createElement(_core.ThemeProvider, {
|
|
48
248
|
theme: themeConfig
|
|
249
|
+
}, /*#__PURE__*/_react.default.createElement("div", {
|
|
250
|
+
ref: containerRef,
|
|
251
|
+
style: {
|
|
252
|
+
position: 'relative'
|
|
253
|
+
}
|
|
49
254
|
}, /*#__PURE__*/_react.default.createElement(_core.Tabs, {
|
|
50
|
-
onChange: (event, value) =>
|
|
51
|
-
|
|
255
|
+
onChange: (event, value) => {
|
|
256
|
+
// Convert relative position to actual tab index when grouping is enabled
|
|
257
|
+
const actualTabIndex = enableGrouping ? currentGroup * tabLimit + value : value;
|
|
258
|
+
handleTabChange(event, actualTabIndex);
|
|
259
|
+
},
|
|
260
|
+
value: adjustedCurrentTab,
|
|
52
261
|
TabIndicatorProps: {
|
|
53
262
|
style: {
|
|
54
263
|
background: 'none'
|
|
55
264
|
}
|
|
56
265
|
},
|
|
57
266
|
orientation: orientation
|
|
58
|
-
}, TABs)
|
|
267
|
+
}, TABs), shouldShowMoreButton && /*#__PURE__*/_react.default.createElement(_core.Popover, {
|
|
268
|
+
open: showMorePopup,
|
|
269
|
+
anchorEl: moreButtonAnchor,
|
|
270
|
+
onClose: handlePopupClose,
|
|
271
|
+
anchorOrigin: {
|
|
272
|
+
vertical: 'bottom',
|
|
273
|
+
horizontal: 'center'
|
|
274
|
+
},
|
|
275
|
+
transformOrigin: {
|
|
276
|
+
vertical: 'top',
|
|
277
|
+
horizontal: 'center'
|
|
278
|
+
},
|
|
279
|
+
style: {
|
|
280
|
+
marginTop: '0px'
|
|
281
|
+
},
|
|
282
|
+
PaperProps: {
|
|
283
|
+
style: {
|
|
284
|
+
border: '1.5px solid rgb(86, 102, 189)',
|
|
285
|
+
borderRadius: '5px'
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}, /*#__PURE__*/_react.default.createElement(_core.List, {
|
|
289
|
+
className: "popover-list"
|
|
290
|
+
}, popupTabs.map(_ref3 => {
|
|
291
|
+
let {
|
|
292
|
+
tab,
|
|
293
|
+
originalIndex
|
|
294
|
+
} = _ref3;
|
|
295
|
+
if (!tab || !tab.name) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
return /*#__PURE__*/_react.default.createElement(_core.ListItem, {
|
|
299
|
+
key: originalIndex,
|
|
300
|
+
button: true,
|
|
301
|
+
onClick: () => handlePopupTabClick(originalIndex),
|
|
302
|
+
className: "popover-list-item"
|
|
303
|
+
}, /*#__PURE__*/_react.default.createElement("span", {
|
|
304
|
+
className: "popover-tab-name"
|
|
305
|
+
}, tab.name), /*#__PURE__*/_react.default.createElement("span", {
|
|
306
|
+
className: "popover-tab-count"
|
|
307
|
+
}, tab.count || ''));
|
|
308
|
+
})))));
|
|
59
309
|
};
|
|
60
310
|
var _default = TabItems;
|
|
61
311
|
exports.default = _default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
3
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
4
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
5
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
6
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
7
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
8
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
9
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
10
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
11
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
12
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
13
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
14
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
15
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
16
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
17
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bento-core/tab",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1-ccdiintegrated.1",
|
|
4
4
|
"description": "",
|
|
5
|
+
"homepage": "https://github.com/CBIIT/bento-frontend#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/CBIIT/bento-frontend/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/CBIIT/bento-frontend.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "CTOS Bento Team",
|
|
15
|
+
"type": "commonjs",
|
|
5
16
|
"main": "dist/index.js",
|
|
6
17
|
"scripts": {
|
|
7
18
|
"build": "npm run lint && cross-env-shell rm -rf dist && NODE_ENV=production BABEL_ENV=es babel src --out-dir dist --copy-files",
|
|
8
19
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
9
20
|
"lint": "eslint src"
|
|
10
21
|
},
|
|
11
|
-
"repository": "https://github.com/CBIIT/bento-frontend",
|
|
12
|
-
"publishConfig": {
|
|
13
|
-
"access": "public"
|
|
14
|
-
},
|
|
15
22
|
"peerDependencies": {
|
|
16
23
|
"@material-ui/core": "^4.12.4",
|
|
24
|
+
"@bento-core/tool-tip": "1.0.1-ccdiintegrated.1",
|
|
17
25
|
"react": "^17.0.2",
|
|
18
26
|
"react-dom": "^17.0.0",
|
|
19
27
|
"react-redux": "^7.2.1"
|
|
20
28
|
},
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
24
32
|
}
|
package/src/Tabs.js
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React, {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
} from 'react';
|
|
2
7
|
import {
|
|
3
8
|
Tab,
|
|
4
9
|
Tabs,
|
|
5
10
|
createTheme,
|
|
6
11
|
ThemeProvider,
|
|
12
|
+
Button,
|
|
13
|
+
Popover,
|
|
14
|
+
List,
|
|
15
|
+
ListItem,
|
|
7
16
|
} from '@material-ui/core';
|
|
17
|
+
import ToolTip from '@bento-core/tool-tip';
|
|
8
18
|
import { defaultTheme } from './defaultTheme';
|
|
19
|
+
import MoreVerticalIcon from './assets/icons/more-vertical.svg';
|
|
20
|
+
|
|
21
|
+
// Calculate default window width for SSR (ensures max tabs)
|
|
22
|
+
const getDefaultWindowWidth = (responsiveBreakpoints) => {
|
|
23
|
+
if (
|
|
24
|
+
!responsiveBreakpoints
|
|
25
|
+
|| !Array.isArray(responsiveBreakpoints.breakpoints)
|
|
26
|
+
|| responsiveBreakpoints.breakpoints.length === 0
|
|
27
|
+
) {
|
|
28
|
+
return 1800; // Fallback if no config provided
|
|
29
|
+
}
|
|
30
|
+
// Use width above the highest breakpoint to ensure default tab limit
|
|
31
|
+
const { breakpoints } = responsiveBreakpoints;
|
|
32
|
+
const highestBreakpoint = breakpoints[breakpoints.length - 1];
|
|
33
|
+
return highestBreakpoint.maxWidth + 100;
|
|
34
|
+
};
|
|
9
35
|
|
|
10
36
|
const TabItems = ({
|
|
11
37
|
tabItems,
|
|
@@ -13,47 +39,282 @@ const TabItems = ({
|
|
|
13
39
|
currentTab,
|
|
14
40
|
orientation,
|
|
15
41
|
customTheme = {},
|
|
42
|
+
maxVisibleTabs = 6,
|
|
43
|
+
enableGrouping = false,
|
|
44
|
+
responsiveBreakpoints = null,
|
|
16
45
|
}) => {
|
|
17
|
-
const
|
|
46
|
+
const [currentGroup, setCurrentGroup] = useState(0);
|
|
47
|
+
const [showMorePopup, setShowMorePopup] = useState(false);
|
|
48
|
+
const [moreButtonAnchor, setMoreButtonAnchor] = useState(null);
|
|
49
|
+
const [containerWidth, setContainerWidth] = useState(
|
|
50
|
+
getDefaultWindowWidth(responsiveBreakpoints),
|
|
51
|
+
);
|
|
52
|
+
const containerRef = useRef(null);
|
|
53
|
+
|
|
54
|
+
// Calculate tab limit based on screen width breakpoints
|
|
55
|
+
// We are now using the div container width instead of window width
|
|
56
|
+
// This is to support the facet kickout feature so that the tabs respond
|
|
57
|
+
// to the available space in the container div
|
|
58
|
+
// These breakpoints are calculated by multiplying the width of each tab
|
|
59
|
+
// including the padding/margin (203px)
|
|
60
|
+
// and counting the more button as a tab (203px)
|
|
61
|
+
// We will have enough space for tabs + more button + empty tab space
|
|
62
|
+
// e.g. 2 tabs: (203 * 2) + 203 + 203 = 812px
|
|
63
|
+
const getTabLimitByWidth = (width) => {
|
|
64
|
+
if (!responsiveBreakpoints) {
|
|
65
|
+
// Fallback to original hardcoded values if no config provided
|
|
66
|
+
if (width < 812) return 2;
|
|
67
|
+
if (width < 1015) return 3;
|
|
68
|
+
if (width < 1281) return 4;
|
|
69
|
+
if (width < 1421) return 5;
|
|
70
|
+
return 6; // >= 1700px
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Use configuration-based breakpoints
|
|
74
|
+
for (let i = 0; i < responsiveBreakpoints.breakpoints.length; i += 1) {
|
|
75
|
+
const { maxWidth, tabLimit } = responsiveBreakpoints.breakpoints[i];
|
|
76
|
+
if (width <= maxWidth) {
|
|
77
|
+
return tabLimit;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return responsiveBreakpoints.defaultTabLimit;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Grouping logic with responsive breakpoints
|
|
84
|
+
const tabLimit = enableGrouping ? getTabLimitByWidth(containerWidth) : maxVisibleTabs;
|
|
85
|
+
const shouldShowMoreButton = enableGrouping && tabItems.length > tabLimit;
|
|
86
|
+
|
|
87
|
+
// ResizeObserver to monitor container div width for responsive breakpoints
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!enableGrouping || !containerRef.current) {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
94
|
+
if (entries.length > 0) {
|
|
95
|
+
const newWidth = entries[0].contentRect.width;
|
|
96
|
+
setContainerWidth(newWidth);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
resizeObserver.observe(containerRef.current);
|
|
101
|
+
|
|
102
|
+
// Set initial width
|
|
103
|
+
setContainerWidth(containerRef.current.offsetWidth);
|
|
104
|
+
|
|
105
|
+
return () => {
|
|
106
|
+
resizeObserver.disconnect();
|
|
107
|
+
};
|
|
108
|
+
}, [enableGrouping]);
|
|
109
|
+
|
|
110
|
+
// Consolidated effect: handle tab limit changes and active tab group recalculation
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!enableGrouping) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const newActiveTabGroup = Math.floor(currentTab / tabLimit);
|
|
117
|
+
if (newActiveTabGroup !== currentGroup) {
|
|
118
|
+
setCurrentGroup(newActiveTabGroup);
|
|
119
|
+
}
|
|
120
|
+
}, [tabLimit, currentTab, currentGroup, enableGrouping]);
|
|
121
|
+
|
|
122
|
+
// Get visible tabs for current group
|
|
123
|
+
const getVisibleTabs = () => {
|
|
124
|
+
if (!enableGrouping) {
|
|
125
|
+
return tabItems;
|
|
126
|
+
}
|
|
127
|
+
const startIndex = currentGroup * tabLimit;
|
|
128
|
+
const endIndex = Math.min(startIndex + tabLimit, tabItems.length);
|
|
129
|
+
return tabItems.slice(startIndex, endIndex);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Get popup tabs with wrap-around logic (memoized for performance)
|
|
133
|
+
const popupTabs = useMemo(() => {
|
|
134
|
+
if (!enableGrouping || !shouldShowMoreButton) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const visibleStart = currentGroup * tabLimit;
|
|
139
|
+
const visibleEnd = Math.min(visibleStart + tabLimit, tabItems.length);
|
|
140
|
+
|
|
141
|
+
// Create hidden tabs with their original indices to avoid O(n²) findIndex
|
|
142
|
+
const hiddenTabsWithIndex = [];
|
|
143
|
+
|
|
144
|
+
// Add tabs after visible range
|
|
145
|
+
for (let i = visibleEnd; i < tabItems.length; i += 1) {
|
|
146
|
+
const tab = tabItems[i];
|
|
147
|
+
if (tab) {
|
|
148
|
+
hiddenTabsWithIndex.push({ tab, originalIndex: i });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add tabs before visible range (wrap-around)
|
|
153
|
+
for (let i = 0; i < visibleStart; i += 1) {
|
|
154
|
+
const tab = tabItems[i];
|
|
155
|
+
if (tab) {
|
|
156
|
+
hiddenTabsWithIndex.push({ tab, originalIndex: i });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return hiddenTabsWithIndex;
|
|
161
|
+
}, [enableGrouping, shouldShowMoreButton, currentGroup, tabLimit, tabItems]);
|
|
162
|
+
|
|
163
|
+
const handleMoreButtonClick = (event) => {
|
|
164
|
+
setMoreButtonAnchor(event.currentTarget);
|
|
165
|
+
setShowMorePopup(true);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const handlePopupClose = () => {
|
|
169
|
+
setShowMorePopup(false);
|
|
170
|
+
setMoreButtonAnchor(null);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handlePopupTabClick = (tabIndex) => {
|
|
174
|
+
const newGroup = Math.floor(tabIndex / tabLimit);
|
|
175
|
+
setCurrentGroup(newGroup);
|
|
176
|
+
handleTabChange(null, tabIndex);
|
|
177
|
+
handlePopupClose();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const getTabLabel = ({
|
|
18
181
|
name, count, clsName, index,
|
|
19
182
|
}) => (
|
|
20
|
-
|
|
21
|
-
<span>
|
|
22
|
-
{name
|
|
23
|
-
|
|
183
|
+
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
|
184
|
+
<span style={{ display: 'flex', flexDirection: 'column' }}>
|
|
185
|
+
{name.split(' ').map((word, index2) => (
|
|
186
|
+
<span key={index2}>{word}</span>
|
|
187
|
+
))}
|
|
188
|
+
</span>
|
|
189
|
+
{count && (
|
|
24
190
|
<span
|
|
25
191
|
className={`index_${index} ${clsName}_count`}
|
|
192
|
+
style={{ paddingLeft: '4px' }}
|
|
26
193
|
>
|
|
27
194
|
{count}
|
|
28
195
|
</span>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
</>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
32
198
|
);
|
|
33
199
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
200
|
+
const visibleTabs = getVisibleTabs();
|
|
201
|
+
|
|
202
|
+
const TABs = visibleTabs.map((tab, visibleIndex) => {
|
|
203
|
+
// Calculate the actual tab index in the full tabItems array
|
|
204
|
+
const actualIndex = enableGrouping
|
|
205
|
+
? (currentGroup * tabLimit) + visibleIndex
|
|
206
|
+
: visibleIndex;
|
|
207
|
+
|
|
208
|
+
return tab.hasToolTip
|
|
209
|
+
? (
|
|
210
|
+
<ToolTip {...tab.tooltipStyles} title={tab.toolTipText || '.'} arrow placement="top" key={actualIndex}>
|
|
211
|
+
<Tab
|
|
212
|
+
index={actualIndex}
|
|
213
|
+
label={getTabLabel({ ...tab, index: actualIndex })}
|
|
214
|
+
className={tab.clsName}
|
|
215
|
+
disableRipple
|
|
216
|
+
/>
|
|
217
|
+
</ToolTip>
|
|
218
|
+
)
|
|
219
|
+
: (
|
|
220
|
+
<Tab
|
|
221
|
+
index={actualIndex}
|
|
222
|
+
label={getTabLabel({ ...tab, index: actualIndex })}
|
|
223
|
+
key={actualIndex}
|
|
224
|
+
className={tab.clsName}
|
|
225
|
+
disableRipple
|
|
226
|
+
/>
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Add More button if needed
|
|
231
|
+
if (shouldShowMoreButton) {
|
|
232
|
+
const hiddenTabsCount = popupTabs.length;
|
|
233
|
+
TABs.push(
|
|
234
|
+
<Button
|
|
235
|
+
key="more-button"
|
|
236
|
+
onClick={handleMoreButtonClick}
|
|
237
|
+
className="more-button"
|
|
238
|
+
>
|
|
239
|
+
<span>
|
|
240
|
+
<img src={MoreVerticalIcon} alt="More options" />
|
|
241
|
+
{`More(${hiddenTabsCount})`}
|
|
242
|
+
</span>
|
|
243
|
+
</Button>,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Adjust currentTab value for visible tabs when grouping is enabled
|
|
248
|
+
const adjustedCurrentTab = enableGrouping
|
|
249
|
+
? currentTab - (currentGroup * tabLimit)
|
|
250
|
+
: currentTab;
|
|
45
251
|
|
|
46
252
|
const themeConfig = createTheme({ overrides: { ...defaultTheme(), ...customTheme } });
|
|
47
253
|
return (
|
|
48
254
|
<ThemeProvider theme={themeConfig}>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
255
|
+
<div ref={containerRef} style={{ position: 'relative' }}>
|
|
256
|
+
<Tabs
|
|
257
|
+
onChange={(event, value) => {
|
|
258
|
+
// Convert relative position to actual tab index when grouping is enabled
|
|
259
|
+
const actualTabIndex = enableGrouping
|
|
260
|
+
? (currentGroup * tabLimit) + value
|
|
261
|
+
: value;
|
|
262
|
+
handleTabChange(event, actualTabIndex);
|
|
263
|
+
}}
|
|
264
|
+
value={adjustedCurrentTab}
|
|
265
|
+
TabIndicatorProps={{ style: { background: 'none' } }}
|
|
266
|
+
orientation={orientation}
|
|
267
|
+
>
|
|
268
|
+
{TABs}
|
|
269
|
+
</Tabs>
|
|
270
|
+
|
|
271
|
+
{/* More button popup */}
|
|
272
|
+
{shouldShowMoreButton && (
|
|
273
|
+
<Popover
|
|
274
|
+
open={showMorePopup}
|
|
275
|
+
anchorEl={moreButtonAnchor}
|
|
276
|
+
onClose={handlePopupClose}
|
|
277
|
+
anchorOrigin={{
|
|
278
|
+
vertical: 'bottom',
|
|
279
|
+
horizontal: 'center',
|
|
280
|
+
}}
|
|
281
|
+
transformOrigin={{
|
|
282
|
+
vertical: 'top',
|
|
283
|
+
horizontal: 'center',
|
|
284
|
+
}}
|
|
285
|
+
style={{ marginTop: '0px' }}
|
|
286
|
+
PaperProps={{
|
|
287
|
+
style: {
|
|
288
|
+
border: '1.5px solid rgb(86, 102, 189)',
|
|
289
|
+
borderRadius: '5px',
|
|
290
|
+
},
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
<List className="popover-list">
|
|
294
|
+
{popupTabs.map(({ tab, originalIndex }) => {
|
|
295
|
+
if (!tab || !tab.name) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
return (
|
|
299
|
+
<ListItem
|
|
300
|
+
key={originalIndex}
|
|
301
|
+
button
|
|
302
|
+
onClick={() => handlePopupTabClick(originalIndex)}
|
|
303
|
+
className="popover-list-item"
|
|
304
|
+
>
|
|
305
|
+
<span className="popover-tab-name">
|
|
306
|
+
{tab.name}
|
|
307
|
+
</span>
|
|
308
|
+
<span className="popover-tab-count">
|
|
309
|
+
{tab.count || ''}
|
|
310
|
+
</span>
|
|
311
|
+
</ListItem>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
314
|
+
</List>
|
|
315
|
+
</Popover>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
57
318
|
</ThemeProvider>
|
|
58
319
|
);
|
|
59
320
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
3
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
4
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
5
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
6
|
+
<circle cx="1.8811" cy="2.4519" r="1.84937" fill="black"/>
|
|
7
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
8
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
9
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
10
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
11
|
+
<circle cx="1.8811" cy="7.99976" r="1.84937" fill="black"/>
|
|
12
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
13
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
14
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
15
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
16
|
+
<circle cx="1.8811" cy="13.5481" r="1.84937" fill="black"/>
|
|
17
|
+
</svg>
|