Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
a7208d4
Spike for breadcrumbs overflow
pksjce Aug 11, 2025
54c9148
Add changeset for breadcrumbs overflow
pksjce Aug 11, 2025
dc5dac1
Add review comments and change behavior
pksjce Aug 12, 2025
7df321a
Fix up some issues.
pksjce Aug 13, 2025
611d703
check for zero children
pksjce Aug 13, 2025
1a84b8a
Fix bug with hideRoot
pksjce Aug 13, 2025
c567b15
Add test cases
pksjce Aug 13, 2025
35c0971
Add docs
pksjce Aug 13, 2025
901ba0c
Breadcrumbs can have more stories
pksjce Aug 13, 2025
f8fd857
Add features stories for breadcrumbs
pksjce Aug 13, 2025
f415276
Add wrapped breadcrumbs in story
pksjce Aug 14, 2025
68eba14
Fix for ssr and child key
pksjce Aug 15, 2025
f4bba96
Add IconButton
pksjce Aug 15, 2025
b55755c
Fix for SSR
pksjce Aug 18, 2025
9477af7
Fix for SSR
pksjce Aug 18, 2025
df9aa0f
Final changes for SSR
pksjce Aug 18, 2025
fcd8a8e
Rework calculations
pksjce Aug 19, 2025
0a09e45
Tests are passing
pksjce Aug 19, 2025
3b9891a
Small fixes to menu button
pksjce Aug 21, 2025
273a055
Fix styling issues with button
pksjce Aug 21, 2025
08637ae
Fix focus states
pksjce Aug 22, 2025
17a3107
Fix focus states
pksjce Aug 22, 2025
ed838f3
Make sure old behavior works
pksjce Aug 25, 2025
1870eff
Fix tests and lint
pksjce Aug 25, 2025
0eaf106
Create eighty-queens-tap.md
pksjce Aug 25, 2025
a24d75a
Merge branch 'main' into pk/breadcrumbs-with-overflow-menu
pksjce Aug 25, 2025
40cb6a9
chore(deps-dev): bump the eslint group with 3 updates (#6657)
dependabot[bot] Aug 25, 2025
027f4f6
chore(deps): bump the rollup group with 2 updates (#6659)
dependabot[bot] Aug 25, 2025
7b4921c
chore(deps-dev): bump postcss-mixins from 11.0.1 to 12.1.2 (#6660)
dependabot[bot] Aug 25, 2025
bc2749c
chore(deps-dev): bump @github/markdownlint-github from 0.7.0 to 0.8.0…
dependabot[bot] Aug 25, 2025
f9c9a2d
feat(mcp): add better primitives output, add coding guidelines tool (…
joshblack Aug 25, 2025
870a8ca
Convert menu to disclosure pattern
pksjce Aug 28, 2025
bc99a17
Fix bugs
pksjce Aug 28, 2025
020f0b7
Fix up infinite loop at 1 remaining visible item
pksjce Aug 29, 2025
8cf263e
Remove unnecessary comments
pksjce Aug 29, 2025
23ddd0e
Use ref callback for menu width calculation
pksjce Aug 29, 2025
639a782
Fix up review comments
pksjce Sep 1, 2025
2e367bf
Add feature flags
pksjce Sep 1, 2025
a5b9c20
Merge branch 'pk/breadcrumbs-with-overflow-menu' of https://github.co…
pksjce Sep 1, 2025
bb5aa77
Delete .changeset/good-cougars-hug.md
pksjce Sep 1, 2025
b8063b8
Add different prop to hideRoot
pksjce Sep 1, 2025
c7669d6
Add tests
pksjce Sep 1, 2025
5c3fc21
Merge branch 'pk/breadcrumbs-with-overflow-menu' of https://github.co…
pksjce Sep 1, 2025
e2f34c5
Merge branch 'main' into pk/breadcrumbs-with-overflow-menu
pksjce Sep 1, 2025
a7418b7
Fix lint and test issue
pksjce Sep 1, 2025
7c2ced8
Merge branch 'pk/breadcrumbs-with-overflow-menu' of https://github.co…
pksjce Sep 1, 2025
0846a5a
Dont use story in aat
pksjce Sep 1, 2025
7df8356
Fix for aat
pksjce Sep 1, 2025
92b813e
Story color needs changed
pksjce Sep 1, 2025
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/eighty-queens-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

Breadcrumbs : Add overflow menu for responsive behavior
28 changes: 24 additions & 4 deletions packages/react/src/Breadcrumbs/Breadcrumbs.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,27 @@
"name": "className",
"type": "string",
"required": false,
"description": "",
"description": "Additional CSS class names to apply to the breadcrumbs container",
"defaultValue": ""
},
{
"name": "children",
"type": "Breadcrumbs.Item[]",
"defaultValue": "",
"description": ""
"description": "Breadcrumb items to render. Each item should be a Breadcrumbs.Item component."
},
{
"name": "overflow",
"type": "'wrap' | 'menu' | 'menu-with-root'",
"required": false,
"description": "How to handle overflow when breadcrumbs don't fit in the container. 'wrap' allows items to wrap to new lines. 'menu' collapses items into an overflow menu. 'menu-with-root' also collapses items into an overflow menu but includes the root (first) breadcrumb in the menu so only the last items remain visible.",
"defaultValue": "'wrap'"
},
{
"name": "sx",
"type": "SystemStyleObject",
"deprecated": true
"deprecated": true,
"description": "System styles (deprecated, use CSS classes instead)"
}
],
"subcomponents": [
Expand All @@ -37,7 +45,7 @@
"name": "selected",
"type": "boolean",
"defaultValue": "false",
"description": "Whether this item represents the current page"
"description": "Whether this item represents the current page. Sets aria-current='page' for accessibility."
},
{
"name": "to",
Expand All @@ -46,6 +54,18 @@
"description": "Used when the item is rendered using a component like React Router's `Link`. The path to navigate to.",
"defaultValue": ""
},
{
"name": "href",
"type": "string",
"required": false,
"description": "The URL that the breadcrumb item links to. Used with regular anchor elements."
},
{
"name": "children",
"type": "React.ReactNode",
"required": true,
"description": "The content to display inside the breadcrumb item, typically text."
},
{
"name": "ref",
"type": "React.RefObject<HTMLAnchorElement>"
Expand Down
207 changes: 207 additions & 0 deletions packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import type {Meta} from '@storybook/react-vite'
import type React from 'react'
import {useState} from 'react'
import type {ComponentProps} from '../utils/types'
import Breadcrumbs from './Breadcrumbs'
import TextInput from '../TextInput'

export default {
title: 'Components/Breadcrumbs/Features',
component: Breadcrumbs,
} as Meta<ComponentProps<typeof Breadcrumbs>>

export const OverflowWrap = () => (
<Breadcrumbs overflow="wrap">
<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 OverflowMenu = () => (
<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>
)

export const OverflowMenuShowRoot = () => (
<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>
)

export const OverflowMenuNarrowContainer = () => (
<div style={{width: '350px', border: '1px solid #ccc', padding: '8px'}}>
<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="#" selected>
Current Page
</Breadcrumbs.Item>
</Breadcrumbs>
</div>
)

// Wrapper components to test that BreadcrumbsItem works when wrapped
const StyledWrapper = ({children}: {children: React.ReactNode}) => (
<span style={{padding: '2px', border: '1px dotted #999'}}>{children}</span>
)

const ConditionalWrapper = ({children, condition}: {children: React.ReactNode; condition: boolean}) => {
return condition ? <strong>{children}</strong> : <>{children}</>
}

const DataAttributeWrapper = ({children}: {children: React.ReactNode}) => (
<span data-testid="wrapper" className="custom-wrapper">
{children}
</span>
)

export const WrappedBreadcrumbItemsWithOverflow = () => (
<Breadcrumbs overflow="menu">
<StyledWrapper>
<Breadcrumbs.Item href="#">Wrapped Home</Breadcrumbs.Item>
</StyledWrapper>
<ConditionalWrapper condition={false}>
<Breadcrumbs.Item href="#">Products</Breadcrumbs.Item>
</ConditionalWrapper>
<DataAttributeWrapper>
<Breadcrumbs.Item href="#">Category</Breadcrumbs.Item>
</DataAttributeWrapper>
<StyledWrapper>
<Breadcrumbs.Item href="#">Subcategory</Breadcrumbs.Item>
</StyledWrapper>
<ConditionalWrapper condition={true}>
<Breadcrumbs.Item href="#">Item</Breadcrumbs.Item>
</ConditionalWrapper>
<DataAttributeWrapper>
<Breadcrumbs.Item href="#">Details</Breadcrumbs.Item>
</DataAttributeWrapper>
<Breadcrumbs.Item href="#" selected>
Current Page
</Breadcrumbs.Item>
</Breadcrumbs>
)

export const WithEditableNameInput = () => (
<Breadcrumbs>
<Breadcrumbs.Item href="#">Home</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Documents</Breadcrumbs.Item>
<Breadcrumbs.Item href="#">Project Alpha</Breadcrumbs.Item>
<Breadcrumbs.Item>
<TextInput
defaultValue="Untitled Document"
size="small"
sx={{
minWidth: '120px',
maxWidth: '180px',
fontSize: 'inherit',
border: '1px dashed var(--borderColor-muted)',
'&:focus': {
border: '1px solid var(--borderColor-accent-emphasis)',
},
}}
aria-label="Edit document name"
/>
</Breadcrumbs.Item>
</Breadcrumbs>
)

export const DynamicChildren = () => {
const [items, setItems] = useState([
{id: 1, href: '#', name: 'Home'},
{id: 2, href: '#', name: 'Docs'},
{id: 3, href: '#', name: 'Components'},
])

const addItem = () => {
const newId = Math.max(...items.map(item => item.id)) + 1
const names = ['Advanced', 'Examples', 'Guides', 'API', 'Tutorials', 'Reference']
const randomName = names[Math.floor(Math.random() * names.length)]
setItems([...items, {id: newId, href: '#', name: `${randomName}-${newId}`}])
}

const removeItem = () => {
if (items.length > 1) {
setItems(items.slice(0, -1))
}
}

const addMultipleItems = () => {
const newItems = [
{id: Date.now() + 1, href: '#', name: 'Category'},
{id: Date.now() + 2, href: '#', name: 'Subcategory'},
{id: Date.now() + 3, href: '#', name: 'Item'},
{id: Date.now() + 4, href: '#', name: 'Details'},
{id: Date.now() + 5, href: '#', name: 'Specifications'},
]
setItems([...items, ...newItems])
}

const reset = () => {
setItems([
{id: 1, href: '#', name: 'Home'},
{id: 2, href: '#', name: 'Docs'},
{id: 3, href: '#', name: 'Components'},
])
}

return (
<div style={{display: 'flex', flexDirection: 'column', gap: '16px'}}>
<div style={{display: 'flex', gap: '8px', marginBottom: '16px'}}>
<button type="button" onClick={addItem} style={{padding: '4px 8px'}}>
Add Item
</button>
<button type="button" onClick={removeItem} style={{padding: '4px 8px'}}>
Remove Item
</button>
<button type="button" onClick={addMultipleItems} style={{padding: '4px 8px'}}>
Add Many Items
</button>
<button type="button" onClick={reset} style={{padding: '4px 8px'}}>
Reset
</button>
</div>

<div>
<h4 id="dynamic-breadcrumbs-heading" style={{margin: '0 0 8px 0'}}>
Dynamic breadcrumbs
</h4>
<Breadcrumbs overflow="menu-with-root">
{items.map((item, index) => (
<Breadcrumbs.Item key={item.id} href={item.href} selected={index === items.length - 1}>
{item.name}
</Breadcrumbs.Item>
))}
</Breadcrumbs>
</div>

<div style={{marginTop: '16px', fontSize: '12px'}}>
Current items: {items.length} | Try adding/removing items to see how overflow behavior changes
</div>
</div>
)
}
95 changes: 95 additions & 0 deletions packages/react/src/Breadcrumbs/Breadcrumbs.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.BreadcrumbsBase {
display: flex;
justify-content: space-between;
width: 100%;
}

.BreadcrumbsList {
Expand All @@ -9,6 +10,100 @@
margin-bottom: 0;
}

[data-overflow='menu'] .BreadcrumbsList,
[data-overflow='menu-with-root'] .BreadcrumbsList {
white-space: nowrap;
display: flex;
flex-direction: row;
}

.BreadcrumbsItem {
display: inline-grid;
grid-auto-flow: column;
align-items: center;
flex: 0 99999 auto;
min-width: auto;
font-size: var(--text-body-size-medium);
white-space: nowrap;
list-style: none;

&:first-child {
margin-left: 0;
}

&:last-child {
.ItemSeparator {
display: none;
}
}
}

[data-overflow='menu'] .Item,
[data-overflow='menu-with-root'] .Item {
display: inline-block;
font-size: var(--text-body-size-medium);
color: var(--fgColor-link);
text-decoration: none;
padding-inline: var(--base-size-6);
padding-block: var(--base-size-4);
border-radius: var(--borderRadius-medium);

&:hover {
background: var(--control-transparent-bgColor-hover);
}

&:focus {
box-shadow: none;
outline: 2px solid var(--focus-outlineColor, var(--color-accent-fg));
outline-offset: -2px;
}
}

.MenuSummary {
list-style: none;
display: inline-block;
outline: none;
height: 0.5rem;
}

.MenuSummary::-webkit-details-marker {
display: none;
}

.MenuDetails {
position: relative;
display: inline-block;
}

.MenuDetails summary {
list-style: none;
cursor: pointer;
outline: none;
}

.MenuDetails summary::-webkit-details-marker {
display: none;
}

.MenuOverlay {
position: absolute;
z-index: 1;
box-shadow: var(--shadow-resting-medium);
border-radius: var(--borderRadius-large);
background-color: var(--overlay-bgColor);
list-style: none;
width: max-content;
}

.ItemSeparator {
color: var(--fgColor-muted);
display: flex;
align-self: center;
justify-content: center;
white-space: nowrap;
user-select: none;
}

.ItemWrapper {
display: inline-block;
font-size: var(--text-body-size-medium);
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {Meta} from '@storybook/react-vite'
import React from 'react'
import type {ComponentProps} from '../utils/types'
import Breadcrumbs from './Breadcrumbs'

Expand Down
Loading
Loading