Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/graduate-breadcrumbs-overflow-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Breadcrumbs: Graduate `primer_react_breadcrumbs_overflow_menu` feature flag, making the overflow menu behavior always-on.
60 changes: 13 additions & 47 deletions packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {Meta} from '@storybook/react-vite'
import type React from 'react'
import type {ComponentProps} from '../utils/types'
import Breadcrumbs from './Breadcrumbs'
import {FeatureFlags} from '../FeatureFlags'

export default {
title: 'Components/Breadcrumbs/Features',
Expand All @@ -23,23 +22,7 @@ export const OverflowWrap = () => (
</Breadcrumbs>
)

export const OverflowMenuFeatureFlagEnabled = () => (
<FeatureFlags flags={{primer_react_breadcrumbs_overflow_menu: true}}>
<Breadcrumbs overflow="menu">
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Products</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Category</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Subcategory</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Item</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Details</Breadcrumbs.Item>
<Breadcrumbs.Item href="#" selected>
Current Page
</Breadcrumbs.Item>
</Breadcrumbs>
</FeatureFlags>
)

export const OverflowMenuFeatureFlagDisabled = () => (
export const OverflowMenu = () => (
<Breadcrumbs overflow="menu">
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Products</Breadcrumbs.Item>
Expand All @@ -53,7 +36,7 @@ export const OverflowMenuFeatureFlagDisabled = () => (
</Breadcrumbs>
)

export const OverflowMenuShowRootFeatureFlagDisabled = () => (
export const OverflowMenuWithRoot = () => (
<Breadcrumbs overflow="menu-with-root">
<Breadcrumbs.Item href="#">github</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Teams</Breadcrumbs.Item>
Expand All @@ -66,35 +49,18 @@ export const OverflowMenuShowRootFeatureFlagDisabled = () => (
</Breadcrumbs>
)

export const OverflowMenuShowRootFeatureFlagEnabled = () => (
<FeatureFlags flags={{primer_react_breadcrumbs_overflow_menu: true}}>
<Breadcrumbs overflow="menu-with-root">
<Breadcrumbs.Item href="#">github</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Teams</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Engineering</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">core-productivity</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">collaboration-workflows-flex</Breadcrumbs.Item>
<Breadcrumbs.Item href="#" selected>
global-navigation-reviewers
</Breadcrumbs.Item>
</Breadcrumbs>
</FeatureFlags>
)

export const SpaciousVariantWithOverflowMenu = () => (
<FeatureFlags flags={{primer_react_breadcrumbs_overflow_menu: true}}>
<Breadcrumbs overflow="menu" variant="spacious">
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Products</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Category</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Subcategory</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Item</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Details</Breadcrumbs.Item>
<Breadcrumbs.Item href="#" selected>
Current Page
</Breadcrumbs.Item>
</Breadcrumbs>
</FeatureFlags>
<Breadcrumbs overflow="menu" variant="spacious">
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Products</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Category</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Subcategory</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Item</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Details</Breadcrumbs.Item>
<Breadcrumbs.Item href="#" selected>
Current Page
</Breadcrumbs.Item>
</Breadcrumbs>
)

export const SpaciousVariantWithOverflowWrap = () => (
Expand Down
121 changes: 50 additions & 71 deletions packages/react/src/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {useResizeObserver} from '../hooks/useResizeObserver'
import type {ResizeObserverEntry} from '../hooks/useResizeObserver'
import {useOnEscapePress} from '../hooks/useOnEscapePress'
import {useOnOutsideClick} from '../hooks/useOnOutsideClick'
import {useFeatureFlag} from '../FeatureFlags'
import {type PolymorphicProps, fixedForwardRef} from '../utils/modern-polymorphic'

export type BreadcrumbsProps = React.PropsWithChildren<{
Expand Down Expand Up @@ -146,8 +145,6 @@ const getValidChildren = (children: React.ReactNode) => {
}

function Breadcrumbs({className, children, style, overflow = 'wrap', variant = 'normal'}: BreadcrumbsProps) {
const overflowMenuEnabled = useFeatureFlag('primer_react_breadcrumbs_overflow_menu')
const wrappedChildren = React.Children.map(children, child => <li className={classes.ItemWrapper}>{child}</li>)
const containerRef = useRef<HTMLElement>(null)

const measureMenuButton = useCallback((element: HTMLDetailsElement | null) => {
Expand Down Expand Up @@ -180,18 +177,13 @@ function Breadcrumbs({className, children, style, overflow = 'wrap', variant = '

useEffect(() => {
const listElement = containerRef.current?.querySelector('ol')
if (
overflowMenuEnabled &&
listElement &&
listElement.children.length > 0 &&
listElement.children.length === childArray.length
) {
if (listElement && listElement.children.length > 0 && listElement.children.length === childArray.length) {
const listElementArray = Array.from(listElement.children) as HTMLElement[]
const widths = listElementArray.map(child => child.offsetWidth)
setChildArrayWidths(widths)
setRootItemWidth(listElementArray[0].offsetWidth)
}
}, [childArray, overflowMenuEnabled])
}, [childArray])

const calculateOverflow = useCallback(
(availableWidth: number) => {
Expand Down Expand Up @@ -252,7 +244,7 @@ function Breadcrumbs({className, children, style, overflow = 'wrap', variant = '

const handleResize = useCallback(
(entries: ResizeObserverEntry[]) => {
if (overflowMenuEnabled && entries[0]) {
if (entries[0]) {
const containerWidth = entries[0].contentRect.width
const result = calculateOverflow(containerWidth)
if (
Expand All @@ -265,73 +257,69 @@ function Breadcrumbs({className, children, style, overflow = 'wrap', variant = '
}
}
},
[calculateOverflow, effectiveHideRoot, menuItems.length, overflowMenuEnabled, visibleItems.length],
[calculateOverflow, effectiveHideRoot, menuItems.length, visibleItems.length],
)

useResizeObserver(handleResize, containerRef)

useEffect(() => {
if (
overflowMenuEnabled &&
(overflow === 'menu' || overflow === 'menu-with-root') &&
childArray.length > 5 &&
menuItems.length === 0
) {
if ((overflow === 'menu' || overflow === 'menu-with-root') && childArray.length > 5 && menuItems.length === 0) {
const containerWidth = containerRef.current?.offsetWidth || 800
const result = calculateOverflow(containerWidth)

setVisibleItems(result.visibleItems)

setMenuItems(result.menuItems)

setEffectiveHideRoot(result.effectiveHideRoot)
}
}, [overflow, childArray, calculateOverflow, menuItems.length, overflowMenuEnabled])
}, [overflow, childArray, calculateOverflow, menuItems.length])

const finalChildren = React.useMemo(() => {
if (overflowMenuEnabled) {
if (overflow === 'wrap' || menuItems.length === 0) {
return React.Children.map(children, child => <li className={classes.ItemWrapper}>{child}</li>)
}
if (overflow === 'wrap' || menuItems.length === 0) {
return React.Children.map(children, child => <li className={classes.ItemWrapper}>{child}</li>)
}

let effectiveMenuItems = [...menuItems]
// In 'menu-with-root' mode, include the root item inside the menu even if it's visible in the breadcrumbs
if (!effectiveHideRoot) {
effectiveMenuItems = [...menuItems.slice(1)]
}
const menuElement = (
<li className={classes.BreadcrumbsItem} key="breadcrumbs-menu">
<BreadcrumbsMenuItem
ref={measureMenuButton}
items={effectiveMenuItems}
aria-label={`${effectiveMenuItems.length} more breadcrumb items`}
/>
<ItemSeparator />
</li>
)

const visibleElements = visibleItems.map((child, index) => (
<li className={classes.BreadcrumbsItem} key={`visible + ${index}`}>
{child}
<ItemSeparator />
</li>
))

const rootElement = (
<li className={classes.BreadcrumbsItem} key={`rootElement`}>
{rootItem}
<ItemSeparator />
</li>
)

if (effectiveHideRoot) {
// Show: [overflow menu, leaf breadcrumb]
return [menuElement, ...visibleElements]
} else {
// Show: [root breadcrumb, overflow menu, leaf breadcrumb]
return [rootElement, menuElement, ...visibleElements]
}
let effectiveMenuItems = [...menuItems]
// In 'menu-with-root' mode, include the root item inside the menu even if it's visible in the breadcrumbs
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the root item should be included inside the overflow menu when it’s visible, but the code slices it out (menuItems.slice(1)) when !effectiveHideRoot. Either update the comment to match the behavior, or adjust the logic if the root is intended to appear in the menu as well.

Suggested change
// In 'menu-with-root' mode, include the root item inside the menu even if it's visible in the breadcrumbs
// When the root item remains visible in the breadcrumbs, exclude it from the overflow menu to avoid duplication.

Copilot uses AI. Check for mistakes.
if (!effectiveHideRoot) {
effectiveMenuItems = [...menuItems.slice(1)]
}
}, [overflowMenuEnabled, overflow, menuItems, effectiveHideRoot, measureMenuButton, visibleItems, rootItem, children])
const menuElement = (
<li className={classes.BreadcrumbsItem} key="breadcrumbs-menu">
<BreadcrumbsMenuItem
ref={measureMenuButton}
items={effectiveMenuItems}
aria-label={`${effectiveMenuItems.length} more breadcrumb items`}
/>
<ItemSeparator />
</li>
)

const visibleElements = visibleItems.map((child, index) => (
<li className={classes.BreadcrumbsItem} key={`visible + ${index}`}>
{child}
<ItemSeparator />
</li>
))

const rootElement = (
<li className={classes.BreadcrumbsItem} key={`rootElement`}>
{rootItem}
<ItemSeparator />
</li>
)

return overflowMenuEnabled ? (
if (effectiveHideRoot) {
// Show: [overflow menu, leaf breadcrumb]
return [menuElement, ...visibleElements]
} else {
// Show: [root breadcrumb, overflow menu, leaf breadcrumb]
return [rootElement, menuElement, ...visibleElements]
}
}, [overflow, menuItems, effectiveHideRoot, measureMenuButton, visibleItems, rootItem, children])

return (
<nav
className={clsx(className, classes.BreadcrumbsBase)}
aria-label="Breadcrumbs"
Expand All @@ -342,15 +330,6 @@ function Breadcrumbs({className, children, style, overflow = 'wrap', variant = '
>
<BreadcrumbsList>{finalChildren}</BreadcrumbsList>
</nav>
) : (
<nav
className={clsx(className, classes.BreadcrumbsBase)}
aria-label="Breadcrumbs"
style={style}
data-variant={variant}
>
<BreadcrumbsList>{wrappedChildren}</BreadcrumbsList>
</nav>
)
}

Expand Down
Loading
Loading