@variocube/app-ui 1.16.1 → 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.
@@ -16,16 +16,16 @@ export function useStorage(key, defaultValue, storageType) {
16
16
  }, [key, updateStateFromStorage]);
17
17
  const typedValue = useMemo(() => {
18
18
  if (value === undefined) {
19
- return defaultValue;
19
+ return defaultValueSerialized !== undefined ? JSON.parse(defaultValueSerialized) : undefined;
20
20
  }
21
21
  try {
22
22
  return JSON.parse(value);
23
23
  }
24
24
  catch (e) {
25
25
  console.warn(`Failed to parse storage value for key "${key}", falling back to default value.`, e);
26
- return defaultValue;
26
+ return defaultValueSerialized !== undefined ? JSON.parse(defaultValueSerialized) : undefined;
27
27
  }
28
- }, [value, key, defaultValue]);
28
+ }, [value, key, defaultValueSerialized]);
29
29
  const setTypedValue = useCallback((newValue) => {
30
30
  const value = JSON.stringify(newValue);
31
31
  if (value != defaultValueSerialized) {
@@ -1 +1 @@
1
- {"version":3,"file":"useStorage.js","sourceRoot":"","sources":["../../src/storage/useStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAC;AACtE,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AAGlC,MAAM,UAAU,UAAU,CAAI,GAAW,EAAE,YAAe,EAAE,WAAyB;IACpF,MAAM,sBAAsB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAE3F,MAAM,oBAAoB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7C,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QACpD,OAAO,YAAY,aAAZ,YAAY,cAAZ,YAAY,GAAI,sBAAsB,CAAC;IAC/C,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,EAAE,WAAW,CAAC,CAAC,CAAC;IAE/C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,oBAAoB,CAAC,CAAC;IAEzD,MAAM,sBAAsB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC/C,QAAQ,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAClC,CAAC,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC;IAE3B,eAAe,CAAC,GAAG,EAAE;QACpB,OAAO,CAAC,iBAAiB,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC;QACvD,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC;IACxE,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAElC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE;QAC/B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,YAAY,CAAC;QACrB,CAAC;QACD,IAAI,CAAC;YACJ,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,0CAA0C,GAAG,mCAAmC,EAAE,CAAC,CAAC,CAAC;YAClG,OAAO,YAAY,CAAC;QACrB,CAAC;IACF,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC;IAE/B,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,QAAW,EAAE,EAAE;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,KAAK,IAAI,sBAAsB,EAAE,CAAC;YACrC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;QACxC,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAClC,CAAC;IACF,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,EAAE,WAAW,CAAC,CAAC,CAAC;IAE/C,OAAO,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAI,GAAW,EAAE,YAAe;IAC9D,OAAO,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;AAC/C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAI,GAAW,EAAE,YAAe;IAChE,OAAO,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"useStorage.js","sourceRoot":"","sources":["../../src/storage/useStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import * as React from "react";
5
+ import { useRef } from "react";
6
+ import { act, create } from "react-test-renderer";
7
+ import { useStorage } from "./useStorage";
8
+ // Reset storage between tests
9
+ beforeEach(() => {
10
+ localStorage.clear();
11
+ sessionStorage.clear();
12
+ });
13
+ function TestComponent({ storageKey }) {
14
+ const renderCount = useRef(0);
15
+ renderCount.current++;
16
+ // Inline object literal: new reference every render (the bug trigger)
17
+ const [value] = useStorage(storageKey, { page: 0, size: 25 });
18
+ return (React.createElement("div", { "data-render-count": renderCount.current, "data-value": JSON.stringify(value) }));
19
+ }
20
+ function getTestResult(renderer) {
21
+ const div = renderer.root.findByType("div");
22
+ return {
23
+ renderCount: Number(div.props["data-render-count"]),
24
+ value: JSON.parse(div.props["data-value"]),
25
+ };
26
+ }
27
+ describe("useStorage", () => {
28
+ test("does not cause infinite re-renders with unstable object defaultValue", () => {
29
+ let renderer;
30
+ act(() => {
31
+ renderer = create(React.createElement(TestComponent, { storageKey: "test-key" }));
32
+ });
33
+ const afterMount = getTestResult(renderer);
34
+ // Force a re-render with the same props (simulates parent re-render)
35
+ act(() => {
36
+ renderer.update(React.createElement(TestComponent, { storageKey: "test-key" }));
37
+ });
38
+ const afterUpdate = getTestResult(renderer);
39
+ // Render count should stay small, not grow unboundedly
40
+ expect(afterUpdate.renderCount).toBeLessThanOrEqual(3);
41
+ expect(afterUpdate.value).toEqual({ page: 0, size: 25 });
42
+ });
43
+ test("returns default value when no stored value exists", () => {
44
+ let renderer;
45
+ act(() => {
46
+ renderer = create(React.createElement(TestComponent, { storageKey: "missing-key" }));
47
+ });
48
+ const result = getTestResult(renderer);
49
+ expect(result.value).toEqual({ page: 0, size: 25 });
50
+ });
51
+ test("returns stored value when it exists", () => {
52
+ localStorage.setItem("stored-key", JSON.stringify({ page: 2, size: 50 }));
53
+ let renderer;
54
+ act(() => {
55
+ renderer = create(React.createElement(TestComponent, { storageKey: "stored-key" }));
56
+ });
57
+ const result = getTestResult(renderer);
58
+ expect(result.value).toEqual({ page: 2, size: 50 });
59
+ });
60
+ });
61
+ //# sourceMappingURL=useStorage.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStorage.spec.js","sourceRoot":"","sources":["../../src/storage/useStorage.spec.tsx"],"names":[],"mappings":"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"}
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
- function handleTabOverflow(root, items) {
32
- // clientWidth: what the user can see
33
- // scrollWidth: actual size of the DOM object
34
- const { clientWidth } = root;
35
- // this basically loop through each item size and adding up the width of all items
36
- // if the width exceed the clientWidth of the root node, item will be hidden from the horizontal tab display
37
- // else the item remains visible and its ref from the dropdown list will be hidden otherwise
38
- let overflowIndex = 0;
39
- let itemsWidth = 0;
40
- for (let i = 0; i < items.length; i++) {
41
- itemsWidth += items[i].width;
42
- if (itemsWidth < clientWidth) {
43
- overflowIndex++;
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
- setOverflowIndex(overflowIndex);
47
- }
48
- useEffect(() => {
49
- if (ref.current) {
50
- // fetch the root node of tabs
51
- const root = Array.from(ref.current.children).find(e => e.className.includes("tabRoot"));
52
- if (root) {
53
- // from the root node, traverse down to list node of tab items
54
- const list = Array.from(root.children).find(e => e.className.includes("tabList"));
55
- const items = list ? Array.from(list.children).map((e, index) => ({ index, width: e.clientWidth })) : [];
56
- // apply the resize observer to the root node and watch for the size changes
57
- const resizeObserver = new ResizeObserver(() => handleTabOverflow(root, items));
58
- resizeObserver.observe(root);
59
- return () => resizeObserver.disconnect();
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
- }, [ref]);
63
- return (React.createElement(Stack, { flex: 1, direction: "row", alignItems: "center", sx: {
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" })),
@@ -1 +1 @@
1
- {"version":3,"file":"Tabs.js","sourceRoot":"","sources":["../../src/tabs/Tabs.tsx"],"names":[],"mappings":"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"}
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.1",
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",
@@ -58,7 +58,7 @@
58
58
  "react-dom": "^17.0.2",
59
59
  "react-router": "^6.3.0",
60
60
  "react-router-dom": "^6.3.0",
61
- "react-syntax-highlighter": "^15.5.0",
61
+ "react-syntax-highlighter": "^16.1.0",
62
62
  "react-test-renderer": "^17.0.2",
63
63
  "rimraf": "^3.0.2",
64
64
  "ts-jest": "^29.1.2",
@@ -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
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import * as React from "react";
6
+ import {useRef} from "react";
7
+ import {act, create, ReactTestRenderer} from "react-test-renderer";
8
+ import {useStorage} from "./useStorage";
9
+
10
+ // Reset storage between tests
11
+ beforeEach(() => {
12
+ localStorage.clear();
13
+ sessionStorage.clear();
14
+ });
15
+
16
+ interface TestResult<T> {
17
+ value: T;
18
+ renderCount: number;
19
+ }
20
+
21
+ function TestComponent({storageKey}: { storageKey: string }) {
22
+ const renderCount = useRef(0);
23
+ renderCount.current++;
24
+
25
+ // Inline object literal: new reference every render (the bug trigger)
26
+ const [value] = useStorage(storageKey, {page: 0, size: 25});
27
+
28
+ return (
29
+ <div
30
+ data-render-count={renderCount.current}
31
+ data-value={JSON.stringify(value)}
32
+ />
33
+ );
34
+ }
35
+
36
+ function getTestResult<T>(renderer: ReactTestRenderer): TestResult<T> {
37
+ const div = renderer.root.findByType("div");
38
+ return {
39
+ renderCount: Number(div.props["data-render-count"]),
40
+ value: JSON.parse(div.props["data-value"]),
41
+ };
42
+ }
43
+
44
+ describe("useStorage", () => {
45
+ test("does not cause infinite re-renders with unstable object defaultValue", () => {
46
+ let renderer: ReactTestRenderer;
47
+ act(() => {
48
+ renderer = create(<TestComponent storageKey="test-key" />);
49
+ });
50
+
51
+ const afterMount = getTestResult(renderer!);
52
+
53
+ // Force a re-render with the same props (simulates parent re-render)
54
+ act(() => {
55
+ renderer.update(<TestComponent storageKey="test-key" />);
56
+ });
57
+
58
+ const afterUpdate = getTestResult(renderer!);
59
+
60
+ // Render count should stay small, not grow unboundedly
61
+ expect(afterUpdate.renderCount).toBeLessThanOrEqual(3);
62
+ expect(afterUpdate.value).toEqual({page: 0, size: 25});
63
+ });
64
+
65
+ test("returns default value when no stored value exists", () => {
66
+ let renderer: ReactTestRenderer;
67
+ act(() => {
68
+ renderer = create(<TestComponent storageKey="missing-key" />);
69
+ });
70
+
71
+ const result = getTestResult(renderer!);
72
+ expect(result.value).toEqual({page: 0, size: 25});
73
+ });
74
+
75
+ test("returns stored value when it exists", () => {
76
+ localStorage.setItem("stored-key", JSON.stringify({page: 2, size: 50}));
77
+
78
+ let renderer: ReactTestRenderer;
79
+ act(() => {
80
+ renderer = create(<TestComponent storageKey="stored-key" />);
81
+ });
82
+
83
+ const result = getTestResult(renderer!);
84
+ expect(result.value).toEqual({page: 2, size: 50});
85
+ });
86
+ });
@@ -23,15 +23,15 @@ export function useStorage<T>(key: string, defaultValue: T, storageType?: Storag
23
23
 
24
24
  const typedValue = useMemo(() => {
25
25
  if (value === undefined) {
26
- return defaultValue;
26
+ return defaultValueSerialized !== undefined ? JSON.parse(defaultValueSerialized) : undefined;
27
27
  }
28
28
  try {
29
29
  return JSON.parse(value);
30
30
  } catch (e) {
31
31
  console.warn(`Failed to parse storage value for key "${key}", falling back to default value.`, e);
32
- return defaultValue;
32
+ return defaultValueSerialized !== undefined ? JSON.parse(defaultValueSerialized) : undefined;
33
33
  }
34
- }, [value, key, defaultValue]);
34
+ }, [value, key, defaultValueSerialized]);
35
35
 
36
36
  const setTypedValue = useCallback((newValue: T) => {
37
37
  const value = JSON.stringify(newValue);
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
- interface TabItem {
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
- function handleTabOverflow(root: Element, items: TabItem[]) {
52
- // clientWidth: what the user can see
53
- // scrollWidth: actual size of the DOM object
54
- const {clientWidth} = root;
55
-
56
- // this basically loop through each item size and adding up the width of all items
57
- // if the width exceed the clientWidth of the root node, item will be hidden from the horizontal tab display
58
- // else the item remains visible and its ref from the dropdown list will be hidden otherwise
59
- let overflowIndex = 0;
60
- let itemsWidth = 0;
61
- for (let i = 0; i < items.length; i++) {
62
- itemsWidth += items[i].width;
63
- if (itemsWidth < clientWidth) {
64
- overflowIndex++;
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
- useEffect(() => {
71
- if (ref.current) {
72
- // fetch the root node of tabs
73
- const root = Array.from((ref.current as HTMLButtonElement).children).find(e =>
74
- e.className.includes("tabRoot")
75
- );
76
- if (root) {
77
- // from the root node, traverse down to list node of tab items
78
- const list = Array.from(root.children).find(e => e.className.includes("tabList"));
79
- const items = list ? Array.from(list.children).map((e, index) => ({index, width: e.clientWidth})) : [];
80
-
81
- // apply the resize observer to the root node and watch for the size changes
82
- const resizeObserver = new ResizeObserver(() => handleTabOverflow(root, items));
83
- resizeObserver.observe(root);
84
- return () => resizeObserver.disconnect();
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
- }, [ref]);
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
  }}