Skip to content

Commit 2179d4c

Browse files
committed
fix: fixed Tabs menu is not displaying the "more" button
1 parent ac471e2 commit 2179d4c

File tree

1 file changed

+52
-52
lines changed

1 file changed

+52
-52
lines changed
Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { useLayoutEffect, useRef, useState } from 'react';
22

3-
import { useWindowSize } from '@openedx/paragon';
4-
53
const invisibleStyle = {
64
position: 'absolute',
75
left: 0,
@@ -10,68 +8,70 @@ const invisibleStyle = {
108
};
119

1210
/**
13-
* This hook will find the index of the last child of a containing element
14-
* that fits within its bounding rectangle. This is done by summing the widths
15-
* of the children until they exceed the width of the container.
11+
* This hook calculates the index of the last child that can fit into the
12+
* container element without overflowing. All children are rendered, but those
13+
* that exceed the available width are styled with `invisibleStyle` to hide them
14+
* visually while preserving their dimensions for measurement.
1615
*
17-
* The hook returns an array containing:
18-
* [indexOfLastVisibleChild, containerElementRef, invisibleStyle, overflowElementRef]
16+
* It uses ResizeObserver to automatically react to any changes in container
17+
* size or child widths — without requiring a window resize event.
1918
*
20-
* indexOfLastVisibleChild - the index of the last visible child
21-
* containerElementRef - a ref to be added to the containing html node
22-
* invisibleStyle - a set of styles to be applied to child of the containing node
23-
* if it needs to be hidden. These styles remove the element visually, from
24-
* screen readers, and from normal layout flow. But, importantly, these styles
25-
* preserve the width of the element, so that future width calculations will
26-
* still be accurate.
27-
* overflowElementRef - a ref to be added to an html node inside the container
28-
* that is likely to be used to contain a "More" type dropdown or other
29-
* mechanism to reveal hidden children. The width of this element is always
30-
* included when determining which children will fit or not. Usage of this ref
31-
* is optional.
19+
* Returns:
20+
* [
21+
* indexOfLastVisibleChild, // Index of the last tab that fits in the container
22+
* containerElementRef, // Ref to attach to the tabs container
23+
* invisibleStyle, // Style object to apply to "hidden" tabs
24+
* overflowElementRef // Ref to the overflow ("More...") element
25+
* ]
3226
*/
3327
export default function useIndexOfLastVisibleChild() {
3428
const containerElementRef = useRef(null);
3529
const overflowElementRef = useRef(null);
36-
const containingRectRef = useRef({});
3730
const [indexOfLastVisibleChild, setIndexOfLastVisibleChild] = useState(-1);
38-
const windowSize = useWindowSize();
3931

40-
useLayoutEffect(() => {
41-
const containingRect = containerElementRef.current.getBoundingClientRect();
32+
// Measures how many tab elements fit within the container's width
33+
const measureVisibleChildren = () => {
34+
const container = containerElementRef.current;
35+
const overflow = overflowElementRef.current;
36+
if (!container) { return; }
37+
38+
const containingRect = container.getBoundingClientRect();
39+
40+
// Get all children excluding the overflow element
41+
const children = Array.from(container.children).filter(child => child !== overflow);
42+
43+
let sumWidth = overflow ? overflow.getBoundingClientRect().width : 0;
44+
let lastVisibleIndex = -1;
4245

43-
// No-op if the width is unchanged.
44-
// (Assumes tabs themselves don't change count or width).
45-
if (!containingRect.width === containingRectRef.current.width) {
46-
return;
46+
for (let i = 0; i < children.length; i++) {
47+
const width = Math.floor(children[i].getBoundingClientRect().width);
48+
sumWidth += width;
49+
50+
if (sumWidth <= containingRect.width) {
51+
lastVisibleIndex = i;
52+
} else {
53+
break;
54+
}
4755
}
48-
// Update for future comparison
49-
containingRectRef.current = containingRect;
5056

51-
// Get array of child nodes from NodeList form
52-
const childNodesArr = Array.prototype.slice.call(containerElementRef.current.children);
53-
const { nextIndexOfLastVisibleChild } = childNodesArr
54-
// filter out the overflow element
55-
.filter(childNode => childNode !== overflowElementRef.current)
56-
// sum the widths to find the last visible element's index
57-
.reduce((acc, childNode, index) => {
58-
// use floor to prevent rounding errors
59-
acc.sumWidth += Math.floor(childNode.getBoundingClientRect().width);
60-
if (acc.sumWidth <= containingRect.width) {
61-
acc.nextIndexOfLastVisibleChild = index;
62-
}
63-
return acc;
64-
}, {
65-
// Include the overflow element's width to begin with. Doing this means
66-
// sometimes we'll show a dropdown with one item in it when it would fit,
67-
// but allowing this case dramatically simplifies the calculations we need
68-
// to do above.
69-
sumWidth: overflowElementRef.current ? overflowElementRef.current.getBoundingClientRect().width : 0,
70-
nextIndexOfLastVisibleChild: -1,
71-
});
57+
setIndexOfLastVisibleChild(lastVisibleIndex);
58+
};
59+
60+
useLayoutEffect(() => {
61+
const container = containerElementRef.current;
62+
if (!container) { return undefined; }
63+
64+
// ResizeObserver tracks size changes of the container or its children
65+
const resizeObserver = new ResizeObserver(() => {
66+
measureVisibleChildren();
67+
});
68+
69+
resizeObserver.observe(container);
70+
// Run once on mount to ensure accurate measurement from the start
71+
measureVisibleChildren();
7272

73-
setIndexOfLastVisibleChild(nextIndexOfLastVisibleChild);
74-
}, [windowSize, containerElementRef.current]);
73+
return () => resizeObserver.disconnect();
74+
}, []);
7575

7676
return [indexOfLastVisibleChild, containerElementRef, invisibleStyle, overflowElementRef];
7777
}

0 commit comments

Comments
 (0)