Skip to content

Commit c0f61c3

Browse files
committed
Add review comments and change behavior
1 parent 61122ea commit c0f61c3

File tree

2 files changed

+126
-117
lines changed

2 files changed

+126
-117
lines changed

packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx

Lines changed: 12 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,6 @@ export const OverflowMenu = () => (
4545
</Breadcrumbs>
4646
)
4747

48-
export const OverflowMenuHideRoot = () => (
49-
<Breadcrumbs overflow="menu" hideRoot={true}>
50-
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
51-
<Breadcrumbs.Item href="#">Products</Breadcrumbs.Item>
52-
<Breadcrumbs.Item href="#">Category</Breadcrumbs.Item>
53-
<Breadcrumbs.Item href="#">Subcategory</Breadcrumbs.Item>
54-
<Breadcrumbs.Item href="#">Item</Breadcrumbs.Item>
55-
<Breadcrumbs.Item href="#">Details</Breadcrumbs.Item>
56-
<Breadcrumbs.Item href="#" selected>
57-
Current Page
58-
</Breadcrumbs.Item>
59-
</Breadcrumbs>
60-
)
61-
6248
export const OverflowMenuShowRoot = () => (
6349
<Breadcrumbs overflow="menu" hideRoot={false}>
6450
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
@@ -73,62 +59,16 @@ export const OverflowMenuShowRoot = () => (
7359
</Breadcrumbs>
7460
)
7561

76-
export const OverflowMenuFewItems = () => (
77-
<Breadcrumbs overflow="menu">
78-
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
79-
<Breadcrumbs.Item href="#">About</Breadcrumbs.Item>
80-
<Breadcrumbs.Item href="#" selected>
81-
Team
82-
</Breadcrumbs.Item>
83-
</Breadcrumbs>
84-
)
85-
86-
export const OverflowMenuManyItems = () => (
87-
<Breadcrumbs overflow="menu">
88-
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
89-
<Breadcrumbs.Item href="#">Level 1</Breadcrumbs.Item>
90-
<Breadcrumbs.Item href="#">Level 2</Breadcrumbs.Item>
91-
<Breadcrumbs.Item href="#">Level 3</Breadcrumbs.Item>
92-
<Breadcrumbs.Item href="#">Level 4</Breadcrumbs.Item>
93-
<Breadcrumbs.Item href="#">Level 5</Breadcrumbs.Item>
94-
<Breadcrumbs.Item href="#">Level 6</Breadcrumbs.Item>
95-
<Breadcrumbs.Item href="#">Level 7</Breadcrumbs.Item>
96-
<Breadcrumbs.Item href="#">Level 8</Breadcrumbs.Item>
97-
<Breadcrumbs.Item href="#" selected>
98-
Current Page
99-
</Breadcrumbs.Item>
100-
</Breadcrumbs>
101-
)
102-
103-
export const OverflowMenuLongWords = () => (
104-
<Breadcrumbs overflow="menu">
105-
<Breadcrumbs.Item href="#">SupercalifragilisticexpialidociousRepository</Breadcrumbs.Item>
106-
<Breadcrumbs.Item href="#">AnticonstitutionnellementConfiguration</Breadcrumbs.Item>
107-
<Breadcrumbs.Item href="#">PneumonoultramicroscopicsilicovolcanoconiosisDocumentation</Breadcrumbs.Item>
108-
<Breadcrumbs.Item href="#" selected>
109-
HippopotomonstrosesquippedaliophobiaCurrentPage
110-
</Breadcrumbs.Item>
111-
</Breadcrumbs>
112-
)
113-
114-
export const OverflowMenuLongWordsHideRoot = () => (
115-
<Breadcrumbs overflow="menu" hideRoot={true}>
116-
<Breadcrumbs.Item href="#">SupercalifragilisticexpialidociousRepository</Breadcrumbs.Item>
117-
<Breadcrumbs.Item href="#">AnticonstitutionnellementConfiguration</Breadcrumbs.Item>
118-
<Breadcrumbs.Item href="#">PneumonoultramicroscopicsilicovolcanoconiosisDocumentation</Breadcrumbs.Item>
119-
<Breadcrumbs.Item href="#" selected>
120-
HippopotomonstrosesquippedaliophobiaCurrentPage
121-
</Breadcrumbs.Item>
122-
</Breadcrumbs>
123-
)
124-
125-
export const OverflowMenuLongWordsShowRoot = () => (
126-
<Breadcrumbs overflow="menu" hideRoot={false}>
127-
<Breadcrumbs.Item href="#">SupercalifragilisticexpialidociousRepository</Breadcrumbs.Item>
128-
<Breadcrumbs.Item href="#">AnticonstitutionnellementConfiguration</Breadcrumbs.Item>
129-
<Breadcrumbs.Item href="#">PneumonoultramicroscopicsilicovolcanoconiosisDocumentation</Breadcrumbs.Item>
130-
<Breadcrumbs.Item href="#" selected>
131-
HippopotomonstrosesquippedaliophobiaCurrentPage
132-
</Breadcrumbs.Item>
133-
</Breadcrumbs>
62+
export const OverflowMenuNarrowContainer = () => (
63+
<div style={{width: '200px', border: '1px solid #ccc', padding: '8px'}}>
64+
<Breadcrumbs overflow="menu">
65+
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
66+
<Breadcrumbs.Item href="#">Products</Breadcrumbs.Item>
67+
<Breadcrumbs.Item href="#">Category</Breadcrumbs.Item>
68+
<Breadcrumbs.Item href="#">Subcategory</Breadcrumbs.Item>
69+
<Breadcrumbs.Item href="#" selected>
70+
Current Page
71+
</Breadcrumbs.Item>
72+
</Breadcrumbs>
73+
</div>
13474
)

packages/react/src/Breadcrumbs/Breadcrumbs.tsx

Lines changed: 114 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,18 @@ const BreadcrumbsMenuItem = React.forwardRef<HTMLButtonElement, BreadcrumbsMenuI
3636
<ActionMenu>
3737
<ActionMenu.Button
3838
ref={ref}
39-
aria-label={ariaLabel || `${items.length} more items`}
39+
aria-label={ariaLabel || `${items.length} more breadcrumb items`}
40+
aria-haspopup="menu"
41+
aria-expanded="false"
4042
variant="invisible"
43+
trailingAction={null}
4144
style={{display: 'inline-flex'}}
4245
{...rest}
4346
>
4447
4548
</ActionMenu.Button>
4649
<ActionMenu.Overlay width="auto">
47-
<ActionList>
50+
<ActionList role="menu">
4851
{items.map((item, index) => {
4952
const href = item.props.href
5053
const children = item.props.children
@@ -53,6 +56,7 @@ const BreadcrumbsMenuItem = React.forwardRef<HTMLButtonElement, BreadcrumbsMenuI
5356
<ActionList.LinkItem
5457
key={index}
5558
href={href}
59+
role="menuitem"
5660
aria-current={selected ? 'page' : undefined}
5761
className={selected ? classes.ItemSelected : undefined}
5862
>
@@ -75,6 +79,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo
7579
const [visibleItems, setVisibleItems] = useState<React.ReactElement[]>([])
7680
const [menuItems, setMenuItems] = useState<React.ReactElement[]>([])
7781
const [itemWidths, setItemWidths] = useState<number[]>([])
82+
const [effectiveHideRoot, setEffectiveHideRoot] = useState<boolean>(hideRoot)
7883
const previousWidthsRef = useRef<string>('')
7984

8085
const childArray = React.Children.toArray(children).filter(child =>
@@ -120,56 +125,120 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo
120125
if (overflow === 'wrap') {
121126
setVisibleItems(childArray)
122127
setMenuItems([])
128+
setEffectiveHideRoot(hideRoot)
123129
return
124130
}
125131

126132
// For 'menu' overflow mode
127-
const lastItem = childArray[childArray.length - 1] // Leaf breadcrumb
128-
const firstItem = childArray[0] // Root breadcrumb
129-
130-
// First check: if more than 5 items, always use overflow
131-
if (childArray.length > 5) {
132-
if (hideRoot) {
133-
// Show only overflow menu and leaf breadcrumb
134-
const itemsToHide = childArray.slice(0, -1) // All except last
135-
setMenuItems(itemsToHide)
136-
setVisibleItems([lastItem])
137-
} else {
138-
// Show root breadcrumb, overflow menu, and leaf breadcrumb
139-
const itemsToHide = childArray.slice(1, -1) // All except first and last
140-
setMenuItems(itemsToHide)
141-
setVisibleItems([firstItem, lastItem])
133+
// Helper function to calculate visible items and menu items with progressive hiding
134+
const calculateOverflow = (availableWidth: number) => {
135+
const MENU_BUTTON_WIDTH = 50 // Approximate width of "..." button
136+
137+
let currentVisibleItems = [...childArray]
138+
let currentMenuItems: React.ReactElement[] = []
139+
140+
// If more than 5 items, start by reducing to 5 visible items (including menu)
141+
if (childArray.length > 5) {
142+
// Target: 4 visible items + 1 menu = 5 total
143+
const itemsToHide = childArray.slice(0, childArray.length - 4)
144+
currentMenuItems = itemsToHide
145+
currentVisibleItems = childArray.slice(childArray.length - 4)
142146
}
143-
return
144-
}
145147

146-
// Second check: if we have measured widths and container width, check if items fit
147-
if (containerWidth > 0 && itemWidths.length === childArray.length && itemWidths.length > 0) {
148-
const totalItemsWidth = itemWidths.reduce((sum, width) => sum + width, 0)
149-
// Add some buffer for the ellipsis menu button (approximately 50px)
150-
const bufferWidth = 50
151-
152-
if (totalItemsWidth + bufferWidth > containerWidth) {
153-
// Items don't fit, need to overflow
154-
if (hideRoot) {
155-
// Show only overflow menu and leaf breadcrumb
156-
const itemsToHide = childArray.slice(0, -1) // All except last
157-
setMenuItems(itemsToHide)
158-
setVisibleItems([lastItem])
159-
} else {
160-
// Show root breadcrumb, overflow menu, and leaf breadcrumb
161-
const itemsToHide = childArray.slice(1, -1) // All except first and last
162-
setMenuItems(itemsToHide)
163-
setVisibleItems([firstItem, lastItem])
148+
// Now check if current visible items fit in available width
149+
if (availableWidth > 0 && itemWidths.length === childArray.length) {
150+
let visibleItemsWidthTotal = currentVisibleItems
151+
.map(item => {
152+
const index = childArray.findIndex(child => child.key === item.key)
153+
return index !== -1 ? itemWidths[index] : 0
154+
})
155+
.reduce((sum, width) => sum + width, 0)
156+
157+
// Add menu button width if we have hidden items
158+
if (currentMenuItems.length > 0) {
159+
visibleItemsWidthTotal += MENU_BUTTON_WIDTH
160+
}
161+
162+
// Progressive hiding: keep moving items to menu until they fit
163+
let effectiveHideRoot = hideRoot
164+
165+
while (visibleItemsWidthTotal > availableWidth && currentVisibleItems.length > 1) {
166+
// Determine which item to hide based on hideRoot setting
167+
let itemToHide: React.ReactElement
168+
169+
if (effectiveHideRoot) {
170+
// Hide from start when hideRoot is true
171+
itemToHide = currentVisibleItems[0]
172+
currentVisibleItems = currentVisibleItems.slice(1)
173+
} else {
174+
// Try to hide second item (keep root and leaf) when hideRoot is false
175+
itemToHide = currentVisibleItems[1]
176+
currentVisibleItems = [currentVisibleItems[0], ...currentVisibleItems.slice(2)]
177+
}
178+
179+
currentMenuItems = [itemToHide, ...currentMenuItems]
180+
181+
// Recalculate width
182+
visibleItemsWidthTotal = currentVisibleItems
183+
.map(item => {
184+
const index = childArray.findIndex(child => child.key === item.key)
185+
return index !== -1 ? itemWidths[index] : 0
186+
})
187+
.reduce((sum, width) => sum + width, 0)
188+
189+
// Add menu button width
190+
if (currentMenuItems.length > 0) {
191+
visibleItemsWidthTotal += MENU_BUTTON_WIDTH
192+
}
193+
194+
// If hideRoot is false but we still don't fit with root + menu + leaf,
195+
// fallback to hideRoot=true behavior (menu + leaf only)
196+
if (
197+
!hideRoot &&
198+
!effectiveHideRoot &&
199+
currentVisibleItems.length === 2 &&
200+
visibleItemsWidthTotal > availableWidth
201+
) {
202+
effectiveHideRoot = true
203+
// Move the root item to menu as well
204+
const rootItem = currentVisibleItems[0]
205+
currentVisibleItems = currentVisibleItems.slice(1)
206+
currentMenuItems = [rootItem, ...currentMenuItems]
207+
208+
// Recalculate width one more time
209+
visibleItemsWidthTotal = currentVisibleItems
210+
.map(item => {
211+
const index = childArray.findIndex(child => child.key === item.key)
212+
return index !== -1 ? itemWidths[index] : 0
213+
})
214+
.reduce((sum, width) => sum + width, 0)
215+
216+
if (currentMenuItems.length > 0) {
217+
visibleItemsWidthTotal += MENU_BUTTON_WIDTH
218+
}
219+
}
220+
}
221+
222+
// Final check: if even the leaf breadcrumb + menu doesn't fit, just show them anyway
223+
// The CSS will handle truncation of the leaf breadcrumb
224+
if (visibleItemsWidthTotal > availableWidth && currentVisibleItems.length === 1) {
225+
// Keep the current configuration - CSS will handle truncation
164226
}
165-
return
227+
}
228+
229+
return {
230+
visibleItems: currentVisibleItems,
231+
menuItems: currentMenuItems,
232+
effectiveHideRoot,
166233
}
167234
}
168235

169-
// No overflow needed - show all items
170-
setVisibleItems(childArray)
171-
setMenuItems([])
172-
}, [childArray, overflow, containerWidth, hideRoot, itemWidths])
236+
// Apply the overflow calculation
237+
const result = calculateOverflow(containerWidth)
238+
setVisibleItems(result.visibleItems)
239+
setMenuItems(result.menuItems)
240+
setEffectiveHideRoot(result.effectiveHideRoot)
241+
}, [childArray, overflow, containerWidth, hideRoot, itemWidths, effectiveHideRoot])
173242

174243
// Determine final children to render
175244
const finalChildren = React.useMemo(() => {
@@ -194,15 +263,15 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo
194263
</li>
195264
))
196265

197-
// Position menu based on hideRoot setting and visible items
198-
if (hideRoot) {
266+
// Position menu based on effective hideRoot setting and visible items
267+
if (effectiveHideRoot) {
199268
// Show: [overflow menu, leaf breadcrumb]
200269
return [menuElement, ...visibleElements]
201270
} else {
202271
// Show: [root breadcrumb, overflow menu, leaf breadcrumb]
203272
return [visibleElements[0], menuElement, ...visibleElements.slice(1)]
204273
}
205-
}, [overflow, menuItems, visibleItems, hideRoot])
274+
}, [overflow, menuItems, visibleItems, effectiveHideRoot])
206275

207276
return (
208277
<BoxWithFallback

0 commit comments

Comments
 (0)