@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 CHANGED
@@ -4,58 +4,308 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _react = _interopRequireDefault(require("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 getTabLalbel = _ref2 => {
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(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("span", null, name, count && /*#__PURE__*/_react.default.createElement("span", {
32
- className: "index_".concat(index, " ").concat(clsName, "_count")
33
- }, count)));
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 TABs = tabItems.map((tab, index) => /*#__PURE__*/_react.default.createElement(_core.Tab, {
36
- index: index,
37
- label: getTabLalbel(_objectSpread(_objectSpread({}, tab), {}, {
38
- index
39
- })),
40
- key: index,
41
- className: tab.clsName,
42
- disableRipple: true
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) => handleTabChange(event, value),
51
- value: currentTab,
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.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
- "author": "CTOS Bento Team",
22
- "license": "ISC",
23
- "gitHead": "4493c4a5fa666211345fea022ec06841a323d446"
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
24
32
  }
package/src/Tabs.js CHANGED
@@ -1,11 +1,37 @@
1
- import React from '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 getTabLalbel = ({
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
- {count && (
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
- </span>
31
- </>
196
+ )}
197
+ </div>
32
198
  );
33
199
 
34
- const TABs = tabItems.map((tab, index) => (
35
- <Tab
36
- index={index}
37
- label={
38
- getTabLalbel({ ...tab, index })
39
- }
40
- key={index}
41
- className={tab.clsName}
42
- disableRipple
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
- <Tabs
50
- onChange={(event, value) => handleTabChange(event, value)}
51
- value={currentTab}
52
- TabIndicatorProps={{ style: { background: 'none' } }}
53
- orientation={orientation}
54
- >
55
- {TABs}
56
- </Tabs>
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>