diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts index 866a05108..eafd22f3e 100644 --- a/apps/www/src/content/docs/components/breadcrumb/demo.ts +++ b/apps/www/src/content/docs/components/breadcrumb/demo.ts @@ -98,9 +98,9 @@ export const asDemo = { type: 'code', code: ` - }>Home + Home - }>Playground + Playground Docs ` diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx index 0b2449853..e14bfc29b 100644 --- a/apps/www/src/content/docs/components/breadcrumb/index.mdx +++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx @@ -42,7 +42,7 @@ Groups all parts of the breadcrumb navigation. ### Item -Renders an individual breadcrumb link. +Renders an individual breadcrumb link. Ref is forwarded to the link or custom component when using `as` (not when using `dropdownItems`). @@ -94,9 +94,12 @@ Breadcrumb items can include dropdown menus for additional navigation options. S ### As -Use the `as` prop to render the breadcrumb item as a custom component. By default, breadcrumb items are rendered as `a` tags. +Use the `as` prop to render the breadcrumb item as a custom component. Pass the **component reference** (e.g. `as={NextLink}`), not a JSX element. Put `href` and other props on `Breadcrumb.Item`; they are passed through to the component. By default, items render as `a` tags. -When a custom component is provided, the props are merged, with the custom component's props taking precedence over the breadcrumb item's props. +```tsx +// Correct: pass the component, set href on the item +Home +``` diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts index 02b7d4ed9..6507786fa 100644 --- a/apps/www/src/content/docs/components/breadcrumb/props.ts +++ b/apps/www/src/content/docs/components/breadcrumb/props.ts @@ -1,4 +1,4 @@ -import { ReactElement, ReactEventHandler, ReactNode } from 'react'; +import { ElementType, ReactEventHandler, ReactNode } from 'react'; export interface BreadcrumbItem { /** Text to display for the item */ @@ -29,13 +29,12 @@ export interface BreadcrumbItem { }[]; /** - * Custom element used to render the Item. + * Component to render as (e.g. NextLink). Pass the component reference, not a JSX element. + * Receives breadcrumb item props (href, className, ref, children, etc.). * - * All props are merged, with the custom component's props taking precedence over the breadcrumb item's props. - * - * @default "" + * @default "a" */ - as?: ReactElement; + as?: ElementType; } export interface BreadcrumbProps { diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx index 95b9587d7..ce49aa652 100644 --- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen } from '@testing-library/react'; +import React, { forwardRef } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { Breadcrumb } from '../breadcrumb'; import styles from '../breadcrumb.module.css'; @@ -135,24 +136,32 @@ describe('Breadcrumb', () => { ); - const link = container.querySelector('a'); - expect(link).toHaveClass(styles['breadcrumb-link-active']); + // Current page renders as (non-link), not + const currentEl = container.querySelector( + `span.${styles['breadcrumb-link-active']}` + ); + expect(currentEl).toBeInTheDocument(); + expect(currentEl).toHaveAttribute('aria-current', 'page'); }); it('renders with custom element using as prop', () => { - const CustomLink = ({ children, ...props }: any) => ( - + const CustomLink: React.ComponentType< + React.AnchorHTMLAttributes + > = ({ children, ...props }) => ( + + {children} + ); const { container } = render( - }>Custom + Custom ); - const button = container.querySelector('button'); - expect(button).toBeInTheDocument(); - expect(button).toHaveClass(styles['breadcrumb-link']); + const link = container.querySelector('[data-custom-link]'); + expect(link).toBeInTheDocument(); + expect(link).toHaveClass(styles['breadcrumb-link']); expect(screen.getByText('Custom')).toBeInTheDocument(); }); @@ -166,6 +175,26 @@ describe('Breadcrumb', () => { expect(ref).toHaveBeenCalled(); }); + it('forwards ref when using as prop (component)', () => { + const CustomLink = forwardRef< + HTMLAnchorElement, + React.AnchorHTMLAttributes + >(({ children, ...props }, ref) => ( + + {children} + + )); + const ref = vi.fn(); + render( + + + Custom + + + ); + expect(ref).toHaveBeenCalled(); + }); + it('applies custom className', () => { const { container } = render( diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 0b3471ac2..0a8fc1a30 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -2,13 +2,7 @@ import { ChevronDownIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import React, { - cloneElement, - forwardRef, - HTMLAttributes, - ReactElement, - ReactNode -} from 'react'; +import React, { forwardRef, HTMLAttributes, ReactNode } from 'react'; import { Menu } from '../menu'; import styles from './breadcrumb.module.css'; @@ -22,7 +16,8 @@ export interface BreadcrumbItemProps extends HTMLAttributes { current?: boolean; dropdownItems?: BreadcrumbDropdownItem[]; href?: string; - as?: ReactElement; + /** Component to render as (e.g. NextLink). Receives our props (href, className, ref, etc.). Defaults to "a". */ + as?: React.ElementType; } export const BreadcrumbItem = forwardRef< @@ -42,14 +37,15 @@ export const BreadcrumbItem = forwardRef< }, ref ) => { - const renderedElement = as ?? ; - const label = ( + const Component = as ?? 'a'; + // Only wrap in spans when needed for layout (leading icon); otherwise keep link content as plain text so DOM shows Home not Home + const label = leadingIcon ? ( <> - {leadingIcon && ( - {leadingIcon} - )} - {children && {children}} + {leadingIcon} + {children != null && {children}} + ) : ( + children ); if (dropdownItems) { @@ -73,21 +69,35 @@ export const BreadcrumbItem = forwardRef< ); } + // Current page: render as non-link (semantic; no href) + if (current) { + return ( +
  • + } + className={cx( + styles['breadcrumb-link'], + styles['breadcrumb-link-active'] + )} + aria-current='page' + {...props} + > + {label} + +
  • + ); + } + // Link: render as or custom Component (e.g. NextLink → in DOM) return (
  • - {cloneElement( - renderedElement, - { - className: cx( - styles['breadcrumb-link'], - current && styles['breadcrumb-link-active'] - ), - href, - ...props, - ...renderedElement.props - }, - label - )} + + {label} +
  • ); } diff --git a/packages/raystack/components/breadcrumb/breadcrumb-root.tsx b/packages/raystack/components/breadcrumb/breadcrumb-root.tsx index 2fdded41e..0e4705b96 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-root.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-root.tsx @@ -1,7 +1,7 @@ 'use client'; -import { type VariantProps, cva } from 'class-variance-authority'; -import { HTMLAttributes, forwardRef } from 'react'; +import { cva, type VariantProps } from 'class-variance-authority'; +import { forwardRef, HTMLAttributes } from 'react'; import styles from './breadcrumb.module.css'; const breadcrumbVariants = cva(styles['breadcrumb'], { @@ -21,11 +21,15 @@ export interface BreadcrumbProps HTMLAttributes {} export const BreadcrumbRoot = forwardRef( - ({ className, children, size = 'medium', ...props }, ref) => { + ( + { className, children, size = 'medium', 'aria-label': ariaLabel, ...props }, + ref + ) => { return (