diff --git a/apps/www/src/components/playground/breadcrumb-examples.tsx b/apps/www/src/components/playground/breadcrumb-examples.tsx index 9d9fbb7f1..86e4c8f7c 100644 --- a/apps/www/src/components/playground/breadcrumb-examples.tsx +++ b/apps/www/src/components/playground/breadcrumb-examples.tsx @@ -29,7 +29,7 @@ export function BreadcrumbExamples() { Products - } current> + } current> Shoes diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts index 866a05108..e2d5948d8 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..63a53ae4f 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 rendered element when using `render` (not when using `dropdownItems`). @@ -88,15 +88,18 @@ Breadcrumb items can include icons either alongside text or as standalone elemen Breadcrumb items can include dropdown menus for additional navigation options. Specify the dropdown items using the `dropdownItems` prop. -**Note:** When `dropdownItems` is provided, the `as` and `href` props are ignored. +**Note:** When `dropdownItems` is provided, the `render` and `href` props are ignored. -### As +### Render -Use the `as` prop to render the breadcrumb item as a custom component. By default, breadcrumb items are rendered as `a` tags. +Use the `render` prop to render the breadcrumb item as a custom component. Pass a JSX element (e.g. `render={}`) and we will replace the default `` while merging props. 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: set href on the item (it will be forwarded into the rendered element) +}>Home +``` diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts index fd6ea6895..ba334786f 100644 --- a/apps/www/src/content/docs/components/breadcrumb/props.ts +++ b/apps/www/src/content/docs/components/breadcrumb/props.ts @@ -19,7 +19,7 @@ export interface BreadcrumbItem { /** * Optional array of dropdown items * - * When `dropdownItems` is provided, the `as` and `href` props are ignored. + * When `dropdownItems` is provided, the `render` and `href` props are ignored. */ dropdownItems?: { /** Optional stable key for list reconciliation. Falls back to index if omitted. */ @@ -31,13 +31,12 @@ export interface BreadcrumbItem { }[]; /** - * Custom element used to render the Item. + * Render prop for polymorphism (Base UI `useRender`). + * Pass a JSX element to replace the default rendered ``. * - * All props are merged, with the custom component's props taking precedence over the breadcrumb item's props. - * - * @default "" + * Example: `render={}` */ - as?: ReactElement; + render?: ReactElement; } 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..9f4282f1f 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) => ( - + it('renders with custom element using render prop', () => { + 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 render 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 7a993b0c9..cac155543 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -1,13 +1,9 @@ 'use client'; +import { mergeProps, useRender } from '@base-ui/react'; import { ChevronDownIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import React, { - ComponentProps, - cloneElement, - ReactElement, - ReactNode -} from 'react'; +import React, { ReactNode } from 'react'; import { Menu } from '../menu'; import styles from './breadcrumb.module.css'; @@ -17,16 +13,15 @@ export interface BreadcrumbDropdownItem { onClick?: React.MouseEventHandler; } -export interface BreadcrumbItemProps extends ComponentProps<'a'> { +export interface BreadcrumbItemProps extends useRender.ComponentProps<'a'> { leadingIcon?: ReactNode; current?: boolean; dropdownItems?: BreadcrumbDropdownItem[]; - as?: ReactElement; } export const BreadcrumbItem = ({ ref, - as, + render, children, className, leadingIcon, @@ -35,6 +30,15 @@ export const BreadcrumbItem = ({ dropdownItems, ...props }: BreadcrumbItemProps) => { + const label = leadingIcon ? ( + <> + {leadingIcon} + {children != null && {children}} + + ) : ( + children + ); + const { id, title, @@ -43,15 +47,19 @@ export const BreadcrumbItem = ({ 'aria-describedby': ariaDescribedby } = props; - const renderedElement = as ?? ; - const label = ( - <> - {leadingIcon && ( - {leadingIcon} - )} - {children && {children}} - - ); + const linkElement = useRender({ + defaultTagName: 'a', + ref, + render, + props: mergeProps<'a'>( + { + className: cx(styles['breadcrumb-link']), + href, + children: label + }, + props + ) + }); if (dropdownItems) { return ( @@ -84,23 +92,25 @@ export const BreadcrumbItem = ({ ); } - return ( -
  • - {cloneElement( - renderedElement, - { - className: cx( + if (current) { + return ( +
  • + } + className={cx( styles['breadcrumb-link'], - current && styles['breadcrumb-link-active'] - ), - href, - ...props, - ...renderedElement.props, - ref - }, - label - )} -
  • + styles['breadcrumb-link-active'] + )} + aria-current='page' + {...props} + > + {label} + + + ); + } + return ( +
  • {linkElement}
  • ); }; diff --git a/packages/raystack/components/breadcrumb/breadcrumb-root.tsx b/packages/raystack/components/breadcrumb/breadcrumb-root.tsx index 3bcf4ae9c..a348cdc13 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-root.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-root.tsx @@ -23,11 +23,18 @@ export interface BreadcrumbProps export const BreadcrumbRoot = ({ className, children, + ref, size = 'medium', + 'aria-label': ariaLabel, ...props }: BreadcrumbProps) => { return ( -