@@ -81,174 +81,154 @@ const getValidChildren = (children: React.ReactNode) => {
81
81
82
82
function Breadcrumbs ( { className, children, sx : sxProp , overflow = 'wrap' , hideRoot = true } : BreadcrumbsProps ) {
83
83
const containerRef = useRef < HTMLElement > ( null )
84
- const [ containerWidth , setContainerWidth ] = useState < number > ( 0 )
85
- const [ itemWidths , setItemWidths ] = useState < number [ ] > ( [ ] )
86
84
const [ effectiveHideRoot , setEffectiveHideRoot ] = useState < boolean > ( hideRoot )
85
+ let effectiveOverflow = 'wrap'
87
86
88
87
const childArray = useMemo ( ( ) => getValidChildren ( children ) , [ children ] )
89
88
90
- // Initialize visibleItems based on childArray for SSR compatibility
89
+ const rootItem = childArray [ 0 ]
90
+
91
91
const [ visibleItems , setVisibleItems ] = useState < React . ReactElement [ ] > ( ( ) => childArray )
92
+ const [ childArrayWidths , setChildArrayWidths ] = useState < number [ ] > ( ( ) => [ ] )
93
+
92
94
const [ menuItems , setMenuItems ] = useState < React . ReactElement [ ] > ( [ ] )
95
+ const [ rootItemWidth , setRootItemWidth ] = useState < number > ( 0 )
93
96
94
97
// SSR friendly
95
98
if ( typeof window !== 'undefined' ) {
96
- overflow = 'wrap'
99
+ effectiveOverflow = overflow
97
100
}
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
105
102
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 )
109
110
}
110
- } , [ ] )
111
+ } , [ childArray . length ] )
111
112
112
- useResizeObserver ( handleResize , containerRef )
113
+ const calculateOverflow = useCallback (
114
+ ( availableWidth : number ) => {
115
+ const MENU_BUTTON_WIDTH = 50 // Approximate width of "..." button
113
116
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
121
120
}
122
121
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
141
127
142
- let currentVisibleItems = [ ... childArray ]
143
- let currentMenuItems : React . ReactElement [ ] = [ ]
128
+ if ( availableWidth > 0 && currentVisibleItemWidths . length > 0 ) {
129
+ let visibleItemsWidthTotal = calculateVisibleItemsWidth ( currentVisibleItemWidths )
144
130
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
151
134
}
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
158
150
if ( currentMenuItems . length > 0 ) {
159
151
visibleItemsWidthTotal += MENU_BUTTON_WIDTH
160
152
}
161
153
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
204
161
}
205
162
}
206
- return {
207
- visibleItems : currentVisibleItems ,
208
- menuItems : currentMenuItems ,
209
- effectiveHideRoot : eHideRoot ,
210
- }
211
163
}
164
+ return {
165
+ visibleItems : [ ...currentVisibleItems ] ,
166
+ menuItems : [ ...currentMenuItems ] ,
167
+ effectiveHideRoot : eHideRoot ,
168
+ }
169
+ } ,
170
+ [ MIN_VISIBLE_ITEMS , childArray , childArrayWidths , effectiveHideRoot , hideRoot , overflow , rootItemWidth ] ,
171
+ )
212
172
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 )
219
187
220
188
// Determine final children to render
221
189
const finalChildren = React . useMemo ( ( ) => {
222
- if ( overflow === 'wrap' || menuItems . length === 0 ) {
190
+ if ( effectiveOverflow === 'wrap' || menuItems . length === 0 ) {
223
191
return visibleItems . map ( ( child , index ) => (
224
- < li className = { classes . ItemWrapper } key = { index } >
192
+ < li className = { classes . ItemWrapper } key = { `visible + ${ index } ` } >
225
193
{ child }
226
194
</ li >
227
195
) )
228
196
}
229
197
230
- // Create menu item and combine with visible items
198
+ let effectiveMenuItems = [ ...menuItems ]
199
+ if ( ! effectiveHideRoot ) {
200
+ effectiveMenuItems = [ ...menuItems . slice ( 1 ) ]
201
+ }
231
202
const menuElement = (
232
203
< 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
+ />
234
208
</ li >
235
209
)
236
210
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 } ` } >
239
213
{ child }
240
214
</ li >
241
215
) )
242
216
217
+ const rootElement = (
218
+ < li className = { classes . ItemWrapper } key = { `rootElement` } >
219
+ { rootItem }
220
+ </ li >
221
+ )
222
+
243
223
// Position menu based on effective hideRoot setting and visible items
244
224
if ( effectiveHideRoot ) {
245
225
// Show: [overflow menu, leaf breadcrumb]
246
226
return [ menuElement , ...visibleElements ]
247
227
} else {
248
228
// Show: [root breadcrumb, overflow menu, leaf breadcrumb]
249
- return [ visibleElements [ 0 ] , menuElement , ...visibleElements . slice ( 1 ) ]
229
+ return [ rootElement , menuElement , ...visibleElements ]
250
230
}
251
- } , [ overflow , menuItems , visibleItems , effectiveHideRoot ] )
231
+ } , [ effectiveOverflow , menuItems , visibleItems , rootItem , effectiveHideRoot ] )
252
232
253
233
return (
254
234
< BoxWithFallback
@@ -257,7 +237,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo
257
237
aria-label = "Breadcrumbs"
258
238
sx = { sxProp }
259
239
ref = { containerRef }
260
- data-overflow = { overflow }
240
+ data-overflow = { effectiveOverflow }
261
241
>
262
242
< BreadcrumbsList > { finalChildren } </ BreadcrumbsList >
263
243
</ BoxWithFallback >
0 commit comments