diff --git a/.changeset/eighty-queens-tap.md b/.changeset/eighty-queens-tap.md
new file mode 100644
index 00000000000..6d5648d6aae
--- /dev/null
+++ b/.changeset/eighty-queens-tap.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": patch
+---
+
+Breadcrumbs : Add overflow menu for responsive behavior
diff --git a/.changeset/hot-bears-cry.md b/.changeset/hot-bears-cry.md
new file mode 100644
index 00000000000..b675e41c057
--- /dev/null
+++ b/.changeset/hot-bears-cry.md
@@ -0,0 +1,5 @@
+---
+"@primer/react": patch
+---
+
+Breadcrumb overflow styling
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-colorblind-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-colorblind-focus-linux.png
index 80bf84cd927..abac0d357a0 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-colorblind-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-colorblind-focus-linux.png differ
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-dimmed-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-dimmed-focus-linux.png
index aa6bbbcfb2f..7b9d82a8513 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-dimmed-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-dimmed-focus-linux.png differ
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-focus-linux.png
index 80bf84cd927..abac0d357a0 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-focus-linux.png differ
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-high-contrast-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-high-contrast-focus-linux.png
index e8c5b1e5c32..92992ec726a 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-high-contrast-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-high-contrast-focus-linux.png differ
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-tritanopia-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-tritanopia-focus-linux.png
index 80bf84cd927..abac0d357a0 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-tritanopia-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-dark-tritanopia-focus-linux.png differ
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-colorblind-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-colorblind-focus-linux.png
index a95ed49f6ea..28540b3c2d3 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-colorblind-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-colorblind-focus-linux.png differ
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-focus-linux.png
index 7f5a8125dfd..28540b3c2d3 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-focus-linux.png differ
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-high-contrast-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-high-contrast-focus-linux.png
index 29efc80181a..5e6d66f8b8f 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-high-contrast-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-high-contrast-focus-linux.png differ
diff --git a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-tritanopia-focus-linux.png b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-tritanopia-focus-linux.png
index a95ed49f6ea..28540b3c2d3 100644
Binary files a/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-tritanopia-focus-linux.png and b/.playwright/snapshots/components/Breadcrumbs.test.ts-snapshots/Breadcrumbs-Default-light-tritanopia-focus-linux.png differ
diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.dev.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.dev.stories.tsx
new file mode 100644
index 00000000000..d122887dc6c
--- /dev/null
+++ b/packages/react/src/Breadcrumbs/Breadcrumbs.dev.stories.tsx
@@ -0,0 +1,162 @@
+import {useState} from 'react'
+import Breadcrumbs from '.'
+import TextInput from '../TextInput'
+
+export default {
+ title: 'Components/Breadcrumbs/Dev',
+}
+
+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 (
+
+
+
+ Add Item
+
+
+ Remove Item
+
+
+ Add Many Items
+
+
+ Reset
+
+
+
+
+
+ Dynamic breadcrumbs
+
+
+ {items.map((item, index) => (
+
+ {item.name}
+
+ ))}
+
+
+
+
+ Current items: {items.length} | Try adding/removing items to see how overflow behavior changes
+
+
+ )
+}
+
+export const OverflowMenuNarrowContainer = () => (
+
+
+ Home
+ Products
+ Category
+ Subcategory
+
+ Current Page
+
+
+
+)
+
+// Wrapper components to test that BreadcrumbsItem works when wrapped
+const StyledWrapper = ({children}: {children: React.ReactNode}) => (
+ {children}
+)
+
+const ConditionalWrapper = ({children, condition}: {children: React.ReactNode; condition: boolean}) => {
+ return condition ? {children} : <>{children}>
+}
+
+const DataAttributeWrapper = ({children}: {children: React.ReactNode}) => (
+
+ {children}
+
+)
+
+export const WrappedBreadcrumbItemsWithOverflow = () => (
+
+
+ Wrapped Home
+
+
+ Products
+
+
+ Category
+
+
+ Subcategory
+
+
+ Item
+
+
+ Details
+
+
+ Current Page
+
+
+)
+
+export const WithEditableNameInput = () => (
+
+ Home
+ Documents
+ Project Alpha
+
+
+
+
+)
diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.docs.json b/packages/react/src/Breadcrumbs/Breadcrumbs.docs.json
index ce0747151d6..41321620375 100644
--- a/packages/react/src/Breadcrumbs/Breadcrumbs.docs.json
+++ b/packages/react/src/Breadcrumbs/Breadcrumbs.docs.json
@@ -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": [
@@ -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",
@@ -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"
diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx
new file mode 100644
index 00000000000..1e8d695d3a4
--- /dev/null
+++ b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx
@@ -0,0 +1,112 @@
+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',
+ component: Breadcrumbs,
+} as Meta>
+
+export const OverflowWrap = () => (
+
+ Home
+ Products
+ Category
+ Subcategory
+ Item
+ Details
+
+ Current Page
+
+
+)
+
+export const OverflowMenuFeatureFlagEnabled = () => (
+
+
+ Home
+ Products
+ Category
+ Subcategory
+ Item
+ Details
+
+ Current Page
+
+
+
+)
+
+export const OverflowMenuFeatureFlagDisabled = () => (
+
+ Home
+ Products
+ Category
+ Subcategory
+ Item
+ Details
+
+ Current Page
+
+
+)
+
+export const OverflowMenuShowRootFeatureFlagDisabled = () => (
+
+ github
+ Teams
+ Engineering
+ core-productivity
+ collaboration-workflows-flex
+
+ global-navigation-reviewers
+
+
+)
+
+export const OverflowMenuShowRootFeatureFlagEnabled = () => (
+
+
+ github
+ Teams
+ Engineering
+ core-productivity
+ collaboration-workflows-flex
+
+ global-navigation-reviewers
+
+
+
+)
+
+export const SpaciousVariantWithOverflowMenu = () => (
+
+
+ Home
+ Products
+ Category
+ Subcategory
+ Item
+ Details
+
+ Current Page
+
+
+
+)
+
+export const SpaciousVariantWithOverflowWrap = () => (
+
+ Home
+ Products
+ Category
+ Subcategory
+ Item
+ Details
+
+ Current Page
+
+
+)
diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css
index 96c61e7072c..b4d53bfef01 100644
--- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css
+++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css
@@ -1,6 +1,7 @@
.BreadcrumbsBase {
display: flex;
justify-content: space-between;
+ width: 100%;
}
.BreadcrumbsList {
@@ -9,10 +10,27 @@
margin-bottom: 0;
}
+[data-overflow='menu'],
+[data-overflow='menu-with-root'] {
+ & .BreadcrumbsList {
+ white-space: nowrap;
+ display: flex;
+ flex-direction: row;
+ }
+}
+
+.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);
- white-space: nowrap;
list-style: none;
&::after {
@@ -41,20 +59,105 @@
.Item {
display: inline-block;
font-size: var(--text-body-size-medium);
- color: var(--fgColor-link);
- text-decoration: none;
- &:hover,
- &:focus {
- text-decoration: underline;
+ &:focus-visible {
+ @mixin focusOutline 1px;
}
}
-.ItemSelected {
- color: var(--fgColor-default);
- pointer-events: none;
+[data-variant='normal'] {
+ & .Item {
+ color: var(--fgColor-link);
+ text-decoration: none;
+
+ &:not([aria-current]) {
+ &:hover {
+ text-decoration: underline;
+ }
+ }
- &:focus {
+ &:focus-visible {
+ text-decoration: none;
+ }
+
+ &[aria-current] {
+ color: var(--fgColor-default);
+ }
+ }
+}
+
+[data-variant='spacious'] {
+ & .Item {
+ color: var(--fgColor-default);
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);
+ text-decoration: none;
+ }
+
+ &[aria-current] {
+ font-weight: var(--base-text-weight-semibold);
+ }
+ }
+}
+
+.BreadcrumbsItem {
+ display: inline-grid;
+ grid-auto-flow: column;
+ align-items: center;
+ flex: 0 99999 auto;
+ min-width: auto;
+ white-space: nowrap;
+ list-style: none;
+
+ /* allow menu items to wrap line */
+ &:has(.MenuOverlay) {
+ white-space: normal;
+ }
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ .ItemSeparator {
+ display: none;
+ }
+ }
+
+ .MenuDetails {
+ position: relative;
+ display: inline-block;
+
+ & summary {
+ list-style: none;
+ cursor: pointer;
+
+ &::-webkit-details-marker {
+ display: none;
+ }
+ }
+ }
+
+ .MenuOverlay {
+ position: absolute;
+ z-index: 1;
+ box-shadow: var(--shadow-floating-small);
+ border-radius: var(--borderRadius-large);
+ background-color: var(--overlay-bgColor);
+ min-width: var(--overlay-width-xsmall);
+ max-height: 100vh;
+ max-width: var(--overlay-width-small);
+ overflow: hidden;
+ /* stylelint-disable-next-line primer/spacing */
+ top: calc(var(--overlay-offset) + var(--control-small-size));
+
+ @media (prefers-reduced-motion: no-preference) {
+ animation: overlay-appear 200ms cubic-bezier(0.33, 1, 0.68, 1);
+ }
}
}
diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx
index 16be2bd7bff..44e6b6ce1f7 100644
--- a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx
+++ b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx
@@ -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'
diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx
index fe47e2307ec..72cf11d6301 100644
--- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx
+++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx
@@ -1,17 +1,41 @@
import {clsx} from 'clsx'
import type {To} from 'history'
-import React from 'react'
+import React, {useState, useRef, useCallback, useEffect, useMemo} from 'react'
import type {SxProp} from '../sx'
import type {ComponentProps} from '../utils/types'
import classes from './Breadcrumbs.module.css'
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {BoxWithFallback} from '../internal/components/BoxWithFallback'
-
-const SELECTED_CLASS = 'selected'
+import Details from '../Details'
+import {ActionList} from '../ActionList'
+import {IconButton} from '../Button/IconButton'
+import {Tooltip} from '../TooltipV2'
+import {KebabHorizontalIcon} from '@primer/octicons-react'
+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'
export type BreadcrumbsProps = React.PropsWithChildren<
{
+ /**
+ * Optional class name for the breadcrumbs container.
+ */
className?: string
+ /**
+ * Controls the overflow behavior of the breadcrumbs.
+ * By default all overflowing crumbs will "wrap" in the given space taking up extra height.
+ * In the "menu" option we'll see the overflowing crumbs as part of a menu like dropdown instead of the root breadcrumb.
+ * In "menu-with-root" we see that instead of the root, the menu button will take the place of the next breadcrumb.
+ */
+ overflow?: 'wrap' | 'menu' | 'menu-with-root'
+ /**
+ * Controls the visual variant of the breadcrumbs.
+ * By default, the breadcrumbs will have a normal appearance.
+ * In the "spacious" option, the breadcrumbs will have increased padding and a more relaxed layout.
+ */
+ variant?: 'normal' | 'spacious'
} & SxProp
>
@@ -19,15 +43,332 @@ const BreadcrumbsList = ({children}: React.PropsWithChildren) => {
return {children}
}
-function Breadcrumbs({className, children, sx: sxProp}: BreadcrumbsProps) {
+type BreadcrumbsMenuItemProps = {
+ items: React.ReactElement[]
+ 'aria-label'?: string
+}
+
+const BreadcrumbsMenuItem = React.forwardRef(
+ ({items, 'aria-label': ariaLabel, ...rest}, menuRefCallback) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const detailsRef = useRef(null)
+ const menuButtonRef = useRef(null)
+ const menuContainerRef = useRef(null)
+ const detailsRefCallback = useCallback(
+ (element: HTMLDetailsElement | null) => {
+ detailsRef.current = element
+ if (typeof menuRefCallback === 'function') {
+ menuRefCallback(element)
+ }
+ },
+ [menuRefCallback],
+ )
+ const handleSummaryClick = useCallback((event: React.MouseEvent) => {
+ // Prevent the button click from bubbling up and interfering with details toggle
+ event.preventDefault()
+ // Manually toggle the details element
+ if (detailsRef.current) {
+ const newOpenState = !detailsRef.current.open
+ detailsRef.current.open = newOpenState
+ setIsOpen(newOpenState)
+ }
+ }, [])
+
+ const closeOverlay = useCallback(() => {
+ if (detailsRef.current) {
+ detailsRef.current.open = false
+ setIsOpen(false)
+ }
+ }, [])
+
+ const focusOnMenuButton = useCallback(() => {
+ menuButtonRef.current?.focus()
+ }, [])
+
+ useOnEscapePress(
+ (event: KeyboardEvent) => {
+ if (isOpen) {
+ event.preventDefault()
+ closeOverlay()
+ focusOnMenuButton()
+ }
+ },
+ [isOpen],
+ )
+
+ useOnOutsideClick({
+ onClickOutside: closeOverlay,
+ containerRef: menuContainerRef,
+ ignoreClickRefs: [menuButtonRef],
+ })
+
+ return (
+
+
+
+
+
+
+ {items.map((item, index) => {
+ const href = item.props.href
+ const children = item.props.children
+ const selected = item.props.selected
+ return (
+
+ {children}
+
+ )
+ })}
+
+
+
+ )
+ },
+)
+
+BreadcrumbsMenuItem.displayName = 'Breadcrumbs.MenuItem'
+
+const getValidChildren = (children: React.ReactNode) => {
+ return React.Children.toArray(children).filter(child => React.isValidElement(child)) as React.ReactElement[]
+}
+
+function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', variant = 'normal'}: BreadcrumbsProps) {
+ const overflowMenuEnabled = useFeatureFlag('primer_react_breadcrumbs_overflow_menu')
const wrappedChildren = React.Children.map(children, child => {child} )
- return (
-
+ const containerRef = useRef(null)
+
+ const measureMenuButton = useCallback((element: HTMLDetailsElement | null) => {
+ if (element) {
+ const iconButtonElement = element.querySelector('button[data-component="IconButton"]')
+ if (iconButtonElement) {
+ const measuredWidth = (iconButtonElement as HTMLElement).offsetWidth
+ setMenuButtonWidth(measuredWidth)
+ }
+ }
+ }, [])
+
+ const hideRoot = !(overflow === 'menu-with-root')
+ const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot)
+ const childArray = useMemo(() => getValidChildren(children), [children])
+
+ const rootItem = childArray[0]
+
+ const [visibleItems, setVisibleItems] = useState(() => childArray)
+ const [childArrayWidths, setChildArrayWidths] = useState(() => [])
+
+ const [menuItems, setMenuItems] = useState([])
+ const [rootItemWidth, setRootItemWidth] = useState(0)
+
+ const MENU_BUTTON_FALLBACK_WIDTH = 32 // Design system small IconButton
+ const [menuButtonWidth, setMenuButtonWidth] = useState(MENU_BUTTON_FALLBACK_WIDTH)
+
+ // if (typeof window !== 'undefined') {
+ // effectiveOverflow = overflow
+ // }
+
+ useEffect(() => {
+ const listElement = containerRef.current?.querySelector('ol')
+ if (
+ overflowMenuEnabled &&
+ 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])
+
+ const calculateOverflow = useCallback(
+ (availableWidth: number) => {
+ let eHideRoot = effectiveHideRoot
+ const MENU_BUTTON_WIDTH = menuButtonWidth
+ const MIN_VISIBLE_ITEMS = !eHideRoot ? 3 : 4
+
+ const calculateVisibleItemsWidth = (w: number[]) => {
+ const widths = w.reduce((sum, width) => sum + width + 16, 0)
+ return !eHideRoot ? rootItemWidth + widths : widths
+ }
+
+ let currentVisibleItems = [...childArray]
+ let currentVisibleItemWidths = [...childArrayWidths]
+ let currentMenuItems: React.ReactElement[] = []
+ let currentMenuItemsWidths: number[] = []
+
+ if (availableWidth > 0 && currentVisibleItemWidths.length > 0) {
+ let visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths)
+
+ if (currentMenuItems.length > 0) {
+ visibleItemsWidthTotal += MENU_BUTTON_WIDTH
+ }
+ while (
+ (overflow === 'menu' || overflow === 'menu-with-root') &&
+ (visibleItemsWidthTotal > availableWidth || currentVisibleItems.length > MIN_VISIBLE_ITEMS)
+ ) {
+ const itemToHide = currentVisibleItems[0]
+ const itemToHideWidth = currentVisibleItemWidths[0]
+ currentMenuItems = [...currentMenuItems, itemToHide]
+ currentMenuItemsWidths = [...currentMenuItemsWidths, itemToHideWidth]
+ currentVisibleItems = currentVisibleItems.slice(1)
+ currentVisibleItemWidths = currentVisibleItemWidths.slice(1)
+
+ visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths)
+
+ if (currentMenuItems.length > 0) {
+ visibleItemsWidthTotal += MENU_BUTTON_WIDTH
+ }
+
+ if (currentVisibleItems.length === 1 && visibleItemsWidthTotal > availableWidth) {
+ eHideRoot = true
+ break
+ } else {
+ eHideRoot = hideRoot
+ }
+ }
+ }
+ return {
+ visibleItems: currentVisibleItems,
+ menuItems: currentMenuItems,
+ effectiveHideRoot: eHideRoot,
+ }
+ },
+ [childArray, childArrayWidths, effectiveHideRoot, hideRoot, overflow, rootItemWidth, menuButtonWidth],
+ )
+
+ const handleResize = useCallback(
+ (entries: ResizeObserverEntry[]) => {
+ if (overflowMenuEnabled && entries[0]) {
+ const containerWidth = entries[0].contentRect.width
+ const result = calculateOverflow(containerWidth)
+ if (
+ (visibleItems.length !== result.visibleItems.length && menuItems.length !== result.menuItems.length) ||
+ result.effectiveHideRoot !== effectiveHideRoot
+ ) {
+ setVisibleItems(result.visibleItems)
+ setMenuItems(result.menuItems)
+ setEffectiveHideRoot(result.effectiveHideRoot)
+ }
+ }
+ },
+ [calculateOverflow, effectiveHideRoot, menuItems.length, overflowMenuEnabled, visibleItems.length],
+ )
+
+ useResizeObserver(handleResize, containerRef)
+
+ useEffect(() => {
+ if (
+ overflowMenuEnabled &&
+ (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])
+
+ const finalChildren = React.useMemo(() => {
+ if (overflowMenuEnabled) {
+ if (overflow === 'wrap' || menuItems.length === 0) {
+ return React.Children.map(children, child => {child} )
+ }
+
+ 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 = (
+
+
+
+
+ )
+
+ const visibleElements = visibleItems.map((child, index) => (
+
+ {child}
+
+
+ ))
+
+ const rootElement = (
+
+ {rootItem}
+
+
+ )
+
+ if (effectiveHideRoot) {
+ // Show: [overflow menu, leaf breadcrumb]
+ return [menuElement, ...visibleElements]
+ } else {
+ // Show: [root breadcrumb, overflow menu, leaf breadcrumb]
+ return [rootElement, menuElement, ...visibleElements]
+ }
+ }
+ }, [overflowMenuEnabled, overflow, menuItems, effectiveHideRoot, measureMenuButton, visibleItems, rootItem, children])
+
+ return overflowMenuEnabled ? (
+
+ {finalChildren}
+
+ ) : (
+
{wrappedChildren}
)
}
+const ItemSeparator = () => {
+ return (
+
+
+
+
+
+ )
+}
+
type StyledBreadcrumbsItemProps = {
to?: To
selected?: boolean
@@ -40,10 +381,7 @@ const BreadcrumbsItem = React.forwardRef(({selected, className, ...rest}, ref) =
return (
{
- it('should support `className` on the outermost element', () => {
- expect(HTMLRender( ).container.firstChild).toHaveClass(
- 'test-class-name',
- )
- })
+// Helper function to render with theme and feature flags
+const renderWithTheme = (component: React.ReactElement, flags?: Record) => {
+ const wrappedComponent = flags ? (
+
+ {component}
+
+ ) : (
+ {component}
+ )
+ return HTMLRender(wrappedComponent)
+}
+
+// Mock ResizeObserver for tests
+const mockObserve = vi.fn()
+const mockUnobserve = vi.fn()
+const mockDisconnect = vi.fn()
+globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({
+ observe: mockObserve,
+ unobserve: mockUnobserve,
+ disconnect: mockDisconnect,
+}))
+
+describe('Breadcrumbs', () => {
it('renders a ', () => {
const {container} = HTMLRender( )
expect(container.firstChild?.nodeName).toEqual('NAV')
})
+
+ it('renders breadcrumb items correctly', () => {
+ HTMLRender(
+
+ Home
+ Docs
+
+ Components
+
+ ,
+ )
+
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Docs')).toBeInTheDocument()
+ expect(screen.getByText('Components')).toBeInTheDocument()
+ })
+
+ it('sets aria-current="page" on selected item', () => {
+ HTMLRender(
+
+ Home
+
+ Docs
+
+ ,
+ )
+
+ const selectedItem = screen.getByText('Docs')
+ expect(selectedItem).toHaveAttribute('aria-current', 'page')
+ })
+
+ it('sets data-overflow attribute when overflow is menu with feature flag', () => {
+ const {container} = renderWithTheme(
+
+ Home
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ expect(container.firstChild).toHaveAttribute('data-overflow', 'menu')
+ })
+
+ it('sets data-overflow attribute when overflow is wrap', () => {
+ const {container} = renderWithTheme(
+
+ Home
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ expect(container.firstChild).toHaveAttribute('data-overflow', 'wrap')
+ })
+
+ it('renders all items when overflow is wrap', () => {
+ renderWithTheme(
+
+ Item 1
+ Item 2
+ Item 3
+ Item 4
+ Item 5
+ Item 6
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ // All items should be visible in wrap mode
+ expect(screen.getByText('Item 1')).toBeInTheDocument()
+ expect(screen.getByText('Item 2')).toBeInTheDocument()
+ expect(screen.getByText('Item 3')).toBeInTheDocument()
+ expect(screen.getByText('Item 4')).toBeInTheDocument()
+ expect(screen.getByText('Item 5')).toBeInTheDocument()
+ expect(screen.getByText('Item 6')).toBeInTheDocument()
+ })
+
+ it('shows overflow menu when more than 5 items in menu mode', () => {
+ renderWithTheme(
+
+ Item 1
+ Item 2
+ Item 3
+ Item 4
+ Item 5
+ Item 6
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ // Should have overflow menu button
+ expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument()
+
+ // Last 4 items should be visible
+ expect(screen.getByText('Item 3')).toBeInTheDocument()
+ expect(screen.getByText('Item 4')).toBeInTheDocument()
+ expect(screen.getByText('Item 5')).toBeInTheDocument()
+ expect(screen.getByText('Item 6')).toBeInTheDocument()
+ })
+
+ it('show root in menu', () => {
+ expect(() => {
+ renderWithTheme(
+
+ Home
+ Docs
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+ }).not.toThrow()
+ })
+
+ it('includes root item in overflow menu when overflow is menu-with-root', async () => {
+ const user = userEvent.setup()
+
+ renderWithTheme(
+
+ Home
+ Category
+ Subcategory
+ Product
+ Details
+ Specifications
+
+ Reviews
+
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ // Should have overflow menu button
+ const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i})
+ expect(menuButton).toBeInTheDocument()
+
+ // Open the overflow menu
+ await user.click(menuButton)
+
+ // Find the element that contains the overflow menu
+ const detailsEl = menuButton.closest('details') as HTMLElement | null
+ expect(detailsEl).not.toBeNull()
+ const detailsScope = within(detailsEl!)
+
+ await waitFor(() => {
+ expect(screen.getByRole('link', {name: 'Home'})).toBeInTheDocument()
+ })
+
+ // These links should be inside the details (overflow) content
+ expect(detailsScope.getByRole('link', {name: 'Category'})).toBeInTheDocument()
+ expect(detailsScope.getByRole('link', {name: 'Subcategory'})).toBeInTheDocument()
+ expect(detailsScope.getByRole('link', {name: 'Product'})).toBeInTheDocument()
+
+ // Verify that the last few items are visible as regular breadcrumb items
+ expect(screen.getByRole('link', {name: 'Details'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Specifications'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Reviews'})).toBeInTheDocument()
+
+ // Verify the selected item (Reviews) has aria-current in the visible breadcrumb
+ const selectedBreadcrumb = screen.getByRole('link', {name: 'Reviews'})
+ expect(selectedBreadcrumb).toHaveAttribute('aria-current', 'page')
+ })
+
+ it('renders accessible overflow menu', () => {
+ renderWithTheme(
+
+ Item 1
+ Item 2
+ Item 3
+ Item 4
+ Item 5
+ Item 6
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i})
+ expect(menuButton).toHaveAttribute('aria-expanded', 'false')
+ })
+
+ it('shows overflow menu during resize when items exceed container width', () => {
+ let resizeCallback: ((entries: ResizeObserverEntry[]) => void) | undefined
+
+ const mockResizeObserver = vi.fn().mockImplementation(callback => {
+ resizeCallback = callback
+ return {
+ observe: mockObserve,
+ unobserve: mockUnobserve,
+ disconnect: mockDisconnect,
+ }
+ })
+ globalThis.ResizeObserver = mockResizeObserver
+
+ renderWithTheme(
+
+ Item 1
+ Item 2
+ Item 3
+ Item 4
+ Item 5
+ Item 6
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ expect(resizeCallback).toBeDefined()
+
+ // Initially should show overflow menu for >5 items
+ expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument()
+
+ // Simulate a wide container resize
+ if (resizeCallback) {
+ resizeCallback([
+ {
+ contentRect: {width: 800, height: 40},
+ } as ResizeObserverEntry,
+ ])
+ }
+
+ // Should still have overflow menu for 6 items (>5 rule)
+ expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument()
+
+ // Simulate a narrow container resize
+ if (resizeCallback) {
+ resizeCallback([
+ {
+ contentRect: {width: 250, height: 40},
+ } as ResizeObserverEntry,
+ ])
+ }
+
+ // Should maintain overflow menu for narrow container
+ expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument()
+
+ // Verify the navigation element is still present after resizes
+ expect(screen.getByRole('navigation')).toBeInTheDocument()
+ })
+
+ it('correctly populates overflow menu during resize events', async () => {
+ let resizeCallback: ((entries: ResizeObserverEntry[]) => void) | undefined
+
+ const mockResizeObserver = vi.fn().mockImplementation(callback => {
+ resizeCallback = callback
+ return {
+ observe: mockObserve,
+ unobserve: mockUnobserve,
+ disconnect: mockDisconnect,
+ }
+ })
+ globalThis.ResizeObserver = mockResizeObserver
+
+ const user = userEvent.setup()
+
+ renderWithTheme(
+
+ Home
+ Category
+ Subcategory
+ Product
+ Details
+ Specifications
+ Reviews
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ expect(resizeCallback).toBeDefined()
+
+ // Initially should show overflow menu for >5 items
+ const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i})
+ expect(menuButton).toBeInTheDocument()
+
+ // Open the overflow menu
+ await user.click(menuButton)
+
+ // Verify menu items are present (first 3 items should be in overflow for 7 total items)
+ await waitFor(() => {
+ expect(screen.getByRole('link', {name: 'Home'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Category'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Subcategory'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Product'})).toBeInTheDocument()
+ })
+
+ // Verify that the last 4 items are visible as regular breadcrumb items (not in menu)
+ expect(screen.getByRole('link', {name: 'Details'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Specifications'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Reviews'})).toBeInTheDocument()
+
+ // Close menu by clicking outside
+ await user.click(document.body)
+ await waitFor(() => {
+ expect
+ })
+
+ // Simulate a very narrow container resize that would affect overflow calculation
+ if (resizeCallback) {
+ resizeCallback([
+ {
+ contentRect: {width: 200, height: 40},
+ } as ResizeObserverEntry,
+ ])
+ }
+
+ // Menu button should still be present
+ expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument()
+
+ // Simulate a very wide container resize
+ if (resizeCallback) {
+ resizeCallback([
+ {
+ contentRect: {width: 1200, height: 40},
+ } as ResizeObserverEntry,
+ ])
+ }
+
+ // Menu button should still be present (7 items > 5)
+ expect(screen.getByRole('button', {name: /more breadcrumb items/i})).toBeInTheDocument()
+
+ // Open menu again to verify it still works after resize
+ await user.click(screen.getByRole('button', {name: /more breadcrumb items/i}))
+
+ // Verify menu still contains expected items (first 3 items)
+ await waitFor(() => {
+ expect(screen.getByRole('link', {name: 'Home'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Category'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Subcategory'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Product'})).toBeInTheDocument()
+ })
+
+ // Verify visible breadcrumb items are still accessible (last 4 items)
+ expect(screen.getByRole('link', {name: 'Details'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Specifications'})).toBeInTheDocument()
+ expect(screen.getByRole('link', {name: 'Reviews'})).toBeInTheDocument()
+ })
+
+ describe('BreadcrumbsMenuItem interactions', () => {
+ it('closes menu on Escape key press', async () => {
+ const user = userEvent.setup()
+
+ renderWithTheme(
+
+ Home
+ Docs
+ Components
+ Breadcrumbs
+ Examples
+
+ Advanced
+
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ // Open the overflow menu
+ const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i})
+ // Initially collapsed
+ expect(menuButton).toHaveAttribute('aria-expanded', 'false')
+ await user.click(menuButton)
+
+ // Verify menu is open
+ await waitFor(() => {
+ expect(menuButton).toHaveAttribute('aria-expanded', 'true')
+ })
+
+ // Press Escape key
+ await user.keyboard('{Escape}') // sometimes tooltip swallows this escape
+
+ // Verify menu is closed
+ await waitFor(() => {
+ expect(menuButton).toHaveAttribute('aria-expanded', 'false')
+ })
+ })
+
+ it('closes menu when clicking outside', async () => {
+ const user = userEvent.setup()
+
+ renderWithTheme(
+
+
+ Outside Button
+
+
+ Home
+ Docs
+ Components
+ Breadcrumbs
+ Examples
+
+ Advanced
+
+
+
,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ // Open the overflow menu
+ const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i})
+ await user.click(menuButton)
+
+ // Verify menu is open
+ await waitFor(() => {
+ expect(screen.getByRole('link', {name: 'Home'})).toBeInTheDocument()
+ })
+
+ // Click outside element
+ const outsideButton = screen.getByTestId('outside-button')
+ await user.click(outsideButton)
+
+ // Verify menu is closed
+ await waitFor(() => {
+ expect(menuButton).toHaveAttribute('aria-expanded', 'false')
+ })
+ })
+
+ it('allows tab navigation through menu items', async () => {
+ const user = userEvent.setup()
+
+ renderWithTheme(
+
+ Home
+ Docs
+ Components
+ Breadcrumbs
+ Examples
+
+ Advanced
+
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ // Open the overflow menu
+ const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i})
+ await user.click(menuButton)
+
+ // Verify menu is open
+ await waitFor(() => {
+ expect(screen.getByRole('link', {name: 'Home'})).toBeInTheDocument()
+ })
+
+ // Tab through menu items
+ await user.keyboard('{Tab}')
+ const homeMenuItem = screen.getByRole('link', {name: 'Home'})
+ expect(homeMenuItem).toHaveFocus()
+
+ await user.keyboard('{Tab}')
+ const docsMenuItem = screen.getByRole('link', {name: 'Docs'})
+ expect(docsMenuItem).toHaveFocus()
+
+ await user.keyboard('{Tab}')
+ const componentsMenuItem = screen.getByRole('link', {name: 'Components'})
+ expect(componentsMenuItem).toHaveFocus()
+ })
+
+ it('maintains focus on menu button when menu is closed', async () => {
+ const user = userEvent.setup()
+
+ renderWithTheme(
+
+ Home
+ Docs
+ Components
+ Breadcrumbs
+ Examples
+
+ Advanced
+
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+
+ const menuButton = screen.getByRole('button', {name: /more breadcrumb items/i})
+
+ // Focus the menu button
+ menuButton.focus()
+ expect(menuButton).toHaveFocus()
+
+ // Open menu with Enter key
+ await user.keyboard('{Enter}')
+
+ // Verify menu is open
+ await waitFor(() => {
+ expect(screen.getByRole('link', {name: 'Home'})).toBeInTheDocument()
+ })
+
+ // Close with Escape
+ await user.keyboard('{Escape}')
+
+ // Verify focus returns to button
+ expect(menuButton).toHaveFocus()
+ })
+ })
+
+ describe('variant prop (feature flag on)', () => {
+ it('sets data-variant="normal" by default', () => {
+ const {container} = renderWithTheme(
+
+ Home
+
+ Docs
+
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+ expect(container.firstChild).toHaveAttribute('data-variant', 'normal')
+ })
+
+ it('sets data-variant="spacious" when variant prop provided', () => {
+ const {container} = renderWithTheme(
+
+ Home
+
+ Docs
+
+ ,
+ {primer_react_breadcrumbs_overflow_menu: true},
+ )
+ expect(container.firstChild).toHaveAttribute('data-variant', 'spacious')
+ })
+ })
})
diff --git a/packages/react/src/Breadcrumbs/__tests__/__snapshots__/BreadcrumbsItem.test.tsx.snap b/packages/react/src/Breadcrumbs/__tests__/__snapshots__/BreadcrumbsItem.test.tsx.snap
index 1fac9c17f5b..532e0315ca2 100644
--- a/packages/react/src/Breadcrumbs/__tests__/__snapshots__/BreadcrumbsItem.test.tsx.snap
+++ b/packages/react/src/Breadcrumbs/__tests__/__snapshots__/BreadcrumbsItem.test.tsx.snap
@@ -3,6 +3,6 @@
exports[`Breadcrumbs.Item > respects the "selected" prop 1`] = `
`;
diff --git a/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts b/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts
index 9a387d232bb..f60c9687834 100644
--- a/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts
+++ b/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts
@@ -2,6 +2,7 @@ import {FeatureFlagScope} from './FeatureFlagScope'
export const DefaultFeatureFlags = FeatureFlagScope.create({
primer_react_action_list_item_as_button: false,
+ primer_react_breadcrumbs_overflow_menu: false,
primer_react_overlay_overflow: false,
primer_react_segmented_control_tooltip: false,
primer_react_select_panel_fullscreen_on_narrow: false,
diff --git a/packages/react/src/__tests__/storybook.test.tsx b/packages/react/src/__tests__/storybook.test.tsx
index f8afa6c5691..06a109c9010 100644
--- a/packages/react/src/__tests__/storybook.test.tsx
+++ b/packages/react/src/__tests__/storybook.test.tsx
@@ -15,7 +15,6 @@ const allowlist = [
'Autocomplete',
'Avatar',
'AvatarStack',
- 'Breadcrumbs',
'BranchName',
'Blankslate',
'Box',