From a7208d425b4895d42e2585c0fb89ae1f2bc7030f Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 11 Aug 2025 17:06:41 +1000 Subject: [PATCH 01/46] Spike for breadcrumbs overflow --- .../src/Breadcrumbs/Breadcrumbs.module.css | 6 + .../src/Breadcrumbs/Breadcrumbs.stories.tsx | 116 ++++++++++ .../react/src/Breadcrumbs/Breadcrumbs.tsx | 200 +++++++++++++++++- 3 files changed, 317 insertions(+), 5 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 96c61e7072c..92f1c9c0ab3 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -9,6 +9,12 @@ margin-bottom: 0; } +/* Prevent wrapping when using menu overflow */ +[data-overflow='menu'] .BreadcrumbsList { + white-space: nowrap; + overflow: hidden; +} + .ItemWrapper { display: inline-block; font-size: var(--text-body-size-medium); diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx index 16be2bd7bff..af02fde2c1e 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx @@ -16,3 +16,119 @@ export const Default = () => ( ) + +export const OverflowWrap = () => ( + + Home + Products + Category + Subcategory + Item + Details + + Current Page + + +) + +export const OverflowMenu = () => ( + + Home + Products + Category + Subcategory + Item + Details + + Current Page + + +) + +export const OverflowMenuHideRoot = () => ( + + Home + Products + Category + Subcategory + Item + Details + + Current Page + + +) + +export const OverflowMenuShowRoot = () => ( + + Home + Products + Category + Subcategory + Item + Details + + Current Page + + +) + +export const OverflowMenuFewItems = () => ( + + Home + About + + Team + + +) + +export const OverflowMenuManyItems = () => ( + + Home + Level 1 + Level 2 + Level 3 + Level 4 + Level 5 + Level 6 + Level 7 + Level 8 + + Current Page + + +) + +export const OverflowMenuLongWords = () => ( + + SupercalifragilisticexpialidociousRepository + AnticonstitutionnellementConfiguration + PneumonoultramicroscopicsilicovolcanoconiosisDocumentation + + HippopotomonstrosesquippedaliophobiaCurrentPage + + +) + +export const OverflowMenuLongWordsHideRoot = () => ( + + SupercalifragilisticexpialidociousRepository + AnticonstitutionnellementConfiguration + PneumonoultramicroscopicsilicovolcanoconiosisDocumentation + + HippopotomonstrosesquippedaliophobiaCurrentPage + + +) + +export const OverflowMenuLongWordsShowRoot = () => ( + + SupercalifragilisticexpialidociousRepository + AnticonstitutionnellementConfiguration + PneumonoultramicroscopicsilicovolcanoconiosisDocumentation + + HippopotomonstrosesquippedaliophobiaCurrentPage + + +) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index fe47e2307ec..c42f55d1af1 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -1,17 +1,23 @@ import {clsx} from 'clsx' import type {To} from 'history' -import React from 'react' +import React, {useState, useRef, useCallback, useEffect} 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' +import {ActionMenu} from '../ActionMenu' +import {ActionList} from '../ActionList' +import {useResizeObserver} from '../hooks/useResizeObserver' +import type {ResizeObserverEntry} from '../hooks/useResizeObserver' const SELECTED_CLASS = 'selected' export type BreadcrumbsProps = React.PropsWithChildren< { className?: string + overflow?: 'wrap' | 'menu' + hideRoot?: boolean } & SxProp > @@ -19,11 +25,195 @@ const BreadcrumbsList = ({children}: React.PropsWithChildren) => { return
    {children}
} -function Breadcrumbs({className, children, sx: sxProp}: BreadcrumbsProps) { - const wrappedChildren = React.Children.map(children, child =>
  • {child}
  • ) +type BreadcrumbsMenuItemProps = { + items: React.ReactElement[] + 'aria-label'?: string +} + +const BreadcrumbsMenuItem = React.forwardRef( + ({items, 'aria-label': ariaLabel, ...rest}, ref) => { + 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' + +function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRoot = true}: BreadcrumbsProps) { + const containerRef = useRef(null) + const [containerWidth, setContainerWidth] = useState(0) + const [visibleItems, setVisibleItems] = useState([]) + const [menuItems, setMenuItems] = useState([]) + const [itemWidths, setItemWidths] = useState([]) + const previousWidthsRef = useRef('') + + const childArray = React.Children.toArray(children).filter(child => + React.isValidElement(child), + ) as React.ReactElement[] + + // Initialize visible items to show all items initially for measurement + useEffect(() => { + if (visibleItems.length === 0 && childArray.length > 0) { + setVisibleItems(childArray) + } + }, [childArray, visibleItems.length]) + + const handleResize = useCallback((entries: ResizeObserverEntry[]) => { + if (entries[0]) { + setContainerWidth(entries[0].contentRect.width) + } + }, []) + + useResizeObserver(handleResize, containerRef) + + // Calculate item widths from rendered items using parent container + useEffect(() => { + if (containerRef.current && overflow === 'menu') { + const listElement = containerRef.current.querySelector('ol') + if (listElement && listElement.children.length > 0) { + // Only measure widths when all original items are visible (no overflow menu yet) + if (listElement.children.length === childArray.length) { + const widths = Array.from(listElement.children).map(child => (child as HTMLElement).offsetWidth) + const widthsString = JSON.stringify(widths) + // Only update if widths have actually changed to prevent infinite loops + if (widthsString !== previousWidthsRef.current) { + previousWidthsRef.current = widthsString + setItemWidths(widths) + } + } + } + } + }, [childArray, overflow, visibleItems]) + + // Calculate which items are visible vs in menu + useEffect(() => { + if (overflow === 'wrap') { + setVisibleItems(childArray) + setMenuItems([]) + return + } + + // For 'menu' overflow mode + const lastItem = childArray[childArray.length - 1] // Leaf breadcrumb + const firstItem = childArray[0] // Root breadcrumb + + // First check: if more than 5 items, always use overflow + if (childArray.length > 5) { + if (hideRoot) { + // Show only overflow menu and leaf breadcrumb + const itemsToHide = childArray.slice(0, -1) // All except last + setMenuItems(itemsToHide) + setVisibleItems([lastItem]) + } else { + // Show root breadcrumb, overflow menu, and leaf breadcrumb + const itemsToHide = childArray.slice(1, -1) // All except first and last + setMenuItems(itemsToHide) + setVisibleItems([firstItem, lastItem]) + } + return + } + + // Second check: if we have measured widths and container width, check if items fit + if (containerWidth > 0 && itemWidths.length === childArray.length && itemWidths.length > 0) { + const totalItemsWidth = itemWidths.reduce((sum, width) => sum + width, 0) + // Add some buffer for the ellipsis menu button (approximately 50px) + const bufferWidth = 50 + + if (totalItemsWidth + bufferWidth > containerWidth) { + // Items don't fit, need to overflow + if (hideRoot) { + // Show only overflow menu and leaf breadcrumb + const itemsToHide = childArray.slice(0, -1) // All except last + setMenuItems(itemsToHide) + setVisibleItems([lastItem]) + } else { + // Show root breadcrumb, overflow menu, and leaf breadcrumb + const itemsToHide = childArray.slice(1, -1) // All except first and last + setMenuItems(itemsToHide) + setVisibleItems([firstItem, lastItem]) + } + return + } + } + + // No overflow needed - show all items + setVisibleItems(childArray) + setMenuItems([]) + }, [childArray, overflow, containerWidth, hideRoot, itemWidths]) + + // Determine final children to render + const finalChildren = React.useMemo(() => { + if (overflow === 'wrap' || menuItems.length === 0) { + return visibleItems.map(child => ( +
  • + {child} +
  • + )) + } + + // Create menu item and combine with visible items + const menuElement = ( +
  • + +
  • + ) + + const visibleElements = visibleItems.map(child => ( +
  • + {child} +
  • + )) + + // Position menu based on hideRoot setting and visible items + if (hideRoot) { + // Show: [overflow menu, leaf breadcrumb] + return [menuElement, ...visibleElements] + } else { + // Show: [root breadcrumb, overflow menu, leaf breadcrumb] + return [visibleElements[0], menuElement, ...visibleElements.slice(1)] + } + }, [overflow, menuItems, visibleItems, hideRoot]) + return ( - - {wrappedChildren} + + {finalChildren} ) } From 54c9148a38eeb4c4bb8cc6cda4d362437a497f18 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 11 Aug 2025 22:44:15 +1000 Subject: [PATCH 02/46] Add changeset for breadcrumbs overflow --- .changeset/good-cougars-hug.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/good-cougars-hug.md diff --git a/.changeset/good-cougars-hug.md b/.changeset/good-cougars-hug.md new file mode 100644 index 00000000000..27786935bdc --- /dev/null +++ b/.changeset/good-cougars-hug.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +Spike for breadcrumbs overflow From dc5dac1794ffa5476d391a7504a9768b56ed2d0c Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Tue, 12 Aug 2025 15:32:20 +1000 Subject: [PATCH 03/46] Add review comments and change behavior --- .../src/Breadcrumbs/Breadcrumbs.stories.tsx | 84 ++------- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 159 +++++++++++++----- 2 files changed, 126 insertions(+), 117 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx index af02fde2c1e..f0a6451f607 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx @@ -45,20 +45,6 @@ export const OverflowMenu = () => ( ) -export const OverflowMenuHideRoot = () => ( - - Home - Products - Category - Subcategory - Item - Details - - Current Page - - -) - export const OverflowMenuShowRoot = () => ( Home @@ -73,62 +59,16 @@ export const OverflowMenuShowRoot = () => ( ) -export const OverflowMenuFewItems = () => ( - - Home - About - - Team - - -) - -export const OverflowMenuManyItems = () => ( - - Home - Level 1 - Level 2 - Level 3 - Level 4 - Level 5 - Level 6 - Level 7 - Level 8 - - Current Page - - -) - -export const OverflowMenuLongWords = () => ( - - SupercalifragilisticexpialidociousRepository - AnticonstitutionnellementConfiguration - PneumonoultramicroscopicsilicovolcanoconiosisDocumentation - - HippopotomonstrosesquippedaliophobiaCurrentPage - - -) - -export const OverflowMenuLongWordsHideRoot = () => ( - - SupercalifragilisticexpialidociousRepository - AnticonstitutionnellementConfiguration - PneumonoultramicroscopicsilicovolcanoconiosisDocumentation - - HippopotomonstrosesquippedaliophobiaCurrentPage - - -) - -export const OverflowMenuLongWordsShowRoot = () => ( - - SupercalifragilisticexpialidociousRepository - AnticonstitutionnellementConfiguration - PneumonoultramicroscopicsilicovolcanoconiosisDocumentation - - HippopotomonstrosesquippedaliophobiaCurrentPage - - +export const OverflowMenuNarrowContainer = () => ( +
    + + Home + Products + Category + Subcategory + + Current Page + + +
    ) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index c42f55d1af1..13a8e0f9e51 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -36,15 +36,18 @@ const BreadcrumbsMenuItem = React.forwardRef - + {items.map((item, index) => { const href = item.props.href const children = item.props.children @@ -53,6 +56,7 @@ const BreadcrumbsMenuItem = React.forwardRef @@ -75,6 +79,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const [visibleItems, setVisibleItems] = useState([]) const [menuItems, setMenuItems] = useState([]) const [itemWidths, setItemWidths] = useState([]) + const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) const previousWidthsRef = useRef('') const childArray = React.Children.toArray(children).filter(child => @@ -120,56 +125,120 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo if (overflow === 'wrap') { setVisibleItems(childArray) setMenuItems([]) + setEffectiveHideRoot(hideRoot) return } // For 'menu' overflow mode - const lastItem = childArray[childArray.length - 1] // Leaf breadcrumb - const firstItem = childArray[0] // Root breadcrumb - - // First check: if more than 5 items, always use overflow - if (childArray.length > 5) { - if (hideRoot) { - // Show only overflow menu and leaf breadcrumb - const itemsToHide = childArray.slice(0, -1) // All except last - setMenuItems(itemsToHide) - setVisibleItems([lastItem]) - } else { - // Show root breadcrumb, overflow menu, and leaf breadcrumb - const itemsToHide = childArray.slice(1, -1) // All except first and last - setMenuItems(itemsToHide) - setVisibleItems([firstItem, lastItem]) + // Helper function to calculate visible items and menu items with progressive hiding + const calculateOverflow = (availableWidth: number) => { + const MENU_BUTTON_WIDTH = 50 // Approximate width of "..." button + + let currentVisibleItems = [...childArray] + let currentMenuItems: React.ReactElement[] = [] + + // If more than 5 items, start by reducing to 5 visible items (including menu) + if (childArray.length > 5) { + // Target: 4 visible items + 1 menu = 5 total + const itemsToHide = childArray.slice(0, childArray.length - 4) + currentMenuItems = itemsToHide + currentVisibleItems = childArray.slice(childArray.length - 4) } - return - } - // Second check: if we have measured widths and container width, check if items fit - if (containerWidth > 0 && itemWidths.length === childArray.length && itemWidths.length > 0) { - const totalItemsWidth = itemWidths.reduce((sum, width) => sum + width, 0) - // Add some buffer for the ellipsis menu button (approximately 50px) - const bufferWidth = 50 - - if (totalItemsWidth + bufferWidth > containerWidth) { - // Items don't fit, need to overflow - if (hideRoot) { - // Show only overflow menu and leaf breadcrumb - const itemsToHide = childArray.slice(0, -1) // All except last - setMenuItems(itemsToHide) - setVisibleItems([lastItem]) - } else { - // Show root breadcrumb, overflow menu, and leaf breadcrumb - const itemsToHide = childArray.slice(1, -1) // All except first and last - setMenuItems(itemsToHide) - setVisibleItems([firstItem, lastItem]) + // Now check if current visible items fit in available width + if (availableWidth > 0 && itemWidths.length === childArray.length) { + let visibleItemsWidthTotal = currentVisibleItems + .map(item => { + const index = childArray.findIndex(child => child.key === item.key) + return index !== -1 ? itemWidths[index] : 0 + }) + .reduce((sum, width) => sum + width, 0) + + // Add menu button width if we have hidden items + if (currentMenuItems.length > 0) { + visibleItemsWidthTotal += MENU_BUTTON_WIDTH + } + + // Progressive hiding: keep moving items to menu until they fit + let effectiveHideRoot = hideRoot + + while (visibleItemsWidthTotal > availableWidth && currentVisibleItems.length > 1) { + // Determine which item to hide based on hideRoot setting + let itemToHide: React.ReactElement + + if (effectiveHideRoot) { + // Hide from start when hideRoot is true + itemToHide = currentVisibleItems[0] + currentVisibleItems = currentVisibleItems.slice(1) + } else { + // Try to hide second item (keep root and leaf) when hideRoot is false + itemToHide = currentVisibleItems[1] + currentVisibleItems = [currentVisibleItems[0], ...currentVisibleItems.slice(2)] + } + + currentMenuItems = [itemToHide, ...currentMenuItems] + + // Recalculate width + visibleItemsWidthTotal = currentVisibleItems + .map(item => { + const index = childArray.findIndex(child => child.key === item.key) + return index !== -1 ? itemWidths[index] : 0 + }) + .reduce((sum, width) => sum + width, 0) + + // Add menu button width + if (currentMenuItems.length > 0) { + visibleItemsWidthTotal += MENU_BUTTON_WIDTH + } + + // If hideRoot is false but we still don't fit with root + menu + leaf, + // fallback to hideRoot=true behavior (menu + leaf only) + if ( + !hideRoot && + !effectiveHideRoot && + currentVisibleItems.length === 2 && + visibleItemsWidthTotal > availableWidth + ) { + effectiveHideRoot = true + // Move the root item to menu as well + const rootItem = currentVisibleItems[0] + currentVisibleItems = currentVisibleItems.slice(1) + currentMenuItems = [rootItem, ...currentMenuItems] + + // Recalculate width one more time + visibleItemsWidthTotal = currentVisibleItems + .map(item => { + const index = childArray.findIndex(child => child.key === item.key) + return index !== -1 ? itemWidths[index] : 0 + }) + .reduce((sum, width) => sum + width, 0) + + if (currentMenuItems.length > 0) { + visibleItemsWidthTotal += MENU_BUTTON_WIDTH + } + } + } + + // Final check: if even the leaf breadcrumb + menu doesn't fit, just show them anyway + // The CSS will handle truncation of the leaf breadcrumb + if (visibleItemsWidthTotal > availableWidth && currentVisibleItems.length === 1) { + // Keep the current configuration - CSS will handle truncation } - return + } + + return { + visibleItems: currentVisibleItems, + menuItems: currentMenuItems, + effectiveHideRoot, } } - // No overflow needed - show all items - setVisibleItems(childArray) - setMenuItems([]) - }, [childArray, overflow, containerWidth, hideRoot, itemWidths]) + // Apply the overflow calculation + const result = calculateOverflow(containerWidth) + setVisibleItems(result.visibleItems) + setMenuItems(result.menuItems) + setEffectiveHideRoot(result.effectiveHideRoot) + }, [childArray, overflow, containerWidth, hideRoot, itemWidths, effectiveHideRoot]) // Determine final children to render const finalChildren = React.useMemo(() => { @@ -194,15 +263,15 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo )) - // Position menu based on hideRoot setting and visible items - if (hideRoot) { + // Position menu based on effective hideRoot setting and visible items + if (effectiveHideRoot) { // Show: [overflow menu, leaf breadcrumb] return [menuElement, ...visibleElements] } else { // Show: [root breadcrumb, overflow menu, leaf breadcrumb] return [visibleElements[0], menuElement, ...visibleElements.slice(1)] } - }, [overflow, menuItems, visibleItems, hideRoot]) + }, [overflow, menuItems, visibleItems, effectiveHideRoot]) return ( Date: Wed, 13 Aug 2025 16:53:19 +1000 Subject: [PATCH 04/46] Fix up some issues. --- .../src/Breadcrumbs/Breadcrumbs.stories.tsx | 8 +-- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 67 ++++++------------- 2 files changed, 23 insertions(+), 52 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx index f0a6451f607..f16a3e74004 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx @@ -36,9 +36,9 @@ export const OverflowMenu = () => ( Home Products Category - Subcategory - Item - Details + SubcategorySubcategorySubcategorySubcategory + ItemItemItemItemItemItemItem + DetailsDetailsDetailsDetailsDetailsDetailsDetails Current Page @@ -60,7 +60,7 @@ export const OverflowMenuShowRoot = () => ( ) export const OverflowMenuNarrowContainer = () => ( -
    +
    Home Products diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 13a8e0f9e51..9824bfb7d09 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -40,6 +40,7 @@ const BreadcrumbsMenuItem = React.forwardRef
    ) + +// 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 + + +) From 68eba14cdeb6ae2099ceefe7a6c55e6cd4f152a7 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Fri, 15 Aug 2025 18:44:33 +1000 Subject: [PATCH 12/46] Fix for ssr and child key --- packages/react/src/Breadcrumbs/Breadcrumbs.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 2719e155e63..27f2933d57e 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -210,8 +210,8 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // Determine final children to render const finalChildren = React.useMemo(() => { if (overflow === 'wrap' || menuItems.length === 0) { - return visibleItems.map(child => ( -
  • + return visibleItems.map((child, index) => ( +
  • {child}
  • )) @@ -249,7 +249,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo ref={containerRef} data-overflow={overflow} > - {finalChildren} + {finalChildren.length > 0 ? finalChildren : children} ) } From f4bba96e221e432729bbd5afbe7ed17262a98cc2 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Fri, 15 Aug 2025 19:31:10 +1000 Subject: [PATCH 13/46] Add IconButton --- .../src/Breadcrumbs/Breadcrumbs.module.css | 8 +++++ .../react/src/Breadcrumbs/Breadcrumbs.tsx | 31 ++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 92f1c9c0ab3..9fd7ff977d1 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -64,3 +64,11 @@ text-decoration: none; } } + +.MenuButton { + display: inline-flex; + align-items: flex-end; + justify-content: center; + height: 0.75rem; + color: var(--fgColor-link); +} diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 27f2933d57e..746184126ef 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -8,8 +8,11 @@ import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../uti import {BoxWithFallback} from '../internal/components/BoxWithFallback' import {ActionMenu} from '../ActionMenu' import {ActionList} from '../ActionList' +import {IconButton} from '../Button/IconButton' +import {KebabHorizontalIcon} from '@primer/octicons-react' import {useResizeObserver} from '../hooks/useResizeObserver' import type {ResizeObserverEntry} from '../hooks/useResizeObserver' +import useLayoutEffect from '../utils/useIsomorphicLayoutEffect' const SELECTED_CLASS = 'selected' @@ -34,18 +37,18 @@ const BreadcrumbsMenuItem = React.forwardRef { return ( - + + {items.map((item, index) => { @@ -87,7 +90,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const childArray = useMemo(() => getValidChildren(children), [children]) - useEffect(() => { + useLayoutEffect(() => { if (visibleItems.length === 0 && childArray.length > 0) { setVisibleItems(childArray) } @@ -249,7 +252,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo ref={containerRef} data-overflow={overflow} > - {finalChildren.length > 0 ? finalChildren : children} + {finalChildren} ) } From b55755cf1f78029d9e5c12361b4a2b525fde4ed3 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 18 Aug 2025 15:41:41 +1000 Subject: [PATCH 14/46] Fix for SSR --- packages/react/src/Breadcrumbs/Breadcrumbs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 746184126ef..4b817f0c533 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -104,7 +104,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo useResizeObserver(handleResize, containerRef) - useEffect(() => { + useLayoutEffect(() => { if (childArray.length > 0) { if (overflow === 'wrap') { setVisibleItems(childArray) From 9477af78021131150b2632a00bb7578710ac8e38 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 18 Aug 2025 16:29:53 +1000 Subject: [PATCH 15/46] Fix for SSR --- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 4b817f0c533..c3e065b50fe 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -1,6 +1,6 @@ import {clsx} from 'clsx' import type {To} from 'history' -import React, {useState, useRef, useCallback, useEffect, useMemo} from 'react' +import React, {useState, useRef, useCallback, useMemo} from 'react' import type {SxProp} from '../sx' import type {ComponentProps} from '../utils/types' import classes from './Breadcrumbs.module.css' @@ -12,7 +12,7 @@ import {IconButton} from '../Button/IconButton' import {KebabHorizontalIcon} from '@primer/octicons-react' import {useResizeObserver} from '../hooks/useResizeObserver' import type {ResizeObserverEntry} from '../hooks/useResizeObserver' -import useLayoutEffect from '../utils/useIsomorphicLayoutEffect' +import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect' const SELECTED_CLASS = 'selected' @@ -83,18 +83,22 @@ const getValidChildren = (children: React.ReactNode) => { function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRoot = true}: BreadcrumbsProps) { const containerRef = useRef(null) const [containerWidth, setContainerWidth] = useState(0) - const [visibleItems, setVisibleItems] = useState([]) - const [menuItems, setMenuItems] = useState([]) const [itemWidths, setItemWidths] = useState([]) const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) const childArray = useMemo(() => getValidChildren(children), [children]) - useLayoutEffect(() => { - if (visibleItems.length === 0 && childArray.length > 0) { + // Initialize visibleItems based on childArray for SSR compatibility + const [visibleItems, setVisibleItems] = useState(() => childArray) + const [menuItems, setMenuItems] = useState([]) + + // Sync visibleItems when childArray changes (for when children prop updates) + useIsomorphicLayoutEffect(() => { + if (overflow === 'wrap') { setVisibleItems(childArray) + setMenuItems([]) } - }, [childArray, visibleItems.length]) + }, [childArray, overflow]) const handleResize = useCallback((entries: ResizeObserverEntry[]) => { if (entries[0]) { @@ -104,7 +108,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo useResizeObserver(handleResize, containerRef) - useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { if (childArray.length > 0) { if (overflow === 'wrap') { setVisibleItems(childArray) From df9aa0f591f59271c74ac02a813225a5a0cd534b Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 18 Aug 2025 16:42:35 +1000 Subject: [PATCH 16/46] Final changes for SSR --- packages/react/src/Breadcrumbs/Breadcrumbs.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index c3e065b50fe..d2efd1c2ad2 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -1,6 +1,6 @@ import {clsx} from 'clsx' import type {To} from 'history' -import React, {useState, useRef, useCallback, useMemo} 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' @@ -12,7 +12,6 @@ import {IconButton} from '../Button/IconButton' import {KebabHorizontalIcon} from '@primer/octicons-react' import {useResizeObserver} from '../hooks/useResizeObserver' import type {ResizeObserverEntry} from '../hooks/useResizeObserver' -import useIsomorphicLayoutEffect from '../utils/useIsomorphicLayoutEffect' const SELECTED_CLASS = 'selected' @@ -92,8 +91,12 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const [visibleItems, setVisibleItems] = useState(() => childArray) const [menuItems, setMenuItems] = useState([]) + // SSR friendly + if (typeof window !== 'undefined') { + overflow = 'wrap' + } // Sync visibleItems when childArray changes (for when children prop updates) - useIsomorphicLayoutEffect(() => { + useEffect(() => { if (overflow === 'wrap') { setVisibleItems(childArray) setMenuItems([]) @@ -108,7 +111,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo useResizeObserver(handleResize, containerRef) - useIsomorphicLayoutEffect(() => { + useEffect(() => { if (childArray.length > 0) { if (overflow === 'wrap') { setVisibleItems(childArray) From fcd8a8e959841ab5826663fec91092e3439ae76b Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Tue, 19 Aug 2025 21:14:10 +1000 Subject: [PATCH 17/46] Rework calculations --- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 214 ++++++++---------- 1 file changed, 97 insertions(+), 117 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index d2efd1c2ad2..1bed67f31f7 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -81,174 +81,154 @@ const getValidChildren = (children: React.ReactNode) => { function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRoot = true}: BreadcrumbsProps) { const containerRef = useRef(null) - const [containerWidth, setContainerWidth] = useState(0) - const [itemWidths, setItemWidths] = useState([]) const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) + let effectiveOverflow = 'wrap' const childArray = useMemo(() => getValidChildren(children), [children]) - // Initialize visibleItems based on childArray for SSR compatibility + const rootItem = childArray[0] + const [visibleItems, setVisibleItems] = useState(() => childArray) + const [childArrayWidths, setChildArrayWidths] = useState(() => []) + const [menuItems, setMenuItems] = useState([]) + const [rootItemWidth, setRootItemWidth] = useState(0) // SSR friendly if (typeof window !== 'undefined') { - overflow = 'wrap' + effectiveOverflow = overflow } - // Sync visibleItems when childArray changes (for when children prop updates) - useEffect(() => { - if (overflow === 'wrap') { - setVisibleItems(childArray) - setMenuItems([]) - } - }, [childArray, overflow]) + const MIN_VISIBLE_ITEMS = !effectiveHideRoot ? 3 : 4 - const handleResize = useCallback((entries: ResizeObserverEntry[]) => { - if (entries[0]) { - setContainerWidth(entries[0].contentRect.width) + useEffect(() => { + const listElement = containerRef.current?.querySelector('ol') + if (listElement && listElement.children.length > 0) { + const listElementArray = Array.from(listElement.children) as HTMLElement[] + const widths = listElementArray.map(child => child.offsetWidth) + setChildArrayWidths(widths) + setRootItemWidth(listElementArray[0].offsetWidth) } - }, []) + }, [childArray.length]) - useResizeObserver(handleResize, containerRef) + const calculateOverflow = useCallback( + (availableWidth: number) => { + const MENU_BUTTON_WIDTH = 50 // Approximate width of "..." button - useEffect(() => { - if (childArray.length > 0) { - if (overflow === 'wrap') { - setVisibleItems(childArray) - setMenuItems([]) - setEffectiveHideRoot(hideRoot) - return + const calculateVisibleItemsWidth = (w: number[]) => { + const widths = w.reduce((sum, width) => sum + width + 16, 0) + return !effectiveHideRoot ? rootItemWidth + widths : widths } - // For 'menu' overflow mode - // Helper function to calculate visible items and menu items with progressive hiding - const calculateOverflow = (availableWidth: number) => { - const listElement = containerRef.current?.querySelector('ol') - if (listElement && listElement.children.length > 0 && itemWidths.length === 0) { - const widths = Array.from(listElement.children).map(child => (child as HTMLElement).offsetWidth) - setItemWidths(widths) - } - const MENU_BUTTON_WIDTH = 50 // Approximate width of "..." button - - // Helper function to calculate total width of visible items - const calculateVisibleItemsWidth = (items: React.ReactElement[]) => { - return items - .map((item, index) => { - return itemWidths[index] - }) - .reduce((sum, width) => sum + width, 0) - } + let currentVisibleItems = [...childArray] + let currentVisibleItemWidths = [...childArrayWidths] + let currentMenuItems: React.ReactElement[] = [] + let currentMenuItemsWidths: number[] = [] + let eHideRoot = effectiveHideRoot - let currentVisibleItems = [...childArray] - let currentMenuItems: React.ReactElement[] = [] + if (availableWidth > 0 && currentVisibleItemWidths.length > 0) { + let visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths) - // If more than 5 items, start by reducing to 5 visible items (including menu) - if (childArray.length > 5) { - // Target: 4 visible items + 1 menu = 5 total - const itemsToHide = childArray.slice(0, childArray.length - 4) - currentMenuItems = itemsToHide - currentVisibleItems = childArray.slice(childArray.length - 4) + // Add menu button width if we have hidden items + if (currentMenuItems.length > 0) { + visibleItemsWidthTotal += MENU_BUTTON_WIDTH } - let eHideRoot = hideRoot - // Now check if current visible items fit in available width - if (availableWidth > 0) { - let visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItems) - - // Add menu button width if we have hidden items + while ( + overflow === 'menu' && + (visibleItemsWidthTotal > availableWidth || currentVisibleItems.length > MIN_VISIBLE_ITEMS) + ) { + // Remove the last visible item + const itemToHide = currentVisibleItems.slice(0)[0] + const itemToHideWidth = currentVisibleItemWidths.slice(0)[0] + currentMenuItems = [...currentMenuItems, itemToHide] + currentMenuItemsWidths = [...currentMenuItemsWidths, itemToHideWidth] + currentVisibleItems = [...currentVisibleItems.slice(1)] + currentVisibleItemWidths = [...currentVisibleItemWidths.slice(1)] + + visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths) + + // Add menu button width if (currentMenuItems.length > 0) { visibleItemsWidthTotal += MENU_BUTTON_WIDTH } - while (visibleItemsWidthTotal > availableWidth && currentVisibleItems.length > 1) { - // Determine which item to hide based on hideRoot setting - let itemToHide: React.ReactElement - - if (eHideRoot) { - // Hide from start when hideRoot is true - itemToHide = currentVisibleItems[0] - currentVisibleItems = currentVisibleItems.slice(1) - } else { - // Try to hide second item (keep root and leaf) when hideRoot is false - itemToHide = currentVisibleItems[1] - currentVisibleItems = [currentVisibleItems[0], ...currentVisibleItems.slice(2)] - } - - currentMenuItems = [itemToHide, ...currentMenuItems] - - visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItems) - - // Add menu button width - if (currentMenuItems.length > 0) { - visibleItemsWidthTotal += MENU_BUTTON_WIDTH - } - - // If hideRoot is false but we still don't fit with root + menu + leaf, - // fallback to hideRoot=true behavior (menu + leaf only) - if ( - !hideRoot && - !eHideRoot && - currentVisibleItems.length === 2 && - visibleItemsWidthTotal > availableWidth - ) { - eHideRoot = true - const rootItem = currentVisibleItems[0] - currentVisibleItems = currentVisibleItems.slice(1) - currentMenuItems = [rootItem, ...currentMenuItems] - - visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItems) - - if (currentMenuItems.length > 0) { - visibleItemsWidthTotal += MENU_BUTTON_WIDTH - } - } + // If hideRoot is false but we still don't fit with root + menu + leaf, + // fallback to hideRoot=true behavior (menu + leaf only) + if (!hideRoot && currentVisibleItems.length === 1 && visibleItemsWidthTotal > availableWidth) { + eHideRoot = true + break + } else { + eHideRoot = hideRoot } } - return { - visibleItems: currentVisibleItems, - menuItems: currentMenuItems, - effectiveHideRoot: eHideRoot, - } } + return { + visibleItems: [...currentVisibleItems], + menuItems: [...currentMenuItems], + effectiveHideRoot: eHideRoot, + } + }, + [MIN_VISIBLE_ITEMS, childArray, childArrayWidths, effectiveHideRoot, hideRoot, overflow, rootItemWidth], + ) - const result = calculateOverflow(containerWidth) - setVisibleItems(result.visibleItems) - setMenuItems(result.menuItems) - setEffectiveHideRoot(result.effectiveHideRoot) - } - }, [childArray, overflow, containerWidth, hideRoot, itemWidths]) + const handleResize = useCallback( + (entries: ResizeObserverEntry[]) => { + if (entries[0]) { + const containerWidth = entries[0].contentRect.width + const result = calculateOverflow(containerWidth) + setVisibleItems(result.visibleItems) + setMenuItems(result.menuItems) + setEffectiveHideRoot(result.effectiveHideRoot) + } + }, + [calculateOverflow], + ) + + useResizeObserver(handleResize, containerRef) // Determine final children to render const finalChildren = React.useMemo(() => { - if (overflow === 'wrap' || menuItems.length === 0) { + if (effectiveOverflow === 'wrap' || menuItems.length === 0) { return visibleItems.map((child, index) => ( -
  • +
  • {child}
  • )) } - // Create menu item and combine with visible items + let effectiveMenuItems = [...menuItems] + if (!effectiveHideRoot) { + effectiveMenuItems = [...menuItems.slice(1)] + } const menuElement = (
  • - +
  • ) - const visibleElements = visibleItems.map(child => ( -
  • + const visibleElements = visibleItems.map((child, index) => ( +
  • {child}
  • )) + const rootElement = ( +
  • + {rootItem} +
  • + ) + // Position menu based on effective hideRoot setting and visible items if (effectiveHideRoot) { // Show: [overflow menu, leaf breadcrumb] return [menuElement, ...visibleElements] } else { // Show: [root breadcrumb, overflow menu, leaf breadcrumb] - return [visibleElements[0], menuElement, ...visibleElements.slice(1)] + return [rootElement, menuElement, ...visibleElements] } - }, [overflow, menuItems, visibleItems, effectiveHideRoot]) + }, [effectiveOverflow, menuItems, visibleItems, rootItem, effectiveHideRoot]) return ( {finalChildren} From 0a09e45cc1e57783f0795c7eaa27322a7232d659 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Tue, 19 Aug 2025 21:54:38 +1000 Subject: [PATCH 18/46] Tests are passing --- packages/react/src/Breadcrumbs/Breadcrumbs.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 1bed67f31f7..bd2212d9713 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -83,7 +83,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const containerRef = useRef(null) const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) let effectiveOverflow = 'wrap' - const childArray = useMemo(() => getValidChildren(children), [children]) const rootItem = childArray[0] @@ -94,7 +93,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const [menuItems, setMenuItems] = useState([]) const [rootItemWidth, setRootItemWidth] = useState(0) - // SSR friendly if (typeof window !== 'undefined') { effectiveOverflow = overflow } @@ -185,6 +183,18 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo useResizeObserver(handleResize, containerRef) + // Initial overflow calculation for testing and >5 items + useEffect(() => { + if (overflow === 'menu' && childArray.length > 5) { + // Get actual container width from DOM or use default + const containerWidth = containerRef.current?.offsetWidth || 800 + const result = calculateOverflow(containerWidth) + setVisibleItems(result.visibleItems) + setMenuItems(result.menuItems) + setEffectiveHideRoot(result.effectiveHideRoot) + } + }, [overflow, childArray.length, calculateOverflow]) + // Determine final children to render const finalChildren = React.useMemo(() => { if (effectiveOverflow === 'wrap' || menuItems.length === 0) { From 3b9891a0e5525163b792c91bab89fc2cf1feba70 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Thu, 21 Aug 2025 16:18:02 +1000 Subject: [PATCH 19/46] Small fixes to menu button --- .../Breadcrumbs.features.stories.tsx | 13 +++-- .../src/Breadcrumbs/Breadcrumbs.module.css | 17 ++++--- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 51 ++++++++++++++----- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx index 872448a2809..0510633a974 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx @@ -38,14 +38,13 @@ export const OverflowMenu = () => ( export const OverflowMenuShowRoot = () => ( - Home - Products - Category - Subcategory - Item - Details + github + Teams + Engineering + core-productivity + collaboration-workflows-flex - Current Page + global-navigation-reviewers ) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 9fd7ff977d1..277300822ab 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -13,9 +13,17 @@ [data-overflow='menu'] .BreadcrumbsList { white-space: nowrap; overflow: hidden; + display: flex; + flex-direction: row; +} + +[data-overflow='menu'] .BreadcrumbsItem { + display: inline-grid; + grid-auto-flow: column; + align-items: center; } -.ItemWrapper { +.BreadcrumbsItem { display: inline-block; font-size: var(--text-body-size-medium); white-space: nowrap; @@ -25,7 +33,7 @@ display: inline-block; height: 0.8em; /* stylelint-disable-next-line primer/spacing */ - margin: 0 0.5em; + margin: 0.25em 0.5em 0 0.5em; font-size: var(--text-body-size-medium); content: ''; /* stylelint-disable-next-line primer/borders, primer/colors */ @@ -66,9 +74,6 @@ } .MenuButton { - display: inline-flex; - align-items: flex-end; - justify-content: center; - height: 0.75rem; color: var(--fgColor-link); + padding-top: 4px; } diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index bd2212d9713..081fcfc9382 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -81,8 +81,9 @@ const getValidChildren = (children: React.ReactNode) => { function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRoot = true}: BreadcrumbsProps) { const containerRef = useRef(null) + const menuButtonRef = useRef(null) const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) - let effectiveOverflow = 'wrap' + //let effectiveOverflow = 'wrap' const childArray = useMemo(() => getValidChildren(children), [children]) const rootItem = childArray[0] @@ -93,9 +94,13 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const [menuItems, setMenuItems] = useState([]) const [rootItemWidth, setRootItemWidth] = useState(0) - if (typeof window !== 'undefined') { - effectiveOverflow = overflow - } + // Menu button width with fallback + const MENU_BUTTON_FALLBACK_WIDTH = 32 // Design system small IconButton + const [menuButtonWidth, setMenuButtonWidth] = useState(MENU_BUTTON_FALLBACK_WIDTH) + + // if (typeof window !== 'undefined') { + // effectiveOverflow = overflow + // } const MIN_VISIBLE_ITEMS = !effectiveHideRoot ? 3 : 4 useEffect(() => { @@ -108,9 +113,19 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo } }, [childArray.length]) + // Measure actual menu button width when it exists + useEffect(() => { + if (menuButtonRef.current) { + const measuredWidth = menuButtonRef.current.offsetWidth + if (measuredWidth > 0) { + setMenuButtonWidth(measuredWidth) + } + } + }, [menuItems.length]) // Re-measure when menu button appears/disappears + const calculateOverflow = useCallback( (availableWidth: number) => { - const MENU_BUTTON_WIDTH = 50 // Approximate width of "..." button + const MENU_BUTTON_WIDTH = menuButtonWidth // Use measured width with fallback const calculateVisibleItemsWidth = (w: number[]) => { const widths = w.reduce((sum, width) => sum + width + 16, 0) @@ -165,7 +180,16 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo effectiveHideRoot: eHideRoot, } }, - [MIN_VISIBLE_ITEMS, childArray, childArrayWidths, effectiveHideRoot, hideRoot, overflow, rootItemWidth], + [ + MIN_VISIBLE_ITEMS, + childArray, + childArrayWidths, + effectiveHideRoot, + hideRoot, + overflow, + rootItemWidth, + menuButtonWidth, + ], ) const handleResize = useCallback( @@ -197,9 +221,9 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // Determine final children to render const finalChildren = React.useMemo(() => { - if (effectiveOverflow === 'wrap' || menuItems.length === 0) { + if (overflow === 'wrap' || menuItems.length === 0) { return visibleItems.map((child, index) => ( -
  • +
  • {child}
  • )) @@ -210,8 +234,9 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo effectiveMenuItems = [...menuItems.slice(1)] } const menuElement = ( -
  • +
  • @@ -219,13 +244,13 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo ) const visibleElements = visibleItems.map((child, index) => ( -
  • +
  • {child}
  • )) const rootElement = ( -
  • +
  • {rootItem}
  • ) @@ -238,7 +263,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // Show: [root breadcrumb, overflow menu, leaf breadcrumb] return [rootElement, menuElement, ...visibleElements] } - }, [effectiveOverflow, menuItems, visibleItems, rootItem, effectiveHideRoot]) + }, [overflow, menuItems, effectiveHideRoot, visibleItems, rootItem]) return ( {finalChildren} From 273a0551ca3ea08da4d4c4dc8d940c6cfee2d276 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Thu, 21 Aug 2025 22:32:34 +1000 Subject: [PATCH 20/46] Fix styling issues with button --- .../src/Breadcrumbs/Breadcrumbs.module.css | 34 ++++++++----------- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 13 +++++++ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 277300822ab..94914f387a5 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -17,37 +17,23 @@ flex-direction: row; } -[data-overflow='menu'] .BreadcrumbsItem { +.BreadcrumbsItem { display: inline-grid; grid-auto-flow: column; align-items: center; -} - -.BreadcrumbsItem { - display: inline-block; + flex: 0 99999 auto; + min-width: auto; font-size: var(--text-body-size-medium); white-space: nowrap; list-style: none; - &::after { - display: inline-block; - height: 0.8em; - /* stylelint-disable-next-line primer/spacing */ - margin: 0.25em 0.5em 0 0.5em; - font-size: var(--text-body-size-medium); - content: ''; - /* stylelint-disable-next-line primer/borders, primer/colors */ - border-right: 0.1em solid var(--fgColor-muted); - transform: rotate(15deg) translateY(0.0625em); - } - &:first-child { margin-left: 0; } &:last-child { - &::after { - content: none; + .ItemSeparator { + display: none; } } } @@ -75,5 +61,13 @@ .MenuButton { color: var(--fgColor-link); - padding-top: 4px; +} + +.ItemSeparator { + color: var(--fgColor-muted); + display: flex; + align-self: center; + justify-content: center; + white-space: nowrap; + user-select: none; } diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 081fcfc9382..ce8569a055c 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -240,18 +240,21 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo items={effectiveMenuItems} aria-label={`${effectiveMenuItems.length} more breadcrumb items`} /> + ) const visibleElements = visibleItems.map((child, index) => (
  • {child} +
  • )) const rootElement = (
  • {rootItem} +
  • ) @@ -279,6 +282,16 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo ) } +const ItemSeparator = () => { + return ( + + + + ) +} + type StyledBreadcrumbsItemProps = { to?: To selected?: boolean From 08637ae6a1b9c63cf16002f16436ed8af1af32c0 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Fri, 22 Aug 2025 15:57:20 +1000 Subject: [PATCH 21/46] Fix focus states --- .../src/Breadcrumbs/Breadcrumbs.module.css | 18 ++++++------------ packages/react/src/Breadcrumbs/Breadcrumbs.tsx | 2 -- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 94914f387a5..845f02ff394 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 { @@ -43,19 +44,12 @@ 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, - &:focus { - text-decoration: underline; - } -} - -.ItemSelected { - color: var(--fgColor-default); - pointer-events: none; - - &:focus { - text-decoration: none; + &:hover { + background: var(--control-transparent-bgColor-hover); } } diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index ce8569a055c..f468cdfaf65 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -60,7 +60,6 @@ const BreadcrumbsMenuItem = React.forwardRef {children} @@ -306,7 +305,6 @@ const BreadcrumbsItem = React.forwardRef(({selected, className, ...rest}, ref) = as="a" className={clsx(className, classes.Item, { [SELECTED_CLASS]: selected, - [classes.ItemSelected]: selected, })} aria-current={selected ? 'page' : undefined} ref={ref} From 17a31075eea03db028a9fd8e472ab957e8f77151 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Fri, 22 Aug 2025 15:59:25 +1000 Subject: [PATCH 22/46] Fix focus states --- packages/react/src/Breadcrumbs/Breadcrumbs.module.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 845f02ff394..1b5803276bd 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -51,6 +51,11 @@ &:hover { background: var(--control-transparent-bgColor-hover); } + &:focus { + box-shadow: none; + outline: 2px solid var(--focus-outlineColor, var(--color-accent-fg)); + outline-offset: -2px; + } } .MenuButton { From ed838f3abab8fbd439a977df26668ca6d2ed4281 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 25 Aug 2025 21:57:13 +1000 Subject: [PATCH 23/46] Make sure old behavior works --- .../src/Breadcrumbs/Breadcrumbs.module.css | 53 ++++++++++++++++++- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 9 ++-- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 1b5803276bd..d5084c0d65e 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -10,7 +10,6 @@ margin-bottom: 0; } -/* Prevent wrapping when using menu overflow */ [data-overflow='menu'] .BreadcrumbsList { white-space: nowrap; overflow: hidden; @@ -39,7 +38,7 @@ } } -.Item { +[data-overflow='menu'] .Item { display: inline-block; font-size: var(--text-body-size-medium); color: var(--fgColor-link); @@ -70,3 +69,53 @@ white-space: nowrap; user-select: none; } + +.ItemWrapper { + display: inline-block; + font-size: var(--text-body-size-medium); + white-space: nowrap; + list-style: none; + + &::after { + display: inline-block; + height: 0.8em; + /* stylelint-disable-next-line primer/spacing */ + margin: 0 0.5em; + font-size: var(--text-body-size-medium); + content: ''; + /* stylelint-disable-next-line primer/borders, primer/colors */ + border-right: 0.1em solid var(--fgColor-muted); + transform: rotate(15deg) translateY(0.0625em); + } + + &:first-child { + margin-left: 0; + } + + &:last-child { + &::after { + content: none; + } + } +} + +.Item { + display: inline-block; + font-size: var(--text-body-size-medium); + color: var(--fgColor-link); + text-decoration: none; + + &:hover, + &:focus { + text-decoration: underline; + } +} + +.ItemSelected { + color: var(--fgColor-default); + pointer-events: none; + + &:focus { + text-decoration: none; + } +} diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index f468cdfaf65..2f0396b4235 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -221,11 +221,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // Determine final children to render const finalChildren = React.useMemo(() => { if (overflow === 'wrap' || menuItems.length === 0) { - return visibleItems.map((child, index) => ( -
  • - {child} -
  • - )) + return React.Children.map(children, child =>
  • {child}
  • ) } let effectiveMenuItems = [...menuItems] @@ -265,7 +261,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // Show: [root breadcrumb, overflow menu, leaf breadcrumb] return [rootElement, menuElement, ...visibleElements] } - }, [overflow, menuItems, effectiveHideRoot, visibleItems, rootItem]) + }, [overflow, menuItems, effectiveHideRoot, visibleItems, rootItem, children]) return ( Date: Mon, 25 Aug 2025 22:56:33 +1000 Subject: [PATCH 24/46] Fix tests and lint --- packages/react/src/Breadcrumbs/Breadcrumbs.module.css | 1 + packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index d5084c0d65e..31bc52059a5 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -50,6 +50,7 @@ &:hover { background: var(--control-transparent-bgColor-hover); } + &:focus { box-shadow: none; outline: 2px solid var(--focus-outlineColor, var(--color-accent-fg)); diff --git a/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx b/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx index 41bbf99cbe7..3a9d8d7c063 100644 --- a/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx +++ b/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx @@ -276,10 +276,10 @@ describe('Breadcrumbs', () => { expect(screen.getByRole('menuitem', {name: 'Home'})).toBeInTheDocument() expect(screen.getByRole('menuitem', {name: 'Category'})).toBeInTheDocument() expect(screen.getByRole('menuitem', {name: 'Subcategory'})).toBeInTheDocument() + expect(screen.getByRole('menuitem', {name: 'Product'})).toBeInTheDocument() }) // Verify that the last 4 items are visible as regular breadcrumb items (not in menu) - expect(screen.getByRole('link', {name: 'Product'})).toBeInTheDocument() expect(screen.getByRole('link', {name: 'Details'})).toBeInTheDocument() expect(screen.getByRole('link', {name: 'Specifications'})).toBeInTheDocument() expect(screen.getByRole('link', {name: 'Reviews'})).toBeInTheDocument() @@ -322,10 +322,10 @@ describe('Breadcrumbs', () => { expect(screen.getByRole('menuitem', {name: 'Home'})).toBeInTheDocument() expect(screen.getByRole('menuitem', {name: 'Category'})).toBeInTheDocument() expect(screen.getByRole('menuitem', {name: 'Subcategory'})).toBeInTheDocument() + expect(screen.getByRole('menuitem', {name: 'Product'})).toBeInTheDocument() }) // Verify visible breadcrumb items are still accessible (last 4 items) - expect(screen.getByRole('link', {name: 'Product'})).toBeInTheDocument() expect(screen.getByRole('link', {name: 'Details'})).toBeInTheDocument() expect(screen.getByRole('link', {name: 'Specifications'})).toBeInTheDocument() expect(screen.getByRole('link', {name: 'Reviews'})).toBeInTheDocument() From 0eaf106ae4f451a1ad8b4793f61310fae378db7c Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Tue, 26 Aug 2025 09:11:50 +1000 Subject: [PATCH 25/46] Create eighty-queens-tap.md --- .changeset/eighty-queens-tap.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eighty-queens-tap.md 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 From 40cb6a9276ee73ef6feee0328d26f5653b2a1cd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:10:45 -0500 Subject: [PATCH 26/46] chore(deps-dev): bump the eslint group with 3 updates (#6657) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 28 ++++++++++++++-------------- package.json | 6 +++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74f9f0d59b6..e357144c797 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@eslint-react/eslint-plugin": "^1.52.6", "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.34.0", "@github/axe-github": "0.7.0", "@github/markdownlint-github": "^0.7.0", "@github/mini-throttle": "2.1.1", @@ -32,7 +32,7 @@ "@size-limit/preset-big-lib": "11.2.0", "@vitest/browser": "^3.2.4", "@vitest/eslint-plugin": "^1.3.4", - "eslint": "^9.33.0", + "eslint": "^9.34.0", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-clsx": "^0.0.10", "eslint-plugin-github": "^6.0.0", @@ -45,7 +45,7 @@ "eslint-plugin-react-compiler": "^19.1.0-rc.2", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-ssr-friendly": "1.3.0", - "eslint-plugin-storybook": "^9.1.2", + "eslint-plugin-storybook": "^9.1.3", "eslint-plugin-testing-library": "^7.6.6", "globals": "^16.2.0", "markdownlint-cli2": "^0.17.2", @@ -3745,9 +3745,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -11514,9 +11514,9 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { @@ -11526,7 +11526,7 @@ "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -12587,9 +12587,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.2.tgz", - "integrity": "sha512-EQa/kChrYrekxv36q3pvW57anqxMlAP4EdPXEDyA/EDrCQJaaTbWEdsMnVZtD744RjPP0M5wzaUjHbMhNooAwQ==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.3.tgz", + "integrity": "sha512-CR576JrlvxLY2ebJIyR6z/YWy6+iyVsB7ORjPrwM3a9SshlRnAntdEn6hyMYbQmFoPIv7kYcRiDznDXBQ/jitA==", "dev": true, "license": "MIT", "dependencies": { @@ -12600,7 +12600,7 @@ }, "peerDependencies": { "eslint": ">=8", - "storybook": "^9.1.2" + "storybook": "^9.1.3" } }, "node_modules/eslint-plugin-testing-library": { diff --git a/package.json b/package.json index 2906294b9f1..2a821d166ad 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@eslint-react/eslint-plugin": "^1.52.6", "@eslint/compat": "^1.3.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.33.0", + "@eslint/js": "^9.34.0", "@github/axe-github": "0.7.0", "@github/markdownlint-github": "^0.7.0", "@github/mini-throttle": "2.1.1", @@ -60,7 +60,7 @@ "@size-limit/preset-big-lib": "11.2.0", "@vitest/browser": "^3.2.4", "@vitest/eslint-plugin": "^1.3.4", - "eslint": "^9.33.0", + "eslint": "^9.34.0", "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-clsx": "^0.0.10", "eslint-plugin-github": "^6.0.0", @@ -73,7 +73,7 @@ "eslint-plugin-react-compiler": "^19.1.0-rc.2", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-ssr-friendly": "1.3.0", - "eslint-plugin-storybook": "^9.1.2", + "eslint-plugin-storybook": "^9.1.3", "eslint-plugin-testing-library": "^7.6.6", "globals": "^16.2.0", "markdownlint-cli2": "^0.17.2", From 027f4f60c8c8d8b7e03d3da9bc650ddea332bd68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:11:25 +0000 Subject: [PATCH 27/46] chore(deps): bump the rollup group with 2 updates (#6659) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 184 ++++++++++++++--------------- package.json | 2 +- packages/mcp/package.json | 2 +- packages/react/package.json | 2 +- packages/styled-react/package.json | 2 +- 5 files changed, 91 insertions(+), 101 deletions(-) diff --git a/package-lock.json b/package-lock.json index e357144c797..ddc061fdfac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "npm": ">=7" }, "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "^4.46.2" + "@rollup/rollup-linux-x64-gnu": "^4.48.0" } }, "examples/codesandbox": { @@ -6832,9 +6832,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.48.0.tgz", + "integrity": "sha512-aVzKH922ogVAWkKiyKXorjYymz2084zrhrZRXtLrA5eEx5SO8Dj0c/4FpCHZyn7MKzhW2pW4tK28vVr+5oQ2xw==", "cpu": [ "arm" ], @@ -6846,9 +6846,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.48.0.tgz", + "integrity": "sha512-diOdQuw43xTa1RddAFbhIA8toirSzFMcnIg8kvlzRbK26xqEnKJ/vqQnghTAajy2Dcy42v+GMPMo6jq67od+Dw==", "cpu": [ "arm64" ], @@ -6860,7 +6860,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.0.tgz", + "integrity": "sha512-QhR2KA18fPlJWFefySJPDYZELaVqIUVnYgAOdtJ+B/uH96CFg2l1TQpX19XpUMWUqMyIiyY45wje8K6F4w4/CA==", "cpu": [ "arm64" ], @@ -6872,9 +6874,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.48.0.tgz", + "integrity": "sha512-Q9RMXnQVJ5S1SYpNSTwXDpoQLgJ/fbInWOyjbCnnqTElEyeNvLAB3QvG5xmMQMhFN74bB5ZZJYkKaFPcOG8sGg==", "cpu": [ "x64" ], @@ -6886,9 +6888,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.48.0.tgz", + "integrity": "sha512-3jzOhHWM8O8PSfyft+ghXZfBkZawQA0PUGtadKYxFqpcYlOYjTi06WsnYBsbMHLawr+4uWirLlbhcYLHDXR16w==", "cpu": [ "arm64" ], @@ -6900,9 +6902,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.48.0.tgz", + "integrity": "sha512-NcD5uVUmE73C/TPJqf78hInZmiSBsDpz3iD5MF/BuB+qzm4ooF2S1HfeTChj5K4AV3y19FFPgxonsxiEpy8v/A==", "cpu": [ "x64" ], @@ -6914,9 +6916,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.48.0.tgz", + "integrity": "sha512-JWnrj8qZgLWRNHr7NbpdnrQ8kcg09EBBq8jVOjmtlB3c8C6IrynAJSMhMVGME4YfTJzIkJqvSUSVJRqkDnu/aA==", "cpu": [ "arm" ], @@ -6928,9 +6930,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.48.0.tgz", + "integrity": "sha512-9xu92F0TxuMH0tD6tG3+GtngwdgSf8Bnz+YcsPG91/r5Vgh5LNofO48jV55priA95p3c92FLmPM7CvsVlnSbGQ==", "cpu": [ "arm" ], @@ -6942,9 +6944,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.48.0.tgz", + "integrity": "sha512-NLtvJB5YpWn7jlp1rJiY0s+G1Z1IVmkDuiywiqUhh96MIraC0n7XQc2SZ1CZz14shqkM+XN2UrfIo7JB6UufOA==", "cpu": [ "arm64" ], @@ -6956,9 +6958,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.48.0.tgz", + "integrity": "sha512-QJ4hCOnz2SXgCh+HmpvZkM+0NSGcZACyYS8DGbWn2PbmA0e5xUk4bIP8eqJyNXLtyB4gZ3/XyvKtQ1IFH671vQ==", "cpu": [ "arm64" ], @@ -6970,9 +6972,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.48.0.tgz", + "integrity": "sha512-Pk0qlGJnhILdIC5zSKQnprFjrGmjfDM7TPZ0FKJxRkoo+kgMRAg4ps1VlTZf8u2vohSicLg7NP+cA5qE96PaFg==", "cpu": [ "loong64" ], @@ -6984,9 +6986,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.48.0.tgz", + "integrity": "sha512-/dNFc6rTpoOzgp5GKoYjT6uLo8okR/Chi2ECOmCZiS4oqh3mc95pThWma7Bgyk6/WTEvjDINpiBCuecPLOgBLQ==", "cpu": [ "ppc64" ], @@ -6998,9 +7000,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.48.0.tgz", + "integrity": "sha512-YBwXsvsFI8CVA4ej+bJF2d9uAeIiSkqKSPQNn0Wyh4eMDY4wxuSp71BauPjQNCKK2tD2/ksJ7uhJ8X/PVY9bHQ==", "cpu": [ "riscv64" ], @@ -7012,9 +7014,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.48.0.tgz", + "integrity": "sha512-FI3Rr2aGAtl1aHzbkBIamsQyuauYtTF9SDUJ8n2wMXuuxwchC3QkumZa1TEXYIv/1AUp1a25Kwy6ONArvnyeVQ==", "cpu": [ "riscv64" ], @@ -7026,9 +7028,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.48.0.tgz", + "integrity": "sha512-Dx7qH0/rvNNFmCcIRe1pyQ9/H0XO4v/f0SDoafwRYwc2J7bJZ5N4CHL/cdjamISZ5Cgnon6iazAVRFlxSoHQnQ==", "cpu": [ "s390x" ], @@ -7040,9 +7042,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.47.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.47.1.tgz", - "integrity": "sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.48.0.tgz", + "integrity": "sha512-GUdZKTeKBq9WmEBzvFYuC88yk26vT66lQV8D5+9TgkfbewhLaTHRNATyzpQwwbHIfJvDJ3N9WJ90wK/uR3cy3Q==", "cpu": [ "x64" ], @@ -7053,9 +7055,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.48.0.tgz", + "integrity": "sha512-ao58Adz/v14MWpQgYAb4a4h3fdw73DrDGtaiF7Opds5wNyEQwtO6M9dBh89nke0yoZzzaegq6J/EXs7eBebG8A==", "cpu": [ "x64" ], @@ -7067,9 +7069,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.48.0.tgz", + "integrity": "sha512-kpFno46bHtjZVdRIOxqaGeiABiToo2J+st7Yce+aiAoo1H0xPi2keyQIP04n2JjDVuxBN6bSz9R6RdTK5hIppw==", "cpu": [ "arm64" ], @@ -7081,9 +7083,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.48.0.tgz", + "integrity": "sha512-rFYrk4lLk9YUTIeihnQMiwMr6gDhGGSbWThPEDfBoU/HdAtOzPXeexKi7yU8jO+LWRKnmqPN9NviHQf6GDwBcQ==", "cpu": [ "ia32" ], @@ -7095,9 +7097,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.48.0.tgz", + "integrity": "sha512-sq0hHLTgdtwOPDB5SJOuaoHyiP1qSwg+71TQWk8iDS04bW1wIE0oQ6otPiRj2ZvLYNASLMaTp8QRGUVZ+5OL5A==", "cpu": [ "x64" ], @@ -20211,7 +20213,9 @@ } }, "node_modules/rollup": { - "version": "4.46.2", + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.0.tgz", + "integrity": "sha512-BXHRqK1vyt9XVSEHZ9y7xdYtuYbwVod2mLwOMFP7t/Eqoc1pHRlG/WdV2qNeNvZHRQdLedaFycljaYYM96RqJQ==", "dev": true, "license": "MIT", "dependencies": { @@ -20225,26 +20229,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", + "@rollup/rollup-android-arm-eabi": "4.48.0", + "@rollup/rollup-android-arm64": "4.48.0", + "@rollup/rollup-darwin-arm64": "4.48.0", + "@rollup/rollup-darwin-x64": "4.48.0", + "@rollup/rollup-freebsd-arm64": "4.48.0", + "@rollup/rollup-freebsd-x64": "4.48.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.48.0", + "@rollup/rollup-linux-arm-musleabihf": "4.48.0", + "@rollup/rollup-linux-arm64-gnu": "4.48.0", + "@rollup/rollup-linux-arm64-musl": "4.48.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.48.0", + "@rollup/rollup-linux-ppc64-gnu": "4.48.0", + "@rollup/rollup-linux-riscv64-gnu": "4.48.0", + "@rollup/rollup-linux-riscv64-musl": "4.48.0", + "@rollup/rollup-linux-s390x-gnu": "4.48.0", + "@rollup/rollup-linux-x64-gnu": "4.48.0", + "@rollup/rollup-linux-x64-musl": "4.48.0", + "@rollup/rollup-win32-arm64-msvc": "4.48.0", + "@rollup/rollup-win32-ia32-msvc": "4.48.0", + "@rollup/rollup-win32-x64-msvc": "4.48.0", "fsevents": "~2.3.2" } }, @@ -21003,20 +21007,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/router": { "version": "2.2.0", "license": "MIT", @@ -25131,7 +25121,7 @@ "@rollup/plugin-node-resolve": "^16.0.1", "@types/turndown": "^5.0.5", "rimraf": "^6.0.1", - "rollup": "^4.46.2", + "rollup": "^4.48.0", "rollup-plugin-typescript2": "^0.36.0", "typescript": "^5.9.2" } @@ -25477,7 +25467,7 @@ "react-test-renderer": "18.3.1", "recast": "0.23.7", "rimraf": "5.0.5", - "rollup": "4.46.2", + "rollup": "4.48.0", "rollup-plugin-import-css": "^0.0.0", "rollup-plugin-postcss": "4.0.2", "rollup-plugin-visualizer": "6.0.3", @@ -26236,7 +26226,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "rimraf": "^6.0.1", - "rollup": "4.46.2", + "rollup": "4.48.0", "rollup-plugin-typescript2": "^0.36.0", "styled-components": "5.3.11", "typescript": "^5.9.2" diff --git a/package.json b/package.json index 2a821d166ad..d2a9192cabd 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "vitest": "^3.2.4" }, "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "^4.46.2" + "@rollup/rollup-linux-x64-gnu": "^4.48.0" }, "overrides": { "nwsapi": "2.2.2" diff --git a/packages/mcp/package.json b/packages/mcp/package.json index d3e5d188fa8..429162e2b30 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -51,7 +51,7 @@ "@rollup/plugin-node-resolve": "^16.0.1", "@types/turndown": "^5.0.5", "rimraf": "^6.0.1", - "rollup": "^4.46.2", + "rollup": "^4.48.0", "rollup-plugin-typescript2": "^0.36.0", "typescript": "^5.9.2" } diff --git a/packages/react/package.json b/packages/react/package.json index 04a39306ed7..4381b368550 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -188,7 +188,7 @@ "react-test-renderer": "18.3.1", "recast": "0.23.7", "rimraf": "5.0.5", - "rollup": "4.46.2", + "rollup": "4.48.0", "rollup-plugin-import-css": "^0.0.0", "rollup-plugin-postcss": "4.0.2", "rollup-plugin-visualizer": "6.0.3", diff --git a/packages/styled-react/package.json b/packages/styled-react/package.json index 03eea48ba70..da90c245792 100644 --- a/packages/styled-react/package.json +++ b/packages/styled-react/package.json @@ -37,7 +37,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "rimraf": "^6.0.1", - "rollup": "4.46.2", + "rollup": "4.48.0", "rollup-plugin-typescript2": "^0.36.0", "styled-components": "5.3.11", "typescript": "^5.9.2" From 7b4921cab234ad94b5807c41755b7f6279b9bcb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:12:43 +0000 Subject: [PATCH 28/46] chore(deps-dev): bump postcss-mixins from 11.0.1 to 12.1.2 (#6660) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 32 ++++++++++++++------- packages/postcss-preset-primer/package.json | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index ddc061fdfac..882be7c1c24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18576,7 +18576,9 @@ } }, "node_modules/postcss-mixins": { - "version": "11.0.1", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-12.1.2.tgz", + "integrity": "sha512-90pSxmZVfbX9e5xCv7tI5RV1mnjdf16y89CJKbf/hD7GyOz1FCxcYMl8ZYA8Hc56dbApTKKmU9HfvgfWdCxlwg==", "dev": true, "funding": [ { @@ -18592,11 +18594,11 @@ "dependencies": { "postcss-js": "^4.0.1", "postcss-simple-vars": "^7.0.1", - "sugarss": "^4.0.1", - "tinyglobby": "^0.2.6" + "sugarss": "^5.0.0", + "tinyglobby": "^0.2.14" }, "engines": { - "node": "^18.0 || ^ 20.0 || >= 22.0" + "node": "^20.0 || ^22.0 || >=24.0" }, "peerDependencies": { "postcss": "^8.2.14" @@ -22568,15 +22570,23 @@ } }, "node_modules/sugarss": { - "version": "4.0.1", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-5.0.1.tgz", + "integrity": "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">=18.0" }, "peerDependencies": { "postcss": "^8.3.3" @@ -25257,7 +25267,7 @@ "cssnano": "^7.0.7", "postcss": "^8.4.41", "postcss-custom-properties-fallback": "^1.0.2", - "postcss-mixins": "^11.0.1", + "postcss-mixins": "^12.1.2", "typescript": "^5.9.2" }, "peerDependencies": { diff --git a/packages/postcss-preset-primer/package.json b/packages/postcss-preset-primer/package.json index 2117b3e29be..a0a8b99e73b 100644 --- a/packages/postcss-preset-primer/package.json +++ b/packages/postcss-preset-primer/package.json @@ -23,7 +23,7 @@ "cssnano": "^7.0.7", "postcss": "^8.4.41", "postcss-custom-properties-fallback": "^1.0.2", - "postcss-mixins": "^11.0.1", + "postcss-mixins": "^12.1.2", "typescript": "^5.9.2" } } From bc2749c5d2640b123c1628751fc257bff6e572d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:17:44 +0000 Subject: [PATCH 29/46] chore(deps-dev): bump @github/markdownlint-github from 0.7.0 to 0.8.0 (#6661) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 ++++-- package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 882be7c1c24..79161dc63c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.34.0", "@github/axe-github": "0.7.0", - "@github/markdownlint-github": "^0.7.0", + "@github/markdownlint-github": "^0.8.0", "@github/mini-throttle": "2.1.1", "@github/prettier-config": "0.0.6", "@mdx-js/react": "1.6.22", @@ -4178,7 +4178,9 @@ "license": "MIT" }, "node_modules/@github/markdownlint-github": { - "version": "0.7.0", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@github/markdownlint-github/-/markdownlint-github-0.8.0.tgz", + "integrity": "sha512-079sWT/2Z8EI5v02GTtSfvG06E1m8Q6xjYoQiGdPg6rSKVntpfBw6in79fGs+vc9cYihBHl73vkOoDcyH/Jl8g==", "dev": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index d2a9192cabd..9ff3bfe7316 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.34.0", "@github/axe-github": "0.7.0", - "@github/markdownlint-github": "^0.7.0", + "@github/markdownlint-github": "^0.8.0", "@github/mini-throttle": "2.1.1", "@github/prettier-config": "0.0.6", "@mdx-js/react": "1.6.22", From f9c9a2d4441d7403f1ae01af21e0c8abad426209 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Mon, 25 Aug 2025 12:28:57 -0500 Subject: [PATCH 30/46] feat(mcp): add better primitives output, add coding guidelines tool (#6599) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/mcp/src/primitives.ts | 103 ++++++++ packages/mcp/src/server.ts | 460 +++------------------------------ 2 files changed, 145 insertions(+), 418 deletions(-) create mode 100644 packages/mcp/src/primitives.ts diff --git a/packages/mcp/src/primitives.ts b/packages/mcp/src/primitives.ts new file mode 100644 index 00000000000..493e6b2b0fe --- /dev/null +++ b/packages/mcp/src/primitives.ts @@ -0,0 +1,103 @@ +import baseMotion from '@primer/primitives/dist/docs/base/motion/motion.json' with {type: 'json'} +import baseSize from '@primer/primitives/dist/docs/base/size/size.json' with {type: 'json'} +import baseTypography from '@primer/primitives/dist/docs/base/typography/typography.json' with {type: 'json'} +import functionalSizeBorder from '@primer/primitives/dist/docs/functional/size/border.json' with {type: 'json'} +import functionalSizeCoarse from '@primer/primitives/dist/docs/functional/size/size-coarse.json' with {type: 'json'} +import functionalSizeFine from '@primer/primitives/dist/docs/functional/size/size-fine.json' with {type: 'json'} +import functionalSize from '@primer/primitives/dist/docs/functional/size/size.json' with {type: 'json'} +import light from '@primer/primitives/dist/docs/functional/themes/light.json' with {type: 'json'} +import functionalTypography from '@primer/primitives/dist/docs/functional/typography/typography.json' with {type: 'json'} + +const categories = { + base: { + motion: Object.values(baseMotion).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + size: Object.values(baseSize).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + typography: Object.values(baseTypography).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + }, + functional: { + border: Object.values(functionalSizeBorder).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + sizeCoarse: Object.values(functionalSizeCoarse).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + sizeFine: Object.values(functionalSizeFine).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + size: Object.values(functionalSize).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + themes: { + light: Object.values(light).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + }, + typography: Object.values(functionalTypography).map(token => { + return { + name: token.name, + type: token.type, + value: token.value, + } + }), + }, +} as const + +const tokens = [ + ...categories.base.motion, + ...categories.base.size, + ...categories.base.typography, + ...categories.functional.border, + ...categories.functional.sizeCoarse, + ...categories.functional.sizeFine, + ...categories.functional.size, + ...categories.functional.themes.light, + ...categories.functional.typography, +] + +function serialize(value: typeof tokens): string { + return value + .map(token => { + return `` + }) + .join('\n') +} + +export {categories, tokens, serialize} diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 16adb4b36ed..07f179c4ece 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -4,6 +4,7 @@ import * as cheerio from 'cheerio' import {z} from 'zod' import TurndownService from 'turndown' import {listComponents, listPatterns, listIcons} from './primer' +import {tokens, serialize} from './primitives' import packageJson from '../package.json' with {type: 'json'} const server = new McpServer({ @@ -413,430 +414,14 @@ ${text}`, }, ) -type Token = TokenCategory | string -type TokenCategory = { - category: string - tokens: Array -} - -const tokens: Array = [ - { - category: 'color', - tokens: [ - { - category: 'foreground', - tokens: [ - '--fgColor-accent', - '--fgColor-attention', - '--fgColor-black', - '--fgColor-closed', - '--fgColor-danger', - '--fgColor-default', - '--fgColor-disabled', - '--fgColor-done', - '--fgColor-link', - '--fgColor-muted', - '--fgColor-neutral', - '--fgColor-onEmphasis', - '--fgColor-onInverse', - '--fgColor-open', - '--fgColor-severe', - '--fgColor-sponsors', - '--fgColor-success', - '--fgColor-upsell', - '--fgColor-white', - ], - }, - { - category: 'background', - tokens: [ - '--bgColor-accent-emphasis', - '--bgColor-accent-muted', - '--bgColor-attention-emphasis', - '--bgColor-attention-muted', - '--bgColor-black', - '--bgColor-closed-emphasis', - '--bgColor-closed-muted', - '--bgColor-danger-emphasis', - '--bgColor-danger-muted', - '--bgColor-default', - '--bgColor-disabled', - '--bgColor-done-emphasis', - '--bgColor-done-muted', - '--bgColor-emphasis', - '--bgColor-inset', - '--bgColor-inverse', - '--bgColor-muted', - '--bgColor-neutral-emphasis', - '--bgColor-neutral-muted', - '--bgColor-open-emphasis', - '--bgColor-open-muted', - '--bgColor-severe-emphasis', - '--bgColor-severe-muted', - '--bgColor-sponsors-emphasis', - '--bgColor-sponsors-muted', - '--bgColor-success-emphasis', - '--bgColor-success-muted', - '--bgColor-transparent', - '--bgColor-upsell-emphasis', - '--bgColor-upsell-muted', - '--bgColor-white', - ], - }, - { - category: 'border', - tokens: [ - '--borderColor-accent-emphasis', - '--borderColor-accent-muted', - '--borderColor-attention-emphasis', - '--borderColor-attention-muted', - '--borderColor-closed-emphasis', - '--borderColor-closed-muted', - '--borderColor-danger-emphasis', - '--borderColor-danger-muted', - '--borderColor-default', - '--borderColor-disabled', - '--borderColor-done-emphasis', - '--borderColor-done-muted', - '--borderColor-emphasis', - '--borderColor-muted', - '--borderColor-neutral-emphasis', - '--borderColor-neutral-muted', - '--borderColor-open-emphasis', - '--borderColor-open-muted', - '--borderColor-severe-emphasis', - '--borderColor-severe-muted', - '--borderColor-sponsors-emphasis', - '--borderColor-sponsors-muted', - '--borderColor-success-emphasis', - '--borderColor-success-muted', - '--borderColor-translucent', - '--borderColor-transparent', - '--borderColor-upsell-emphasis', - '--borderColor-upsell-muted', - ], - }, - { - category: 'shadow', - tokens: [ - '--shadow-floating-large', - '--shadow-floating-legacy', - '--shadow-floating-medium', - '--shadow-floating-small', - '--shadow-floating-xlarge', - '--shadow-inset', - '--shadow-resting-medium', - '--shadow-resting-small', - '--shadow-resting-xsmall', - ], - }, - { - category: 'control', - tokens: [ - '--control-bgColor-active', - '--control-bgColor-disabled', - '--control-bgColor-hover', - '--control-bgColor-rest', - '--control-bgColor-selected', - '--control-borderColor-danger', - '--control-borderColor-disabled', - '--control-borderColor-emphasis', - '--control-borderColor-rest', - '--control-borderColor-selected', - '--control-borderColor-success', - '--control-borderColor-warning', - '--control-checked-bgColor-active', - '--control-checked-bgColor-disabled', - '--control-checked-bgColor-hover', - '--control-checked-bgColor-rest', - '--control-checked-borderColor-active', - '--control-checked-borderColor-disabled', - '--control-checked-borderColor-hover', - '--control-checked-borderColor-rest', - '--control-checked-fgColor-disabled', - '--control-checked-fgColor-rest', - '--control-danger-bgColor-active', - '--control-danger-bgColor-hover', - '--control-danger-fgColor-hover', - '--control-danger-fgColor-rest', - '--control-fgColor-disabled', - '--control-fgColor-placeholder', - '--control-fgColor-rest', - '--control-iconColor-rest', - '--control-transparent-bgColor-active', - '--control-transparent-bgColor-disabled', - '--control-transparent-bgColor-hover', - '--control-transparent-bgColor-rest', - '--control-transparent-bgColor-selected', - '--control-transparent-borderColor-active', - '--control-transparent-borderColor-hover', - '--control-transparent-borderColor-rest', - ], - }, - { - category: 'focus', - tokens: ['--focus-outlineColor'], - }, - { - category: 'overlay', - tokens: ['--overlay-background-bgColor', '--overlay-bgColor', '--overlay-borderColor'], - }, - ], - }, - { - category: 'size', - tokens: [ - { - category: 'base', - tokens: [ - '--base-size-2', - '--base-size-4', - '--base-size-6', - '--base-size-8', - '--base-size-12', - '--base-size-16', - '--base-size-20', - '--base-size-24', - '--base-size-28', - '--base-size-32', - '--base-size-36', - '--base-size-40', - '--base-size-44', - '--base-size-48', - '--base-size-64', - '--base-size-80', - '--base-size-96', - '--base-size-112', - '--base-size-128', - ], - }, - { - category: 'border', - tokens: [ - { - category: 'border-size', - tokens: [ - '--boxShadow-thick', - '--boxShadow-thicker', - '--boxShadow-thin', - '--borderWidth-default', - '--borderWidth-thick', - '--borderWidth-thicker', - '--borderWidth-thin', - ], - }, - { - category: 'border-radius', - tokens: [ - '--borderRadius-default', - '--borderRadius-full', - '--borderRadius-large', - '--borderRadius-medium', - '--borderRadius-small', - ], - }, - { - category: 'outline', - tokens: ['--outline-focus-offset', '--outline-focus-width'], - }, - ], - }, - { - category: 'layout', - tokens: [ - { - category: 'stack', - tokens: [ - '--stack-gap-condensed', - '--stack-gap-normal', - '--stack-gap-spacious', - '--stack-padding-condensed', - '--stack-padding-normal', - '--stack-padding-spacious', - ], - }, - { - category: 'controls', - tokens: [ - '--controlStack-large-gap-auto', - '--controlStack-large-gap-condensed', - '--controlStack-large-gap-spacious', - '--controlStack-medium-gap-condensed', - '--controlStack-medium-gap-spacious', - '--controlStack-small-gap-condensed', - '--controlStack-small-gap-spacious', - '--controlStack-medium-gap-auto', - '--controlStack-small-gap-auto', - - '--control-xsmall-gap', - '--control-small-gap', - '--control-medium-gap', - '--control-large-gap', - '--control-xlarge-gap', - '--control-xsmall-lineBoxHeight', - '--control-small-lineBoxHeight', - '--control-medium-lineBoxHeight', - '--control-large-lineBoxHeight', - '--control-xlarge-lineBoxHeight', - '--control-xsmall-paddingBlock', - '--control-small-paddingBlock', - '--control-medium-paddingBlock', - '--control-large-paddingBlock', - '--control-xlarge-paddingBlock', - '--control-xsmall-paddingInline-condensed', - '--control-small-paddingInline-condensed', - '--control-medium-paddingInline-condensed', - '--control-large-paddingInline-condensed', - '--control-xlarge-paddingInline-condensed', - '--control-xsmall-paddingInline-normal', - '--control-small-paddingInline-normal', - '--control-medium-paddingInline-normal', - '--control-large-paddingInline-normal', - '--control-xlarge-paddingInline-normal', - '--control-xsmall-paddingInline-spacious', - '--control-small-paddingInline-spacious', - '--control-medium-paddingInline-spacious', - '--control-large-paddingInline-spacious', - '--control-xlarge-paddingInline-spacious', - '--control-xsmall-size', - '--control-small-size', - '--control-medium-size', - '--control-large-size', - '--control-xlarge-size', - ], - }, - { - category: 'overlay', - tokens: [ - '--overlay-borderRadius', - '--overlay-height-large', - '--overlay-height-medium', - '--overlay-height-small', - '--overlay-height-xlarge', - '--overlay-offset', - '--overlay-padding-condensed', - '--overlay-padding-normal', - '--overlay-paddingBlock-condensed', - '--overlay-paddingBlock-normal', - '--overlay-width-large', - '--overlay-width-medium', - '--overlay-width-small', - '--overlay-width-xlarge', - '--overlay-width-xsmall', - ], - }, - ], - }, - ], - }, - { - category: 'typography', - tokens: [ - { - category: 'weight', - tokens: [ - '--base-text-weight-light', - '--base-text-weight-normal', - '--base-text-weight-medium', - '--base-text-weight-semibold', - ], - }, - { - category: 'font-family', - tokens: [ - '--fontStack-monospace', - '--fontStack-sansSerif', - '--fontStack-sansSerifDisplay', - '--fontStack-system', - ], - }, - { - category: 'font-shorthand', - tokens: [ - '--text-body-shorthand-large', - '--text-body-shorthand-medium', - '--text-body-shorthand-small', - '--text-caption-shorthand', - '--text-codeBlock-shorthand', - '--text-codeInline-shorthand', - '--text-display-shorthand', - '--text-subtitle-shorthand', - '--text-title-shorthand-large', - '--text-title-shorthand-medium', - '--text-title-shorthand-small', - ], - }, - { - category: 'display', - tokens: [ - '--text-display-lineBoxHeight', - '--text-display-lineHeight', - '--text-display-size', - '--text-display-weight', - ], - }, - { - category: 'title-large', - tokens: ['--text-title-lineHeight-large', '--text-title-size-large', '--text-title-weight-large'], - }, - { - category: 'title-medium', - tokens: ['--text-title-lineHeight-medium', '--text-title-size-medium', '--text-title-weight-medium'], - }, - { - category: 'title-small', - tokens: ['--text-title-lineHeight-small', '--text-title-size-small', '--text-title-weight-small'], - }, - { - category: 'subtitle', - tokens: ['--text-subtitle-lineHeight', '--text-subtitle-size', '--text-subtitle-weight'], - }, - { - category: 'body-large', - tokens: ['--text-body-lineHeight-large', '--text-body-size-large'], - }, - { - category: 'body-medium', - tokens: ['--text-body-lineHeight-medium', '--text-body-size-medium'], - }, - { - category: 'body-small', - tokens: ['--text-body-lineHeight-small', '--text-body-size-small'], - }, - { - category: 'caption', - tokens: ['--text-caption-lineHeight', '--text-caption-size', '--text-caption-weight'], - }, - { - category: 'code-block', - tokens: ['--text-codeBlock-lineHeight', '--text-codeBlock-size', '--text-codeBlock-weight'], - }, - { - category: 'inline-code-block', - tokens: ['--text-codeInline-size', '--text-codeInline-weight'], - }, - ], - }, -] as const - -function serialize(token: Token): string { - if (typeof token === 'string') { - return `` - } - return `\n${token.tokens.map(serialize).join('\n')}\n` -} - // ----------------------------------------------------------------------------- // Design Tokens // ----------------------------------------------------------------------------- server.tool('list_tokens', 'List all of the design tokens available from Primer', async () => { let text = - 'Below is a list of all designs tokens available from Primer. They are organized by category. Tokens are used in CSS and CSS Modules. They can also be referred to in JavaScript files using the style attribute or prop in React components. To refer to the CSS Custom Property for a design token, wrap it in var(name-of-token). To learn how to use a specific token, use a corresponding usage tool for the category of the token. For example, if a token is a color token look for the get_color_usage tool. \n\n' + 'Below is a list of all design tokens available from Primer. Tokens are used in CSS and CSS Modules. To refer to the CSS Custom Property for a design token, wrap it in var(--{name-of-token}). To learn how to use a specific token, use a corresponding usage tool for the category of the token. For example, if a token is a color token look for the get_color_usage tool. \n\n' - for (const token of tokens) { - text += serialize(token) - text += '\n' - } + text += serialize(tokens) return { content: [ @@ -1005,4 +590,43 @@ ${text}`, }, ) +// ----------------------------------------------------------------------------- +// Coding guidelines +// ----------------------------------------------------------------------------- +server.tool( + 'primer_coding_guidelines', + 'Get the guidelines when writing code that uses Primer or for UI code that you are creating', + async () => { + return { + content: [ + { + type: 'text', + text: `When writing code that uses Primer, follow these guidelines: + +## Design Tokens + +- Prefer design tokens over hard-coded values. For example, use \`var(--fgColor-default)\` instead of \`#24292f\`. Use the \`list_tokens\` tool to find the design token you need. +- Prefer recommending design tokens in the same group for related CSS properties. For example, when styling background and border color, use tokens from the same group/category + +## Authoring & Using Components + +- Prefer re-using a component from Primer when possible over writing a new component. +- Prefer using existing props for a component for styling instead of adding styling to a component +- Prefer using icons from Primer instead of creating new icons. Use the \`list_icons\` tool to find the icon you need. +- Follow patterns from Primer when creating new components. Use the \`list_patterns\` tool to find the pattern you need, if one exists +- When using a component from Primer, make sure to follow the component's usage and accessibility guidelines + +## Coding guidelines + +The following list of coding guidelines must be followed: + +- Do not use the sx prop for styling components. Instead, use CSS Modules. +- Do not use the Box component for styling components. Instead, use CSS Modules. +`, + }, + ], + } + }, +) + export {server} From 870a8ca5b788eae8c257b2fb4877ca092e97a9d9 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Thu, 28 Aug 2025 16:09:18 +1000 Subject: [PATCH 31/46] Convert menu to disclosure pattern --- .../Breadcrumbs.features.stories.tsx | 25 +++++ .../src/Breadcrumbs/Breadcrumbs.module.css | 38 ++++++- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 103 +++++++++++++++--- 3 files changed, 145 insertions(+), 21 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx index 0510633a974..db9d67b9786 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx @@ -2,6 +2,7 @@ import type {Meta} from '@storybook/react-vite' import type React from 'react' import type {ComponentProps} from '../utils/types' import Breadcrumbs from './Breadcrumbs' +import TextInput from '../TextInput' export default { title: 'Components/Breadcrumbs/Features', @@ -103,3 +104,27 @@ export const WrappedBreadcrumbItemsWithOverflow = () => ( ) + +export const WithEditableNameInput = () => ( + + Home + Documents + Project Alpha + + + + +) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 31bc52059a5..2b5141bf09a 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -12,7 +12,6 @@ [data-overflow='menu'] .BreadcrumbsList { white-space: nowrap; - overflow: hidden; display: flex; flex-direction: row; } @@ -58,8 +57,41 @@ } } -.MenuButton { - color: var(--fgColor-link); +.MenuSummary { + list-style: none; + display: inline-block; + outline: none; +} + +.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: + 0 1px 3px rgba(0, 0, 0, 0.12), + 0 1px 2px rgba(0, 0, 0, 0.24); + border-radius: 12px; + background-color: var(--overlay-bgColor); + list-style: none; + width: max-content; } .ItemSeparator { diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 2f0396b4235..a30dc17222e 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -6,12 +6,15 @@ 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' -import {ActionMenu} from '../ActionMenu' +import Details from '../Details' import {ActionList} from '../ActionList' import {IconButton} from '../Button/IconButton' 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 {getAnchoredPosition} from '@primer/behaviors' const SELECTED_CLASS = 'selected' @@ -32,24 +35,83 @@ type BreadcrumbsMenuItemProps = { 'aria-label'?: string } -const BreadcrumbsMenuItem = React.forwardRef( - ({items, 'aria-label': ariaLabel, ...rest}, ref) => { +const BreadcrumbsMenuItem = React.forwardRef( + ({items, 'aria-label': ariaLabel, ...rest}, detailsRef) => { + const [isOpen, setIsOpen] = useState(false) + const [menuPosition, setMenuPosition] = useState<{top?: number; left?: number; right?: number}>({}) + + 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' in detailsRef && detailsRef.current) { + const newOpenState = !detailsRef.current.open + detailsRef.current.open = newOpenState + setIsOpen(newOpenState) + } + }, + [detailsRef], + ) + + const closeOverlay = useCallback(() => { + if (detailsRef && 'current' in detailsRef && detailsRef.current) { + detailsRef.current.open = false + setIsOpen(false) + } + }, [detailsRef]) + + const focusOnMenuButton = useCallback(() => { + iconButtonRef.current?.focus() + }, []) + + const iconButtonRef = useRef(null) + const menuContainerRef = useRef(null) + const summaryRef = useRef(null) + + // Calculate menu position when opening + useEffect(() => { + if (isOpen && summaryRef.current && menuContainerRef.current) { + const {top, left} = getAnchoredPosition(summaryRef.current, menuContainerRef.current, { + align: 'end', + side: 'outside-bottom', + }) + setMenuPosition({top, left}) + } + }, [isOpen]) + + useOnEscapePress( + (event: KeyboardEvent) => { + if (isOpen) { + event.preventDefault() + closeOverlay() + focusOnMenuButton() + } + }, + [isOpen], + ) + + useOnOutsideClick({ + onClickOutside: closeOverlay, + containerRef: menuContainerRef, + ignoreClickRefs: [iconButtonRef], + }) + return ( - - +
    + +
    + {items.map((item, index) => { const href = item.props.href const children = item.props.children @@ -58,16 +120,16 @@ const BreadcrumbsMenuItem = React.forwardRef {children} ) })} - - +
    +
    ) }, ) @@ -80,7 +142,7 @@ const getValidChildren = (children: React.ReactNode) => { function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRoot = true}: BreadcrumbsProps) { const containerRef = useRef(null) - const menuButtonRef = useRef(null) + const menuButtonRef = useRef(null) const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) //let effectiveOverflow = 'wrap' const childArray = useMemo(() => getValidChildren(children), [children]) @@ -115,9 +177,14 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // Measure actual menu button width when it exists useEffect(() => { if (menuButtonRef.current) { - const measuredWidth = menuButtonRef.current.offsetWidth - if (measuredWidth > 0) { - setMenuButtonWidth(measuredWidth) + const iconButtonElement = + menuButtonRef.current.querySelector('button[data-component="IconButton"]') || + menuButtonRef.current.querySelector('button') + if (iconButtonElement) { + const measuredWidth = (iconButtonElement as HTMLElement).offsetWidth + if (measuredWidth > 0) { + setMenuButtonWidth(measuredWidth) + } } } }, [menuItems.length]) // Re-measure when menu button appears/disappears From bc99a1717440ff3a3c02e2ca2b1a5724fc5a5b1b Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Thu, 28 Aug 2025 17:22:15 +1000 Subject: [PATCH 32/46] Fix bugs --- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index a30dc17222e..ec41fcd26be 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -166,13 +166,13 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo useEffect(() => { const listElement = containerRef.current?.querySelector('ol') - if (listElement && listElement.children.length > 0) { + 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.length]) + }, [childArray]) // Measure actual menu button width when it exists useEffect(() => { @@ -187,7 +187,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo } } } - }, [menuItems.length]) // Re-measure when menu button appears/disappears + }, [menuItems]) const calculateOverflow = useCallback( (availableWidth: number) => { @@ -263,12 +263,18 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo if (entries[0]) { const containerWidth = entries[0].contentRect.width const result = calculateOverflow(containerWidth) - setVisibleItems(result.visibleItems) - setMenuItems(result.menuItems) - setEffectiveHideRoot(result.effectiveHideRoot) + if ( + visibleItems.length !== result.visibleItems.length || + menuItems.length !== result.menuItems.length || + effectiveHideRoot !== result.effectiveHideRoot + ) { + setVisibleItems(result.visibleItems) + setMenuItems(result.menuItems) + setEffectiveHideRoot(result.effectiveHideRoot) + } } }, - [calculateOverflow], + [calculateOverflow, effectiveHideRoot, menuItems, visibleItems], ) useResizeObserver(handleResize, containerRef) @@ -283,7 +289,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo setMenuItems(result.menuItems) setEffectiveHideRoot(result.effectiveHideRoot) } - }, [overflow, childArray.length, calculateOverflow]) + }, [overflow, childArray, calculateOverflow]) // Determine final children to render const finalChildren = React.useMemo(() => { From 020f0b70143aedae29b8fe47a1390a797cf38541 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Fri, 29 Aug 2025 23:18:19 +1000 Subject: [PATCH 33/46] Fix up infinite loop at 1 remaining visible item --- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index ec41fcd26be..70bd91631ac 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -67,12 +67,10 @@ const BreadcrumbsMenuItem = React.forwardRef(null) const menuContainerRef = useRef(null) - const summaryRef = useRef(null) - // Calculate menu position when opening useEffect(() => { - if (isOpen && summaryRef.current && menuContainerRef.current) { - const {top, left} = getAnchoredPosition(summaryRef.current, menuContainerRef.current, { + if (isOpen && iconButtonRef.current && menuContainerRef.current) { + const {top, left} = getAnchoredPosition(iconButtonRef.current, menuContainerRef.current, { align: 'end', side: 'outside-bottom', }) @@ -99,7 +97,7 @@ const BreadcrumbsMenuItem = React.forwardRef - + { const listElement = containerRef.current?.querySelector('ol') @@ -174,7 +171,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo } }, [childArray]) - // Measure actual menu button width when it exists useEffect(() => { if (menuButtonRef.current) { const iconButtonElement = @@ -191,18 +187,19 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const calculateOverflow = useCallback( (availableWidth: number) => { - const MENU_BUTTON_WIDTH = menuButtonWidth // Use measured width with fallback + 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 !effectiveHideRoot ? rootItemWidth + widths : widths + return !eHideRoot ? rootItemWidth + widths : widths } let currentVisibleItems = [...childArray] let currentVisibleItemWidths = [...childArrayWidths] let currentMenuItems: React.ReactElement[] = [] let currentMenuItemsWidths: number[] = [] - let eHideRoot = effectiveHideRoot if (availableWidth > 0 && currentVisibleItemWidths.length > 0) { let visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths) @@ -232,7 +229,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // If hideRoot is false but we still don't fit with root + menu + leaf, // fallback to hideRoot=true behavior (menu + leaf only) - if (!hideRoot && currentVisibleItems.length === 1 && visibleItemsWidthTotal > availableWidth) { + if (currentVisibleItems.length === 1 && visibleItemsWidthTotal > availableWidth) { eHideRoot = true break } else { @@ -246,16 +243,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo effectiveHideRoot: eHideRoot, } }, - [ - MIN_VISIBLE_ITEMS, - childArray, - childArrayWidths, - effectiveHideRoot, - hideRoot, - overflow, - rootItemWidth, - menuButtonWidth, - ], + [childArray, childArrayWidths, effectiveHideRoot, hideRoot, overflow, rootItemWidth, menuButtonWidth], ) const handleResize = useCallback( @@ -264,9 +252,8 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const containerWidth = entries[0].contentRect.width const result = calculateOverflow(containerWidth) if ( - visibleItems.length !== result.visibleItems.length || - menuItems.length !== result.menuItems.length || - effectiveHideRoot !== result.effectiveHideRoot + (visibleItems.length !== result.visibleItems.length && menuItems.length !== result.menuItems.length) || + result.effectiveHideRoot !== effectiveHideRoot ) { setVisibleItems(result.visibleItems) setMenuItems(result.menuItems) @@ -281,7 +268,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // Initial overflow calculation for testing and >5 items useEffect(() => { - if (overflow === 'menu' && childArray.length > 5) { + if (overflow === 'menu' && childArray.length > 5 && menuItems.length === 0) { // Get actual container width from DOM or use default const containerWidth = containerRef.current?.offsetWidth || 800 const result = calculateOverflow(containerWidth) @@ -289,7 +276,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo setMenuItems(result.menuItems) setEffectiveHideRoot(result.effectiveHideRoot) } - }, [overflow, childArray, calculateOverflow]) + }, [overflow, childArray, calculateOverflow, menuItems.length]) // Determine final children to render const finalChildren = React.useMemo(() => { From 8cf263e7d121c11ed6594e9fb3e3583d7b6ccc0e Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Fri, 29 Aug 2025 23:23:03 +1000 Subject: [PATCH 34/46] Remove unnecessary comments --- packages/react/src/Breadcrumbs/Breadcrumbs.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 70bd91631ac..9e1d979f3f7 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -142,7 +142,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const containerRef = useRef(null) const menuButtonRef = useRef(null) const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) - //let effectiveOverflow = 'wrap' const childArray = useMemo(() => getValidChildren(children), [children]) const rootItem = childArray[0] @@ -153,7 +152,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const [menuItems, setMenuItems] = useState([]) const [rootItemWidth, setRootItemWidth] = useState(0) - // Menu button width with fallback const MENU_BUTTON_FALLBACK_WIDTH = 32 // Design system small IconButton const [menuButtonWidth, setMenuButtonWidth] = useState(MENU_BUTTON_FALLBACK_WIDTH) @@ -204,7 +202,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo if (availableWidth > 0 && currentVisibleItemWidths.length > 0) { let visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths) - // Add menu button width if we have hidden items if (currentMenuItems.length > 0) { visibleItemsWidthTotal += MENU_BUTTON_WIDTH } @@ -212,7 +209,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo overflow === 'menu' && (visibleItemsWidthTotal > availableWidth || currentVisibleItems.length > MIN_VISIBLE_ITEMS) ) { - // Remove the last visible item const itemToHide = currentVisibleItems.slice(0)[0] const itemToHideWidth = currentVisibleItemWidths.slice(0)[0] currentMenuItems = [...currentMenuItems, itemToHide] @@ -222,13 +218,10 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths) - // Add menu button width if (currentMenuItems.length > 0) { visibleItemsWidthTotal += MENU_BUTTON_WIDTH } - // If hideRoot is false but we still don't fit with root + menu + leaf, - // fallback to hideRoot=true behavior (menu + leaf only) if (currentVisibleItems.length === 1 && visibleItemsWidthTotal > availableWidth) { eHideRoot = true break @@ -266,10 +259,8 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo useResizeObserver(handleResize, containerRef) - // Initial overflow calculation for testing and >5 items useEffect(() => { if (overflow === 'menu' && childArray.length > 5 && menuItems.length === 0) { - // Get actual container width from DOM or use default const containerWidth = containerRef.current?.offsetWidth || 800 const result = calculateOverflow(containerWidth) setVisibleItems(result.visibleItems) @@ -278,7 +269,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo } }, [overflow, childArray, calculateOverflow, menuItems.length]) - // Determine final children to render const finalChildren = React.useMemo(() => { if (overflow === 'wrap' || menuItems.length === 0) { return React.Children.map(children, child =>
  • {child}
  • ) @@ -313,7 +303,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo ) - // Position menu based on effective hideRoot setting and visible items if (effectiveHideRoot) { // Show: [overflow menu, leaf breadcrumb] return [menuElement, ...visibleElements] From 23ddd0ecca352cf57ed8fd9ecc94cc8b02936253 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Fri, 29 Aug 2025 23:33:40 +1000 Subject: [PATCH 35/46] Use ref callback for menu width calculation --- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 9e1d979f3f7..c8b358a230d 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -140,7 +140,17 @@ const getValidChildren = (children: React.ReactNode) => { function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRoot = true}: BreadcrumbsProps) { const containerRef = useRef(null) - const menuButtonRef = 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 [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) const childArray = useMemo(() => getValidChildren(children), [children]) @@ -169,20 +179,6 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo } }, [childArray]) - useEffect(() => { - if (menuButtonRef.current) { - const iconButtonElement = - menuButtonRef.current.querySelector('button[data-component="IconButton"]') || - menuButtonRef.current.querySelector('button') - if (iconButtonElement) { - const measuredWidth = (iconButtonElement as HTMLElement).offsetWidth - if (measuredWidth > 0) { - setMenuButtonWidth(measuredWidth) - } - } - } - }, [menuItems]) - const calculateOverflow = useCallback( (availableWidth: number) => { let eHideRoot = effectiveHideRoot @@ -281,7 +277,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const menuElement = (
  • @@ -310,7 +306,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo // Show: [root breadcrumb, overflow menu, leaf breadcrumb] return [rootElement, menuElement, ...visibleElements] } - }, [overflow, menuItems, effectiveHideRoot, visibleItems, rootItem, children]) + }, [overflow, menuItems, effectiveHideRoot, visibleItems, rootItem, children, measureMenuButton]) return ( Date: Mon, 1 Sep 2025 11:58:25 +1000 Subject: [PATCH 36/46] Fix up review comments --- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index c8b358a230d..0d487eb5f17 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -14,7 +14,6 @@ import {useResizeObserver} from '../hooks/useResizeObserver' import type {ResizeObserverEntry} from '../hooks/useResizeObserver' import {useOnEscapePress} from '../hooks/useOnEscapePress' import {useOnOutsideClick} from '../hooks/useOnOutsideClick' -import {getAnchoredPosition} from '@primer/behaviors' const SELECTED_CLASS = 'selected' @@ -36,16 +35,24 @@ type BreadcrumbsMenuItemProps = { } const BreadcrumbsMenuItem = React.forwardRef( - ({items, 'aria-label': ariaLabel, ...rest}, detailsRef) => { + ({items, 'aria-label': ariaLabel, ...rest}, menuRefCallback) => { const [isOpen, setIsOpen] = useState(false) - const [menuPosition, setMenuPosition] = useState<{top?: number; left?: number; right?: number}>({}) - + const detailsRef = 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' in detailsRef && detailsRef.current) { + if (detailsRef.current) { const newOpenState = !detailsRef.current.open detailsRef.current.open = newOpenState setIsOpen(newOpenState) @@ -55,7 +62,7 @@ const BreadcrumbsMenuItem = React.forwardRef { - if (detailsRef && 'current' in detailsRef && detailsRef.current) { + if (detailsRef.current) { detailsRef.current.open = false setIsOpen(false) } @@ -68,16 +75,6 @@ const BreadcrumbsMenuItem = React.forwardRef(null) const menuContainerRef = useRef(null) - useEffect(() => { - if (isOpen && iconButtonRef.current && menuContainerRef.current) { - const {top, left} = getAnchoredPosition(iconButtonRef.current, menuContainerRef.current, { - align: 'end', - side: 'outside-bottom', - }) - setMenuPosition({top, left}) - } - }, [isOpen]) - useOnEscapePress( (event: KeyboardEvent) => { if (isOpen) { @@ -96,11 +93,12 @@ const BreadcrumbsMenuItem = React.forwardRef +
    -
    +
    {items.map((item, index) => { const href = item.props.href @@ -205,12 +203,12 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo overflow === 'menu' && (visibleItemsWidthTotal > availableWidth || currentVisibleItems.length > MIN_VISIBLE_ITEMS) ) { - const itemToHide = currentVisibleItems.slice(0)[0] - const itemToHideWidth = currentVisibleItemWidths.slice(0)[0] + const itemToHide = currentVisibleItems[0] + const itemToHideWidth = currentVisibleItemWidths[0] currentMenuItems = [...currentMenuItems, itemToHide] currentMenuItemsWidths = [...currentMenuItemsWidths, itemToHideWidth] - currentVisibleItems = [...currentVisibleItems.slice(1)] - currentVisibleItemWidths = [...currentVisibleItemWidths.slice(1)] + currentVisibleItems = currentVisibleItems.slice(1) + currentVisibleItemWidths = currentVisibleItemWidths.slice(1) visibleItemsWidthTotal = calculateVisibleItemsWidth(currentVisibleItemWidths) @@ -227,8 +225,8 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo } } return { - visibleItems: [...currentVisibleItems], - menuItems: [...currentMenuItems], + visibleItems: currentVisibleItems, + menuItems: currentMenuItems, effectiveHideRoot: eHideRoot, } }, From 2e367bf0fa52c83d2e3f8e629f3fa212b11fbc1f Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 1 Sep 2025 12:53:27 +1000 Subject: [PATCH 37/46] Add feature flags --- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 104 ++++++++++-------- .../src/FeatureFlags/DefaultFeatureFlags.ts | 1 + 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 0d487eb5f17..fa927ed6bcf 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -14,6 +14,7 @@ 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' const SELECTED_CLASS = 'selected' @@ -137,6 +138,8 @@ const getValidChildren = (children: React.ReactNode) => { } function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRoot = true}: BreadcrumbsProps) { + const overflowMenuEnabled = useFeatureFlag('primer_react_breadcrumbs_overflow_menu') + const wrappedChildren = React.Children.map(children, child =>
  • {child}
  • ) const containerRef = useRef(null) const measureMenuButton = useCallback((element: HTMLDetailsElement | null) => { @@ -169,13 +172,18 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo useEffect(() => { const listElement = containerRef.current?.querySelector('ol') - if (listElement && listElement.children.length > 0 && listElement.children.length === childArray.length) { + 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]) + }, [childArray, overflowMenuEnabled]) const calculateOverflow = useCallback( (availableWidth: number) => { @@ -235,7 +243,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo const handleResize = useCallback( (entries: ResizeObserverEntry[]) => { - if (entries[0]) { + if (overflowMenuEnabled && entries[0]) { const containerWidth = entries[0].contentRect.width const result = calculateOverflow(containerWidth) if ( @@ -248,65 +256,67 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo } } }, - [calculateOverflow, effectiveHideRoot, menuItems, visibleItems], + [calculateOverflow, effectiveHideRoot, menuItems.length, overflowMenuEnabled, visibleItems.length], ) useResizeObserver(handleResize, containerRef) useEffect(() => { - if (overflow === 'menu' && childArray.length > 5 && menuItems.length === 0) { + if (overflowMenuEnabled && overflow === 'menu' && 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]) + }, [overflow, childArray, calculateOverflow, menuItems.length, overflowMenuEnabled]) const finalChildren = React.useMemo(() => { - if (overflow === 'wrap' || menuItems.length === 0) { - return React.Children.map(children, child =>
  • {child}
  • ) - } - - let effectiveMenuItems = [...menuItems] - if (!effectiveHideRoot) { - effectiveMenuItems = [...menuItems.slice(1)] - } - const menuElement = ( -
  • - - -
  • - ) - - const visibleElements = visibleItems.map((child, index) => ( -
  • - {child} - -
  • - )) - - const rootElement = ( -
  • - {rootItem} - -
  • - ) + if (overflowMenuEnabled) { + if (overflow === 'wrap' || menuItems.length === 0) { + return React.Children.map(children, child =>
  • {child}
  • ) + } - 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] + 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] + } } - }, [overflow, menuItems, effectiveHideRoot, visibleItems, rootItem, children, measureMenuButton]) + }, [overflowMenuEnabled, overflow, menuItems, effectiveHideRoot, measureMenuButton, visibleItems, rootItem, children]) - return ( + return overflowMenuEnabled ? ( {finalChildren} + ) : ( + {wrappedChildren} ) } 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, From bb5aa77c36788bd6fe43b33351f60c878a209e40 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 1 Sep 2025 13:01:56 +1000 Subject: [PATCH 38/46] Delete .changeset/good-cougars-hug.md --- .changeset/good-cougars-hug.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/good-cougars-hug.md diff --git a/.changeset/good-cougars-hug.md b/.changeset/good-cougars-hug.md deleted file mode 100644 index 27786935bdc..00000000000 --- a/.changeset/good-cougars-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@primer/react": patch ---- - -Spike for breadcrumbs overflow From b8063b8892614b60f97f519301afda8eff9b140b Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 1 Sep 2025 13:11:39 +1000 Subject: [PATCH 39/46] Add different prop to hideRoot --- .../src/Breadcrumbs/Breadcrumbs.features.stories.tsx | 8 ++++---- packages/react/src/Breadcrumbs/Breadcrumbs.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx index db9d67b9786..7e86655a813 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx @@ -28,9 +28,9 @@ export const OverflowMenu = () => ( Home Products Category - SubcategorySubcategorySubcategorySubcategory - ItemItemItemItemItemItemItem - DetailsDetailsDetailsDetailsDetailsDetailsDetails + Subcategory + Item + Details Current Page @@ -38,7 +38,7 @@ export const OverflowMenu = () => ( ) export const OverflowMenuShowRoot = () => ( - + github Teams Engineering diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index fa927ed6bcf..43e0c1537d1 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -21,8 +21,7 @@ const SELECTED_CLASS = 'selected' export type BreadcrumbsProps = React.PropsWithChildren< { className?: string - overflow?: 'wrap' | 'menu' - hideRoot?: boolean + overflow?: 'wrap' | 'menu' | 'menu-with-root' } & SxProp > @@ -137,7 +136,7 @@ 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', hideRoot = true}: BreadcrumbsProps) { +function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap'}: BreadcrumbsProps) { const overflowMenuEnabled = useFeatureFlag('primer_react_breadcrumbs_overflow_menu') const wrappedChildren = React.Children.map(children, child =>
  • {child}
  • ) const containerRef = useRef(null) @@ -152,6 +151,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap', hideRo } }, []) + const hideRoot = overflow === 'menu-with-root' const [effectiveHideRoot, setEffectiveHideRoot] = useState(hideRoot) const childArray = useMemo(() => getValidChildren(children), [children]) From c7669d6dd95962b000fc15c674496a753627649d Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 1 Sep 2025 16:08:03 +1000 Subject: [PATCH 40/46] Add tests --- .../src/Breadcrumbs/Breadcrumbs.docs.json | 11 +- .../src/Breadcrumbs/Breadcrumbs.module.css | 7 +- .../src/Breadcrumbs/Breadcrumbs.stories.tsx | 97 ++++++ .../react/src/Breadcrumbs/Breadcrumbs.tsx | 17 +- .../__tests__/Breadcrumbs.test.tsx | 290 +++++++++++++++--- 5 files changed, 360 insertions(+), 62 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.docs.json b/packages/react/src/Breadcrumbs/Breadcrumbs.docs.json index ed4c7d120c3..41321620375 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.docs.json +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.docs.json @@ -25,18 +25,11 @@ }, { "name": "overflow", - "type": "'wrap' | 'menu'", + "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.", + "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": "hideRoot", - "type": "boolean", - "required": false, - "description": "When using overflow='menu', whether to prioritize hiding the root (first) item when space is limited. If false, the root item will be kept visible when possible.", - "defaultValue": "true" - }, { "name": "sx", "type": "SystemStyleObject", diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 2b5141bf09a..218a11ebce5 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -10,7 +10,8 @@ margin-bottom: 0; } -[data-overflow='menu'] .BreadcrumbsList { +[data-overflow='menu'] .BreadcrumbsList, +[data-overflow='menu-with-root'] .BreadcrumbsList { white-space: nowrap; display: flex; flex-direction: row; @@ -37,7 +38,8 @@ } } -[data-overflow='menu'] .Item { +[data-overflow='menu'] .Item, +[data-overflow='menu-with-root'] .Item { display: inline-block; font-size: var(--text-body-size-medium); color: var(--fgColor-link); @@ -61,6 +63,7 @@ list-style: none; display: inline-block; outline: none; + height: 0.5rem; } .MenuSummary::-webkit-details-marker { diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.stories.tsx index 16be2bd7bff..8480d66eb5c 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, {useState} from 'react' import type {ComponentProps} from '../utils/types' import Breadcrumbs from './Breadcrumbs' @@ -16,3 +17,99 @@ export const Default = () => (
    ) + +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 ( +
    +
    + + + + +
    + +
    +

    Overflow: wrap (default)

    + + {items.map((item, index) => ( + + {item.name} + + ))} + +
    + +
    +

    Overflow: menu

    + + {items.map((item, index) => ( + + {item.name} + + ))} + +
    + +
    +

    Overflow: menu

    + + {items.map((item, index) => ( + + {item.name} + + ))} + +
    + +
    + Current items: {items.length} | Try adding/removing items to see how overflow behavior changes +
    +
    + ) +} diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 43e0c1537d1..8ca176e349c 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -98,6 +98,7 @@ const BreadcrumbsMenuItem = React.forwardRef(hideRoot) const childArray = useMemo(() => getValidChildren(children), [children]) @@ -208,7 +209,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap'}: Bread visibleItemsWidthTotal += MENU_BUTTON_WIDTH } while ( - overflow === 'menu' && + (overflow === 'menu' || overflow === 'menu-with-root') && (visibleItemsWidthTotal > availableWidth || currentVisibleItems.length > MIN_VISIBLE_ITEMS) ) { const itemToHide = currentVisibleItems[0] @@ -262,7 +263,12 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap'}: Bread useResizeObserver(handleResize, containerRef) useEffect(() => { - if (overflowMenuEnabled && overflow === 'menu' && childArray.length > 5 && menuItems.length === 0) { + 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) @@ -278,6 +284,7 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap'}: Bread } 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)] } @@ -328,7 +335,9 @@ function Breadcrumbs({className, children, sx: sxProp, overflow = 'wrap'}: Bread {finalChildren} ) : ( - {wrappedChildren} + + {wrappedChildren} + ) } diff --git a/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx b/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx index 3a9d8d7c063..0ef9460e7cc 100644 --- a/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx +++ b/packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx @@ -1,14 +1,21 @@ import Breadcrumbs from '..' -import type React from 'react' -import {render as HTMLRender, screen, waitFor} from '@testing-library/react' +import {render as HTMLRender, screen, waitFor, within} from '@testing-library/react' import {describe, expect, it, vi} from 'vitest' import userEvent from '@testing-library/user-event' import {ThemeProvider} from '../../ThemeProvider' +import {FeatureFlags} from '../../FeatureFlags' import theme from '../../theme' -// Helper function to render with theme -const renderWithTheme = (component: React.ReactElement) => { - return HTMLRender({component}) +// 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 @@ -23,12 +30,6 @@ globalThis.ResizeObserver = vi.fn().mockImplementation(() => ({ })) describe('Breadcrumbs', () => { - it('should support `className` on the outermost element', () => { - expect(HTMLRender().container.firstChild).toHaveClass( - 'test-class-name', - ) - }) - it('renders a
    -

    Overflow: wrap (default)

    - - {items.map((item, index) => ( - - {item.name} - - ))} - -
    - -
    -

    Overflow: menu

    - - {items.map((item, index) => ( - - {item.name} - - ))} - -
    - -
    -

    Overflow: menu

    +

    + Dynamic breadcrumbs +

    {items.map((item, index) => ( From 92b813e44e20361ed6d7153cd8d923dcb964170f Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Mon, 1 Sep 2025 21:31:02 +1000 Subject: [PATCH 44/46] Story color needs changed --- packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx index 4e47d5b05b1..18dfff2137e 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.features.stories.tsx @@ -199,7 +199,7 @@ export const DynamicChildren = () => {
    -
    +
    Current items: {items.length} | Try adding/removing items to see how overflow behavior changes
    From 2854259e548d86b990b20045240cccf62cf22c74 Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Wed, 3 Sep 2025 16:19:43 +1000 Subject: [PATCH 45/46] Some css improvements --- .../src/Breadcrumbs/Breadcrumbs.module.css | 36 +++++++++---------- .../react/src/Breadcrumbs/Breadcrumbs.tsx | 7 ++-- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css index 6a33f30660c..dc6f8db5d33 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.module.css +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.module.css @@ -42,7 +42,7 @@ [data-overflow='menu-with-root'] .Item { display: inline-block; font-size: var(--text-body-size-medium); - color: var(--fgColor-link); + color: var(--fgColor-default); text-decoration: none; padding-inline: var(--base-size-6); padding-block: var(--base-size-4); @@ -50,49 +50,46 @@ &:hover { background: var(--control-transparent-bgColor-hover); + text-decoration: none; } &:focus { - box-shadow: none; - outline: 2px solid var(--focus-outlineColor, var(--color-accent-fg)); - outline-offset: -2px; + @mixin focusOutline; } } -.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 { +summary { list-style: none; cursor: pointer; outline: none; } -.MenuDetails summary::-webkit-details-marker { +summary::-webkit-details-marker { display: none; } .MenuOverlay { position: absolute; z-index: 1; - box-shadow: var(--shadow-resting-medium); + box-shadow: var(--shadow-resting-small); border-radius: var(--borderRadius-large); background-color: var(--overlay-bgColor); list-style: none; - width: max-content; + min-width: var(--overlay-width-xsmall); + max-height: 100vh; + max-width: var(--overlay-width-small); + overflow: hidden; +} + +@media (prefers-reduced-motion: no-preference) { + .MenuOverlay { + animation: overlay-appear 200ms cubic-bezier(0.33, 1, 0.68, 1); + } } .ItemSeparator { @@ -107,7 +104,6 @@ .ItemWrapper { display: inline-block; font-size: var(--text-body-size-medium); - white-space: nowrap; list-style: none; &::after { diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index 8ca176e349c..a5770e311a7 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -9,6 +9,7 @@ import {BoxWithFallback} from '../internal/components/BoxWithFallback' 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' @@ -94,19 +95,19 @@ const BreadcrumbsMenuItem = React.forwardRef - + - +
    {items.map((item, index) => { From b2e8931e2cb51de2f593e8366fb099a0422cc44d Mon Sep 17 00:00:00 2001 From: Pavithra Kodmad Date: Thu, 4 Sep 2025 16:04:26 +1000 Subject: [PATCH 46/46] Fix the button role --- packages/react/src/Breadcrumbs/Breadcrumbs.tsx | 1 + packages/react/src/Breadcrumbs/__tests__/Breadcrumbs.test.tsx | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx index a5770e311a7..1d62fe02400 100644 --- a/packages/react/src/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/react/src/Breadcrumbs/Breadcrumbs.tsx @@ -98,6 +98,7 @@ const BreadcrumbsMenuItem = React.forwardRef { // Press Escape key await user.keyboard('{Escape}') // sometimes tooltip swallows this escape - await user.keyboard('{Escape}') // Verify menu is closed await waitFor(() => { expect(menuButton).toHaveAttribute('aria-expanded', 'false') }) - - // Verify focus returns to menu button - expect(menuButton).toHaveFocus() }) it('closes menu when clicking outside', async () => {