@variocube/app-ui 1.16.2 → 1.16.12
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/esm/tabs/Tabs.js +93 -31
- package/esm/tabs/Tabs.js.map +1 -1
- package/package.json +5 -1
- package/src/tabs/Tabs.tsx +106 -38
package/esm/tabs/Tabs.js
CHANGED
|
@@ -13,7 +13,8 @@ import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
|
|
13
13
|
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Stack } from "@mui/material";
|
|
14
14
|
import MuiTab from "@mui/material/Tab";
|
|
15
15
|
import MuiTabs from "@mui/material/Tabs";
|
|
16
|
-
import React, { Fragment, useEffect, useRef, useState } from "react";
|
|
16
|
+
import React, { Fragment, useCallback, useEffect, useRef, useState } from "react";
|
|
17
|
+
const OVERFLOW_BUTTON_FALLBACK_WIDTH = 48;
|
|
17
18
|
const hiddenSx = {
|
|
18
19
|
visibility: "hidden",
|
|
19
20
|
height: 0,
|
|
@@ -25,42 +26,103 @@ export function Tabs(props) {
|
|
|
25
26
|
const { orientation = "horizontal", scrollButtons = "auto", allowScrollButtonsMobile = true, sx, onChange, items: inItems, value } = props, rest = __rest(props, ["orientation", "scrollButtons", "allowScrollButtonsMobile", "sx", "onChange", "items", "value"]);
|
|
26
27
|
const items = inItems.map((item, i) => (Object.assign(Object.assign({}, item), { value: typeof item.value == "undefined" ? i : item.value })));
|
|
27
28
|
const ref = useRef(null);
|
|
29
|
+
const stackRef = useRef(null);
|
|
30
|
+
const overflowButtonRef = useRef(null);
|
|
28
31
|
const [dropdownEl, setDropdownEl] = useState(null);
|
|
29
32
|
const [overflowIndex, setOverflowIndex] = useState(items.length);
|
|
30
33
|
const selectedIndex = items.findIndex((item) => item.value == value);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
// Store measured tab widths from when all tabs are visible.
|
|
35
|
+
// Uses a ref so the ResizeObserver callback always sees the latest values.
|
|
36
|
+
const tabWidthsRef = useRef([]);
|
|
37
|
+
// Guard to prevent re-entrant measurement loops when showing all tabs to measure hidden ones
|
|
38
|
+
const measuringRef = useRef(false);
|
|
39
|
+
// Dependencies: only refs (stable across renders), so [] is correct.
|
|
40
|
+
const computeOverflow = useCallback(() => {
|
|
41
|
+
if (!ref.current)
|
|
42
|
+
return;
|
|
43
|
+
if (measuringRef.current)
|
|
44
|
+
return;
|
|
45
|
+
const root = Array.from(ref.current.children).find(e => e.classList.contains("tabRoot"));
|
|
46
|
+
if (!root)
|
|
47
|
+
return;
|
|
48
|
+
const list = Array.from(root.children).find(e => e.classList.contains("tabList"));
|
|
49
|
+
if (!list)
|
|
50
|
+
return;
|
|
51
|
+
// Measure tab widths from visible tabs and cache them.
|
|
52
|
+
// Hidden tabs (display: none) have 0 width, so we skip them and keep cached values.
|
|
53
|
+
const children = Array.from(list.children);
|
|
54
|
+
let hasAllWidths = true;
|
|
55
|
+
for (let i = 0; i < children.length; i++) {
|
|
56
|
+
const w = children[i].getBoundingClientRect().width;
|
|
57
|
+
if (w > 0) {
|
|
58
|
+
tabWidthsRef.current[i] = w;
|
|
59
|
+
}
|
|
60
|
+
else if (!tabWidthsRef.current[i]) {
|
|
61
|
+
hasAllWidths = false;
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
const widths = tabWidthsRef.current;
|
|
65
|
+
// If we don't have widths for all tabs yet (e.g. some were hidden before measurement),
|
|
66
|
+
// we can't make a correct decision. Show all tabs to allow measurement on next frame.
|
|
67
|
+
// Use measuringRef guard to prevent re-entrant loops from the ResizeObserver.
|
|
68
|
+
if (!hasAllWidths || widths.length < children.length) {
|
|
69
|
+
measuringRef.current = true;
|
|
70
|
+
setOverflowIndex(children.length);
|
|
71
|
+
requestAnimationFrame(() => {
|
|
72
|
+
measuringRef.current = false;
|
|
73
|
+
computeOverflow();
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const totalTabWidth = widths.reduce((sum, w) => sum + w, 0);
|
|
78
|
+
// Get the full available width from the parent Stack.
|
|
79
|
+
// When the overflow button is shown, the MuiTabs (flex: 1) shrinks to accommodate it.
|
|
80
|
+
// We need the Stack width to know the total available space.
|
|
81
|
+
const fullWidth = stackRef.current ? stackRef.current.clientWidth : root.clientWidth;
|
|
82
|
+
// Two-pass approach:
|
|
83
|
+
// 1. If all tabs fit in the full available width, no overflow needed
|
|
84
|
+
if (totalTabWidth <= fullWidth) {
|
|
85
|
+
setOverflowIndex(children.length);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// 2. Some tabs don't fit — reserve space for the overflow button and calculate
|
|
89
|
+
let overflowButtonWidth = OVERFLOW_BUTTON_FALLBACK_WIDTH;
|
|
90
|
+
if (overflowButtonRef.current) {
|
|
91
|
+
const margin = parseFloat(getComputedStyle(overflowButtonRef.current).marginLeft) || 0;
|
|
92
|
+
overflowButtonWidth = overflowButtonRef.current.getBoundingClientRect().width + margin;
|
|
93
|
+
}
|
|
94
|
+
const availableWidth = fullWidth - overflowButtonWidth;
|
|
95
|
+
let newOverflowIndex = 0;
|
|
96
|
+
let usedWidth = 0;
|
|
97
|
+
for (let i = 0; i < widths.length; i++) {
|
|
98
|
+
usedWidth += widths[i];
|
|
99
|
+
if (usedWidth <= availableWidth) {
|
|
100
|
+
newOverflowIndex++;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
break;
|
|
60
104
|
}
|
|
61
105
|
}
|
|
62
|
-
|
|
63
|
-
|
|
106
|
+
// Ensure at least one tab is visible
|
|
107
|
+
setOverflowIndex(Math.max(1, newOverflowIndex));
|
|
108
|
+
}, []);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!ref.current)
|
|
111
|
+
return;
|
|
112
|
+
const root = Array.from(ref.current.children).find(e => e.classList.contains("tabRoot"));
|
|
113
|
+
if (!root)
|
|
114
|
+
return;
|
|
115
|
+
const resizeObserver = new ResizeObserver(() => computeOverflow());
|
|
116
|
+
resizeObserver.observe(root);
|
|
117
|
+
return () => resizeObserver.disconnect();
|
|
118
|
+
}, [computeOverflow]);
|
|
119
|
+
// Recompute when items change (e.g., conditional tabs added/removed)
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
// Reset cached widths when item count changes so stale entries are cleared
|
|
122
|
+
tabWidthsRef.current = [];
|
|
123
|
+
computeOverflow();
|
|
124
|
+
}, [inItems.length, computeOverflow]);
|
|
125
|
+
return (React.createElement(Stack, { ref: stackRef, flex: 1, direction: "row", alignItems: "center", sx: {
|
|
64
126
|
borderBottom: 1,
|
|
65
127
|
borderColor: "divider",
|
|
66
128
|
} },
|
|
@@ -70,7 +132,7 @@ export function Tabs(props) {
|
|
|
70
132
|
}, value: value, onChange: onChange }, rest), items
|
|
71
133
|
.map((tabProps, i) => (React.createElement(MuiTab, Object.assign({ key: "muiTab-" + i, iconPosition: "start" }, tabProps, { sx: Object.assign(Object.assign(Object.assign({}, tabProps.sx), (i >= overflowIndex ? { display: "none" } : undefined)), { minHeight: 48 }) }))))),
|
|
72
134
|
(overflowIndex < items.length) && (React.createElement(Fragment, null,
|
|
73
|
-
React.createElement(IconButton, { sx: {
|
|
135
|
+
React.createElement(IconButton, { ref: overflowButtonRef, sx: {
|
|
74
136
|
ml: 1,
|
|
75
137
|
}, onClick: ev => setDropdownEl(ev.currentTarget), color: selectedIndex >= overflowIndex ? "primary" : "default" },
|
|
76
138
|
React.createElement(MoreHorizIcon, { fontSize: "inherit" })),
|
package/esm/tabs/Tabs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.js","sourceRoot":"","sources":["../../src/tabs/Tabs.tsx"],"names":[],"mappings":";;;;;;;;;;;AAAA,OAAO,aAAa,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAC,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAC,MAAM,eAAe,CAAC;AAC5F,OAAO,MAAiC,MAAM,mBAAmB,CAAC;AAClE,OAAO,OAAoC,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,EAAC,QAAQ,EAAkB,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Tabs.js","sourceRoot":"","sources":["../../src/tabs/Tabs.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@variocube/app-ui",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.12",
|
|
4
4
|
"description": "Common UI components for Variocube applications.",
|
|
5
5
|
"module": "esm/index.js",
|
|
6
6
|
"types": "esm/index.d.ts",
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"vite": "^7.1.7",
|
|
67
67
|
"vite-plugin-checker": "^0.8.0"
|
|
68
68
|
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/variocube/app-ui.git"
|
|
72
|
+
},
|
|
69
73
|
"publishConfig": {
|
|
70
74
|
"access": "public"
|
|
71
75
|
}
|
package/src/tabs/Tabs.tsx
CHANGED
|
@@ -2,12 +2,9 @@ import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
|
|
2
2
|
import {IconButton, ListItemIcon, ListItemText, Menu, MenuItem, Stack} from "@mui/material";
|
|
3
3
|
import MuiTab, {TabProps as MuiTabProps} from "@mui/material/Tab";
|
|
4
4
|
import MuiTabs, {TabsProps as MuiTabsProps} from "@mui/material/Tabs";
|
|
5
|
-
import React, {Fragment, SyntheticEvent, useEffect, useRef, useState} from "react";
|
|
5
|
+
import React, {Fragment, SyntheticEvent, useCallback, useEffect, useRef, useState} from "react";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
index: number;
|
|
9
|
-
width: number;
|
|
10
|
-
}
|
|
7
|
+
const OVERFLOW_BUTTON_FALLBACK_WIDTH = 48;
|
|
11
8
|
|
|
12
9
|
interface TabProps extends MuiTabProps<React.ElementType> {
|
|
13
10
|
}
|
|
@@ -41,53 +38,123 @@ export function Tabs(props: TabsProps) {
|
|
|
41
38
|
value: typeof item.value == "undefined" ? i : item.value,
|
|
42
39
|
} as TabProps));
|
|
43
40
|
|
|
44
|
-
const ref = useRef(null);
|
|
41
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
42
|
+
const stackRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
const overflowButtonRef = useRef<HTMLButtonElement>(null);
|
|
45
44
|
const [dropdownEl, setDropdownEl] = useState<null | HTMLElement>(null);
|
|
46
45
|
|
|
47
46
|
const [overflowIndex, setOverflowIndex] = useState<number>(items.length);
|
|
48
47
|
|
|
49
48
|
const selectedIndex = items.findIndex((item) => item.value == value);
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
// Store measured tab widths from when all tabs are visible.
|
|
51
|
+
// Uses a ref so the ResizeObserver callback always sees the latest values.
|
|
52
|
+
const tabWidthsRef = useRef<number[]>([]);
|
|
53
|
+
// Guard to prevent re-entrant measurement loops when showing all tabs to measure hidden ones
|
|
54
|
+
const measuringRef = useRef(false);
|
|
55
|
+
|
|
56
|
+
// Dependencies: only refs (stable across renders), so [] is correct.
|
|
57
|
+
const computeOverflow = useCallback(() => {
|
|
58
|
+
if (!ref.current) return;
|
|
59
|
+
if (measuringRef.current) return;
|
|
60
|
+
|
|
61
|
+
const root = Array.from(ref.current.children).find(e => e.classList.contains("tabRoot")) as
|
|
62
|
+
| HTMLElement
|
|
63
|
+
| undefined;
|
|
64
|
+
if (!root) return;
|
|
65
|
+
|
|
66
|
+
const list = Array.from(root.children).find(e => e.classList.contains("tabList")) as HTMLElement | undefined;
|
|
67
|
+
if (!list) return;
|
|
68
|
+
|
|
69
|
+
// Measure tab widths from visible tabs and cache them.
|
|
70
|
+
// Hidden tabs (display: none) have 0 width, so we skip them and keep cached values.
|
|
71
|
+
const children = Array.from(list.children) as HTMLElement[];
|
|
72
|
+
let hasAllWidths = true;
|
|
73
|
+
for (let i = 0; i < children.length; i++) {
|
|
74
|
+
const w = children[i].getBoundingClientRect().width;
|
|
75
|
+
if (w > 0) {
|
|
76
|
+
tabWidthsRef.current[i] = w;
|
|
77
|
+
}
|
|
78
|
+
else if (!tabWidthsRef.current[i]) {
|
|
79
|
+
hasAllWidths = false;
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
|
-
setOverflowIndex(overflowIndex);
|
|
68
|
-
}
|
|
69
82
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
const widths = tabWidthsRef.current;
|
|
84
|
+
|
|
85
|
+
// If we don't have widths for all tabs yet (e.g. some were hidden before measurement),
|
|
86
|
+
// we can't make a correct decision. Show all tabs to allow measurement on next frame.
|
|
87
|
+
// Use measuringRef guard to prevent re-entrant loops from the ResizeObserver.
|
|
88
|
+
if (!hasAllWidths || widths.length < children.length) {
|
|
89
|
+
measuringRef.current = true;
|
|
90
|
+
setOverflowIndex(children.length);
|
|
91
|
+
requestAnimationFrame(() => {
|
|
92
|
+
measuringRef.current = false;
|
|
93
|
+
computeOverflow();
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const totalTabWidth = widths.reduce((sum, w) => sum + w, 0);
|
|
99
|
+
|
|
100
|
+
// Get the full available width from the parent Stack.
|
|
101
|
+
// When the overflow button is shown, the MuiTabs (flex: 1) shrinks to accommodate it.
|
|
102
|
+
// We need the Stack width to know the total available space.
|
|
103
|
+
const fullWidth = stackRef.current ? stackRef.current.clientWidth : root.clientWidth;
|
|
104
|
+
|
|
105
|
+
// Two-pass approach:
|
|
106
|
+
// 1. If all tabs fit in the full available width, no overflow needed
|
|
107
|
+
if (totalTabWidth <= fullWidth) {
|
|
108
|
+
setOverflowIndex(children.length);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Some tabs don't fit — reserve space for the overflow button and calculate
|
|
113
|
+
let overflowButtonWidth = OVERFLOW_BUTTON_FALLBACK_WIDTH;
|
|
114
|
+
if (overflowButtonRef.current) {
|
|
115
|
+
const margin = parseFloat(getComputedStyle(overflowButtonRef.current).marginLeft) || 0;
|
|
116
|
+
overflowButtonWidth = overflowButtonRef.current.getBoundingClientRect().width + margin;
|
|
117
|
+
}
|
|
118
|
+
const availableWidth = fullWidth - overflowButtonWidth;
|
|
119
|
+
|
|
120
|
+
let newOverflowIndex = 0;
|
|
121
|
+
let usedWidth = 0;
|
|
122
|
+
for (let i = 0; i < widths.length; i++) {
|
|
123
|
+
usedWidth += widths[i];
|
|
124
|
+
if (usedWidth <= availableWidth) {
|
|
125
|
+
newOverflowIndex++;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
break;
|
|
85
129
|
}
|
|
86
130
|
}
|
|
87
|
-
|
|
131
|
+
// Ensure at least one tab is visible
|
|
132
|
+
setOverflowIndex(Math.max(1, newOverflowIndex));
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!ref.current) return;
|
|
137
|
+
|
|
138
|
+
const root = Array.from(ref.current.children).find(e => e.classList.contains("tabRoot")) as
|
|
139
|
+
| HTMLElement
|
|
140
|
+
| undefined;
|
|
141
|
+
if (!root) return;
|
|
142
|
+
|
|
143
|
+
const resizeObserver = new ResizeObserver(() => computeOverflow());
|
|
144
|
+
resizeObserver.observe(root);
|
|
145
|
+
return () => resizeObserver.disconnect();
|
|
146
|
+
}, [computeOverflow]);
|
|
147
|
+
|
|
148
|
+
// Recompute when items change (e.g., conditional tabs added/removed)
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
// Reset cached widths when item count changes so stale entries are cleared
|
|
151
|
+
tabWidthsRef.current = [];
|
|
152
|
+
computeOverflow();
|
|
153
|
+
}, [inItems.length, computeOverflow]);
|
|
88
154
|
|
|
89
155
|
return (
|
|
90
156
|
<Stack
|
|
157
|
+
ref={stackRef}
|
|
91
158
|
flex={1}
|
|
92
159
|
direction="row"
|
|
93
160
|
alignItems="center"
|
|
@@ -131,6 +198,7 @@ export function Tabs(props: TabsProps) {
|
|
|
131
198
|
{(overflowIndex < items.length) && (
|
|
132
199
|
<Fragment>
|
|
133
200
|
<IconButton
|
|
201
|
+
ref={overflowButtonRef}
|
|
134
202
|
sx={{
|
|
135
203
|
ml: 1,
|
|
136
204
|
}}
|