Skip to content

Commit a096e08

Browse files
committed
Rework calculations
1 parent e56256f commit a096e08

File tree

1 file changed

+97
-117
lines changed

1 file changed

+97
-117
lines changed

packages/react/src/Breadcrumbs/Breadcrumbs.tsx

Lines changed: 97 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -81,174 +81,154 @@ const getValidChildren = (children: React.ReactNode) => {
8181

8282
function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRoot = true}: BreadcrumbsProps) {
8383
const containerRef = useRef<HTMLElement>(null)
84-
const [containerWidth, setContainerWidth] = useState<number>(0)
85-
const [itemWidths, setItemWidths] = useState<number[]>([])
8684
const [effectiveHideRoot, setEffectiveHideRoot] = useState<boolean>(hideRoot)
85+
let effectiveOverflow = 'wrap'
8786

8887
const childArray = useMemo(() => getValidChildren(children), [children])
8988

90-
// Initialize visibleItems based on childArray for SSR compatibility
89+
const rootItem = childArray[0]
90+
9191
const [visibleItems, setVisibleItems] = useState<React.ReactElement[]>(() => childArray)
92+
const [childArrayWidths, setChildArrayWidths] = useState<number[]>(() => [])
93+
9294
const [menuItems, setMenuItems] = useState<React.ReactElement[]>([])
95+
const [rootItemWidth, setRootItemWidth] = useState<number>(0)
9396

9497
// SSR friendly
9598
if (typeof window !== 'undefined') {
96-
overflow = 'wrap'
99+
effectiveOverflow = overflow
97100
}
98-
// Sync visibleItems when childArray changes (for when children prop updates)
99-
useEffect(() => {
100-
if (overflow === 'wrap') {
101-
setVisibleItems(childArray)
102-
setMenuItems([])
103-
}
104-
}, [childArray, overflow])
101+
const MIN_VISIBLE_ITEMS = !effectiveHideRoot ? 3 : 4
105102

106-
const handleResize = useCallback((entries: ResizeObserverEntry[]) => {
107-
if (entries[0]) {
108-
setContainerWidth(entries[0].contentRect.width)
103+
useEffect(() => {
104+
const listElement = containerRef.current?.querySelector('ol')
105+
if (listElement && listElement.children.length > 0) {
106+
const listElementArray = Array.from(listElement.children) as HTMLElement[]
107+
const widths = listElementArray.map(child => child.offsetWidth)
108+
setChildArrayWidths(widths)
109+
setRootItemWidth(listElementArray[0].offsetWidth)
109110
}
110-
}, [])
111+
}, [childArray.length])
111112

112-
useResizeObserver(handleResize, containerRef)
113+
const calculateOverflow = useCallback(
114+
(availableWidth: number) => {
115+
const MENU_BUTTON_WIDTH = 50 // Approximate width of "..." button
113116

114-
useEffect(() => {
115-
if (childArray.length > 0) {
116-
if (overflow === 'wrap') {
117-
setVisibleItems(childArray)
118-
setMenuItems([])
119-
setEffectiveHideRoot(hideRoot)
120-
return
117+
const calculateVisibleItemsWidth = (w: number[]) => {
118+
const widths = w.reduce((sum, width) => sum + width + 16, 0)
119+
return !effectiveHideRoot ? rootItemWidth + widths : widths
121120
}
122121

123-
// For 'menu' overflow mode
124-
// Helper function to calculate visible items and menu items with progressive hiding
125-
const calculateOverflow = (availableWidth: number) => {
126-
const listElement = containerRef.current?.querySelector('ol')
127-
if (listElement && listElement.children.length > 0 && itemWidths.length === 0) {
128-
const widths = Array.from(listElement.children).map(child => (child as HTMLElement).offsetWidth)
129-
setItemWidths(widths)
130-
}
131-
const MENU_BUTTON_WIDTH = 50 // Approximate width of "..." button
132-
133-
// Helper function to calculate total width of visible items
134-
const calculateVisibleItemsWidth = (items: React.ReactElement[]) => {
135-
return items
136-
.map((item, index) => {
137-
return itemWidths[index]
138-
})
139-
.reduce((sum, width) => sum + width, 0)
140-
}
122+
let currentVisibleItems = [...childArray]
123+
let currentVisibleItemWidths = [...childArrayWidths]
124+
let currentMenuItems: React.ReactElement[] = []
125+
let currentMenuItemsWidths: number[] = []
126+
let eHideRoot = effectiveHideRoot
141127

142-
let currentVisibleItems = [...childArray]
143-
let currentMenuItems: React.ReactElement[] = []
128+
if (availableWidth > 0 && currentVisibleItemWidths.length > 0) {
129+
let visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths)
144130

145-
// If more than 5 items, start by reducing to 5 visible items (including menu)
146-
if (childArray.length > 5) {
147-
// Target: 4 visible items + 1 menu = 5 total
148-
const itemsToHide = childArray.slice(0, childArray.length - 4)
149-
currentMenuItems = itemsToHide
150-
currentVisibleItems = childArray.slice(childArray.length - 4)
131+
// Add menu button width if we have hidden items
132+
if (currentMenuItems.length > 0) {
133+
visibleItemsWidthTotal += MENU_BUTTON_WIDTH
151134
}
152-
let eHideRoot = hideRoot
153-
// Now check if current visible items fit in available width
154-
if (availableWidth > 0) {
155-
let visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItems)
156-
157-
// Add menu button width if we have hidden items
135+
while (
136+
overflow === 'menu' &&
137+
(visibleItemsWidthTotal > availableWidth || currentVisibleItems.length > MIN_VISIBLE_ITEMS)
138+
) {
139+
// Remove the last visible item
140+
const itemToHide = currentVisibleItems.slice(0)[0]
141+
const itemToHideWidth = currentVisibleItemWidths.slice(0)[0]
142+
currentMenuItems = [...currentMenuItems, itemToHide]
143+
currentMenuItemsWidths = [...currentMenuItemsWidths, itemToHideWidth]
144+
currentVisibleItems = [...currentVisibleItems.slice(1)]
145+
currentVisibleItemWidths = [...currentVisibleItemWidths.slice(1)]
146+
147+
visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths)
148+
149+
// Add menu button width
158150
if (currentMenuItems.length > 0) {
159151
visibleItemsWidthTotal += MENU_BUTTON_WIDTH
160152
}
161153

162-
while (visibleItemsWidthTotal > availableWidth && currentVisibleItems.length > 1) {
163-
// Determine which item to hide based on hideRoot setting
164-
let itemToHide: React.ReactElement
165-
166-
if (eHideRoot) {
167-
// Hide from start when hideRoot is true
168-
itemToHide = currentVisibleItems[0]
169-
currentVisibleItems = currentVisibleItems.slice(1)
170-
} else {
171-
// Try to hide second item (keep root and leaf) when hideRoot is false
172-
itemToHide = currentVisibleItems[1]
173-
currentVisibleItems = [currentVisibleItems[0], ...currentVisibleItems.slice(2)]
174-
}
175-
176-
currentMenuItems = [itemToHide, ...currentMenuItems]
177-
178-
visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItems)
179-
180-
// Add menu button width
181-
if (currentMenuItems.length > 0) {
182-
visibleItemsWidthTotal += MENU_BUTTON_WIDTH
183-
}
184-
185-
// If hideRoot is false but we still don't fit with root + menu + leaf,
186-
// fallback to hideRoot=true behavior (menu + leaf only)
187-
if (
188-
!hideRoot &&
189-
!eHideRoot &&
190-
currentVisibleItems.length === 2 &&
191-
visibleItemsWidthTotal > availableWidth
192-
) {
193-
eHideRoot = true
194-
const rootItem = currentVisibleItems[0]
195-
currentVisibleItems = currentVisibleItems.slice(1)
196-
currentMenuItems = [rootItem, ...currentMenuItems]
197-
198-
visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItems)
199-
200-
if (currentMenuItems.length > 0) {
201-
visibleItemsWidthTotal += MENU_BUTTON_WIDTH
202-
}
203-
}
154+
// If hideRoot is false but we still don't fit with root + menu + leaf,
155+
// fallback to hideRoot=true behavior (menu + leaf only)
156+
if (!hideRoot && currentVisibleItems.length === 1 && visibleItemsWidthTotal > availableWidth) {
157+
eHideRoot = true
158+
break
159+
} else {
160+
eHideRoot = hideRoot
204161
}
205162
}
206-
return {
207-
visibleItems: currentVisibleItems,
208-
menuItems: currentMenuItems,
209-
effectiveHideRoot: eHideRoot,
210-
}
211163
}
164+
return {
165+
visibleItems: [...currentVisibleItems],
166+
menuItems: [...currentMenuItems],
167+
effectiveHideRoot: eHideRoot,
168+
}
169+
},
170+
[MIN_VISIBLE_ITEMS, childArray, childArrayWidths, effectiveHideRoot, hideRoot, overflow, rootItemWidth],
171+
)
212172

213-
const result = calculateOverflow(containerWidth)
214-
setVisibleItems(result.visibleItems)
215-
setMenuItems(result.menuItems)
216-
setEffectiveHideRoot(result.effectiveHideRoot)
217-
}
218-
}, [childArray, overflow, containerWidth, hideRoot, itemWidths])
173+
const handleResize = useCallback(
174+
(entries: ResizeObserverEntry[]) => {
175+
if (entries[0]) {
176+
const containerWidth = entries[0].contentRect.width
177+
const result = calculateOverflow(containerWidth)
178+
setVisibleItems(result.visibleItems)
179+
setMenuItems(result.menuItems)
180+
setEffectiveHideRoot(result.effectiveHideRoot)
181+
}
182+
},
183+
[calculateOverflow],
184+
)
185+
186+
useResizeObserver(handleResize, containerRef)
219187

220188
// Determine final children to render
221189
const finalChildren = React.useMemo(() => {
222-
if (overflow === 'wrap' || menuItems.length === 0) {
190+
if (effectiveOverflow === 'wrap' || menuItems.length === 0) {
223191
return visibleItems.map((child, index) => (
224-
<li className={classes.ItemWrapper} key={index}>
192+
<li className={classes.ItemWrapper} key={`visible + ${index}`}>
225193
{child}
226194
</li>
227195
))
228196
}
229197

230-
// Create menu item and combine with visible items
198+
let effectiveMenuItems = [...menuItems]
199+
if (!effectiveHideRoot) {
200+
effectiveMenuItems = [...menuItems.slice(1)]
201+
}
231202
const menuElement = (
232203
<li className={classes.ItemWrapper} key="breadcrumbs-menu">
233-
<BreadcrumbsMenuItem items={menuItems} aria-label={`${menuItems.length} more breadcrumb items`} />
204+
<BreadcrumbsMenuItem
205+
items={effectiveMenuItems}
206+
aria-label={`${effectiveMenuItems.length} more breadcrumb items`}
207+
/>
234208
</li>
235209
)
236210

237-
const visibleElements = visibleItems.map(child => (
238-
<li className={classes.ItemWrapper} key={child.key}>
211+
const visibleElements = visibleItems.map((child, index) => (
212+
<li className={classes.ItemWrapper} key={`visible + ${index}`}>
239213
{child}
240214
</li>
241215
))
242216

217+
const rootElement = (
218+
<li className={classes.ItemWrapper} key={`rootElement`}>
219+
{rootItem}
220+
</li>
221+
)
222+
243223
// Position menu based on effective hideRoot setting and visible items
244224
if (effectiveHideRoot) {
245225
// Show: [overflow menu, leaf breadcrumb]
246226
return [menuElement, ...visibleElements]
247227
} else {
248228
// Show: [root breadcrumb, overflow menu, leaf breadcrumb]
249-
return [visibleElements[0], menuElement, ...visibleElements.slice(1)]
229+
return [rootElement, menuElement, ...visibleElements]
250230
}
251-
}, [overflow, menuItems, visibleItems, effectiveHideRoot])
231+
}, [effectiveOverflow, menuItems, visibleItems, rootItem, effectiveHideRoot])
252232

253233
return (
254234
<BoxWithFallback
@@ -257,7 +237,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo
257237
aria-label="Breadcrumbs"
258238
sx={sxProp}
259239
ref={containerRef}
260-
data-overflow={overflow}
240+
data-overflow={effectiveOverflow}
261241
>
262242
<BreadcrumbsList>{finalChildren}</BreadcrumbsList>
263243
</BoxWithFallback>

0 commit comments

Comments
 (0)