import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbEllipsis, BreadcrumbPage, BreadcrumbList, } from './Breadcrumb' export type CollapsibleBreadcrumbItemProps = { label?: string href?: string } export type CollapsibleBreadcrumbProps = { /** * Array of breadcrumb items to render. * Each item should be an object with the following properties: * - label: The text to display. * - href: The URL to navigate to. */ items: CollapsibleBreadcrumbItemProps[] /** * A render function to be used for rendering links, e.g., with a custom Link component. * It should accept a label and an href, and return a React element. */ renderLink?: ({ label, href, className, children, }: { href: string className?: string } & ( | { label?: never children?: React.ReactNode } | { label?: string children?: never } )) => React.ReactNode /** * Whether the ellipsis dropdown should be open by default. */ defaultOpen?: boolean } type DisplaySlot = | { type: 'link'; index: number } | { type: 'ellipsis'; collapsedIndices: number[] } | { type: 'page'; index: number } /** * Returns an array of display slots for the breadcrumb. * - For 4 or fewer items: all items are shown, with the last one marked as the current page. * - For more than 4 items: display the first item as a link, an ellipsis with collapsed items, * the second-to-last item as a link, and the last item as the current page. * Rule: First and last two page links are shown, remaining items are collapsed into ellipsis. */ const getDisplaySlots = (total: number): DisplaySlot[] => { if (total <= 4) { return Array.from({ length: total }, (_, index) => { if (index === total - 1) { return { type: 'page', index } } return { type: 'link', index } }) } // Collapsed items are from index 1 to total-3 (inclusive) const collapsedIndices = Array.from({ length: total - 3 }, (_, i) => i + 1) return [ { type: 'link', index: 0 }, { type: 'ellipsis', collapsedIndices }, { type: 'link', index: total - 2 }, { type: 'page', index: total - 1 }, ] } const renderBreadcrumbContent = ( slot: DisplaySlot, items: CollapsibleBreadcrumbItemProps[], renderLink?: CollapsibleBreadcrumbProps['renderLink'], defaultOpen?: boolean, ) => { if (slot.type === 'ellipsis') { const collapsedItems = slot.collapsedIndices.map((index) => ({ href: items[index]?.href, label: items[index]?.label, })) return ( renderLink({ href, children }) : undefined } /> ) } if (slot.type === 'page') { return {items[slot.index]?.label} } if (renderLink) { return ( {renderLink({ label: items[slot.index]?.label ?? '', href: items[slot.index]?.href ?? '', })} ) } return ( {items[slot.index]?.label} ) } export const CollapsibleBreadcrumb = ({ items, renderLink, defaultOpen, }: CollapsibleBreadcrumbProps) => { const displaySlots = getDisplaySlots(items.length) return ( {displaySlots.map((slot, index) => ( {renderBreadcrumbContent(slot, items, renderLink, defaultOpen)} {(displaySlots.length === 1 || index < displaySlots.length - 1) && ( )} ))} ) }