Skip to content
Draft
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/violet-horses-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react': minor
---

Update Breadcrumbs component to enable more SSR friendly rendering
2 changes: 1 addition & 1 deletion packages/doc-gen/src/__tests__/ts-utils.patterns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const directory = path.resolve(import.meta.dirname)
const FIXTURE_PATH = path.join(directory, 'fixtures')

describe('getPropTypeForComponent', () => {
it('extracts props for FunctionComponent', () => {
it('extracts props for FunctionComponent', {timeout: 10_000}, () => {
const info = parseTypeInfo(FIXTURE_PATH, 'FunctionComponent')
expect(info.props.foo).toMatchObject({name: 'foo', type: 'string', required: true})
expect(info.props.bar).toMatchObject({name: 'bar', type: 'number', required: false})
Expand Down
179 changes: 178 additions & 1 deletion packages/react/src/Breadcrumbs/Breadcrumbs.dev.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {useState} from 'react'
import Breadcrumbs from '.'
import Breadcrumbs, {useBreadcrumbsResponsive, BreadcrumbsOverflowMenu} from '.'
import TextInput from '../TextInput'
import classes from './Breadcrumbs.dev.stories.module.css'
import overflowClasses from './Breadcrumbs.module.css'

export default {
title: 'Components/Breadcrumbs/Dev',
Expand Down Expand Up @@ -153,3 +154,179 @@ export const WithEditableNameInput = () => (
</Breadcrumbs.Item>
</Breadcrumbs>
)

/**
* Demonstrates using `responsive={false}` to disable automatic overflow behavior.
* All items are rendered as-is without any overflow menu, regardless of container width.
*/
export const ResponsiveFalse = () => (
<div style={{display: 'flex', flexDirection: 'column', gap: '16px'}}>
<div>
<h4 style={{margin: '0 0 8px 0'}}>With responsive=false (no overflow menu)</h4>
<div style={{width: '300px', border: '1px solid #ccc', padding: '8px'}}>
<Breadcrumbs responsive={false} 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>
</div>

<div>
<h4 style={{margin: '0 0 8px 0'}}>With responsive=true (default, shows overflow menu)</h4>
<div style={{width: '300px', border: '1px solid #ccc', padding: '8px'}}>
<Breadcrumbs responsive={true} 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>
</div>
</div>
)

/**
* Demonstrates using the `useBreadcrumbsResponsive` hook for manual control.
* This enables SSR-friendly conditional rendering patterns.
*/
export const UseBreadcrumbsResponsiveHook = () => {
const children = [
<Breadcrumbs.Item key="home" href="#">
Home
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="category" href="#">
Category
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="subcategory" href="#">
Subcategory
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="product" href="#">
Product
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="details" href="#">
Details
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="specifications" href="#">
Specifications
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="reviews" href="#" selected>
Reviews
</Breadcrumbs.Item>,
]

const {visibleItems, menuItems, showRoot, rootItem, containerRef} = useBreadcrumbsResponsive({
children,
overflow: 'menu-with-root',
})

// This is a simplified example - in real usage, you would use the hook
// to conditionally render different layouts for mobile vs desktop

return (
<div style={{display: 'flex', flexDirection: 'column', gap: '24px'}}>
<div>
<h4 style={{margin: '0 0 8px 0'}}>Hook return values:</h4>
<pre style={{background: '#f6f8fa', padding: '12px', borderRadius: '6px', fontSize: '12px'}}>
{JSON.stringify(
{
visibleItemsCount: visibleItems.length,
menuItemsCount: menuItems.length,
showRoot,
hasRootItem: !!rootItem,
hasContainerRef: !!containerRef,
},
null,
2,
)}
</pre>
</div>

<div>
<h4 style={{margin: '0 0 8px 0'}}>Manual rendering using hook data:</h4>
<nav
ref={containerRef as React.RefObject<HTMLElement>}
aria-label="Breadcrumbs"
className={overflowClasses.BreadcrumbsBase}
data-overflow="menu-with-root"
>
<ol className={overflowClasses.BreadcrumbsList}>
{showRoot && rootItem && (
<li className={overflowClasses.BreadcrumbsItem}>
{rootItem}
<ItemSeparator />
</li>
)}
{menuItems.length > 0 && (
<li className={overflowClasses.BreadcrumbsItem}>
<BreadcrumbsOverflowMenu items={menuItems} aria-label={`${menuItems.length} more items`} />
<ItemSeparator />
</li>
)}
{visibleItems.map((item, index) => (
<li key={index} className={overflowClasses.BreadcrumbsItem}>
{item}
<ItemSeparator />
</li>
))}
</ol>
</nav>
</div>

<div style={{fontSize: '12px', color: '#57606a'}}>
<p>
The <code>useBreadcrumbsResponsive</code> hook allows you to get the computed visible/menu items and manually
render the breadcrumbs. This is useful for SSR scenarios where you want to conditionally render different
versions for mobile and desktop.
</p>
</div>
</div>
)
}

// Helper component for stories
const ItemSeparator = () => (
<span className={overflowClasses.ItemSeparator}>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M10.956 1.27994L6.06418 14.7201L5 14.7201L9.89181 1.27994L10.956 1.27994Z" fill="currentcolor" />
</svg>
</span>
)

/**
* Demonstrates using the BreadcrumbsOverflowMenu component standalone.
*/
export const StandaloneBreadcrumbsOverflowMenu = () => {
const items = [
<Breadcrumbs.Item key="1" href="#">
Home
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="2" href="#">
Products
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="3" href="#">
Category
</Breadcrumbs.Item>,
]

return (
<div style={{display: 'flex', flexDirection: 'column', gap: '16px'}}>
<h4 style={{margin: 0}}>BreadcrumbsOverflowMenu as a standalone component:</h4>
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
<span>Click the menu:</span>
<BreadcrumbsOverflowMenu items={items} aria-label="3 hidden breadcrumb items" />
</div>
<p style={{fontSize: '12px', color: '#57606a', margin: 0}}>
The <code>BreadcrumbsOverflowMenu</code> component can be used standalone when building custom breadcrumb
layouts with the <code>useBreadcrumbsResponsive</code> hook.
</p>
</div>
)
}
57 changes: 57 additions & 0 deletions packages/react/src/Breadcrumbs/Breadcrumbs.examples.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type {Meta} from '@storybook/react-vite'
import type React from 'react'
import type {ComponentProps} from '../utils/types'
import Breadcrumbs from './Breadcrumbs'
import {BreadcrumbsOverflowMenu} from './BreadcrumbsOverflowMenu'
import {FeatureFlags} from '../FeatureFlags'

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

export const ExternallyControlled = () => (
<Breadcrumbs responsive={false}>
<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 WithManualOverflow = () => (
<FeatureFlags flags={{primer_react_breadcrumbs_overflow_menu: true}}>
<Breadcrumbs responsive={false} overflow="menu">
<BreadcrumbsOverflowMenu
items={[
<Breadcrumbs.Item key="Home" href="#">
Home
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="Products" href="#">
Products
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="Category" href="#">
Category
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="Subcategory" href="#">
Subcategory
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="Item" href="#">
Item
</Breadcrumbs.Item>,
<Breadcrumbs.Item key="Details" href="#">
Details
</Breadcrumbs.Item>,
]}
/>
<Breadcrumbs.Item href="#" selected>
Current Page
</Breadcrumbs.Item>
</Breadcrumbs>
</FeatureFlags>
)
5 changes: 5 additions & 0 deletions packages/react/src/Breadcrumbs/Breadcrumbs.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
display: flex;
flex-direction: row;
}

/* Prevent overflow during SSR before JS calculates the overflow menu (responsive mode only) */
&[data-responsive='true'] {
overflow: hidden;
}
}

.ItemSeparator {
Expand Down
Loading
Loading