|
| 1 | +import * as React from 'react' |
| 2 | +import { cn } from '@/lib/utils' |
| 3 | + |
| 4 | +interface BreadcrumbProps extends React.ComponentPropsWithoutRef<'nav'> { |
| 5 | + children: |
| 6 | + | React.ReactElement<typeof BreadcrumbItem> |
| 7 | + | React.ReactElement<typeof BreadcrumbItem>[] |
| 8 | + separator?: React.ReactNode |
| 9 | +} |
| 10 | + |
| 11 | +const BreadcrumbContext = React.createContext<boolean>(false) |
| 12 | + |
| 13 | +const Breadcrumb = React.forwardRef<HTMLElement, BreadcrumbProps>( |
| 14 | + ({ className, children, separator, ...props }, ref) => { |
| 15 | + const validChildren = getValidChildren(children) |
| 16 | + |
| 17 | + const count = validChildren.length |
| 18 | + |
| 19 | + const clones = validChildren.map((child, index) => |
| 20 | + React.cloneElement(child, { |
| 21 | + separator, |
| 22 | + isLastChild: count === index + 1, |
| 23 | + }) |
| 24 | + ) |
| 25 | + |
| 26 | + return ( |
| 27 | + <BreadcrumbContext.Provider value={true}> |
| 28 | + <nav ref={ref} aria-label='breadcrumb' className={className} {...props}> |
| 29 | + <ol className={cn(`flex`)}>{clones}</ol> |
| 30 | + </nav> |
| 31 | + </BreadcrumbContext.Provider> |
| 32 | + ) |
| 33 | + } |
| 34 | +) |
| 35 | +Breadcrumb.displayName = 'Breadcrumb' |
| 36 | + |
| 37 | +interface InternalBreadcrumbItemProps { |
| 38 | + separator?: React.ReactNode |
| 39 | + isLastChild: boolean |
| 40 | +} |
| 41 | + |
| 42 | +interface BreadcrumbItemProps |
| 43 | + extends Omit< |
| 44 | + React.ComponentPropsWithoutRef<'li'>, |
| 45 | + keyof InternalBreadcrumbItemProps |
| 46 | + > {} |
| 47 | + |
| 48 | +const BreadcrumbItem = React.forwardRef<HTMLLIElement, BreadcrumbItemProps>( |
| 49 | + ({ className, children, ...props }, ref) => { |
| 50 | + const { separator, isLastChild, ...rest } = |
| 51 | + props as InternalBreadcrumbItemProps |
| 52 | + |
| 53 | + // Check if BreadcrumbItem is used within Breadcrumb |
| 54 | + const isInsideBreadcrumb = React.useContext(BreadcrumbContext) |
| 55 | + if (!isInsideBreadcrumb) { |
| 56 | + throw new Error( |
| 57 | + `${BreadcrumbItem.displayName} must be used within ${Breadcrumb.displayName}.` |
| 58 | + ) |
| 59 | + } |
| 60 | + |
| 61 | + return ( |
| 62 | + <li ref={ref} className={cn(`group`, className)} {...rest}> |
| 63 | + {children} |
| 64 | + {!isLastChild && ( |
| 65 | + <span className='mx-2 *:!inline-block'>{separator ?? '/'}</span> |
| 66 | + )} |
| 67 | + </li> |
| 68 | + ) |
| 69 | + } |
| 70 | +) |
| 71 | +BreadcrumbItem.displayName = 'BreadcrumbItem' |
| 72 | + |
| 73 | +/* ========== Util Func ========== */ |
| 74 | + |
| 75 | +const getValidChildren = (children: React.ReactNode) => |
| 76 | + React.Children.toArray(children).filter((child) => { |
| 77 | + if (React.isValidElement(child) && child.type === BreadcrumbItem) { |
| 78 | + return React.isValidElement(child) |
| 79 | + } |
| 80 | + throw new Error( |
| 81 | + `${Breadcrumb.displayName} can only have ${BreadcrumbItem.displayName} as children.` |
| 82 | + ) |
| 83 | + }) as React.ReactElement[] |
| 84 | + |
| 85 | +export { Breadcrumb, BreadcrumbItem } |
0 commit comments