From a6c8b676b4f54f4f966db9db7ea5808a835c495e Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 5 Mar 2026 20:13:14 +0530 Subject: [PATCH 1/4] feat (breadcrumb-dropdown): add href support --- apps/www/src/app/examples/page.tsx | 30 +++++++++++++++ .../docs/components/breadcrumb/demo.ts | 17 +++++++++ .../docs/components/breadcrumb/index.mdx | 7 +++- .../docs/components/breadcrumb/props.ts | 6 +++ .../breadcrumb/__tests__/breadcrumb.test.tsx | 32 ++++++++++++++++ .../components/breadcrumb/breadcrumb-item.tsx | 38 ++++++++++++++----- .../breadcrumb/breadcrumb.module.css | 13 +++++++ 7 files changed, 132 insertions(+), 11 deletions(-) diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx index 0aa9be777..d9072e296 100644 --- a/apps/www/src/app/examples/page.tsx +++ b/apps/www/src/app/examples/page.tsx @@ -417,6 +417,36 @@ const Page = () => { + + + 11. Dropdown with links (href, target, rel) + + + Home + + console.log('Books clicked') + } + ]} + > + Categories + + + + Current + + + Current Page ` }; + +export const dropdownLinksDemo = { + type: 'code', + code: ` + + Home + + {console.log('Books')}} + ]}>Categories + + Current + ` +}; + export const asDemo = { type: 'code', code: ` diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx index 4fa34fdb8..3665d1455 100644 --- a/apps/www/src/content/docs/components/breadcrumb/index.mdx +++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx @@ -13,6 +13,7 @@ import { maxItemsDemo, itemsBeforeCollapseDemo, dropdownDemo, + dropdownLinksDemo, asDemo, disabledDemo, } from "./demo.ts"; @@ -101,12 +102,14 @@ Breadcrumb items can include icons via `leadingIcon` (before the label) or `trai ### Dropdown -Breadcrumb items can include dropdown menus for additional navigation options. Specify the dropdown items using the `dropdownItems` prop. +Breadcrumb items can include dropdown menus for additional navigation options. Specify the dropdown items using the `dropdownItems` prop. Each item can have `label`, optional `href` (renders as a link), optional `target` and `rel` (e.g. `target="_blank"` `rel="noopener noreferrer"` for new tab), and optional `onClick`. -**Note:** When `dropdownItems` is provided, the `as` and `href` props are ignored. +**Note:** When `dropdownItems` is provided, the `as` and `href` props on the breadcrumb item are ignored. + + ### As Use the `as` prop to render the breadcrumb item as a custom component. By default, breadcrumb items are rendered as `a` tags. diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts index 3ef2eedbf..523f1e15d 100644 --- a/apps/www/src/content/docs/components/breadcrumb/props.ts +++ b/apps/www/src/content/docs/components/breadcrumb/props.ts @@ -33,6 +33,12 @@ export interface BreadcrumbItem { dropdownItems?: { /** Text to display for the dropdown item */ label: string; + /** When set, the option renders as a link. Use with target and rel for new tab (e.g. target="_blank" rel="noopener noreferrer"). */ + href?: string; + /** Link target (e.g. "_blank" for new tab). */ + target?: string; + /** Link rel (e.g. "noopener noreferrer" when target="_blank"). */ + rel?: string; /** Callback function when a dropdown item is clicked */ onClick?: ReactEventHandler; }[]; diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx index 13d2ef57e..ce456c5fe 100644 --- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -350,6 +350,38 @@ describe('Breadcrumb', () => { expect(screen.getByText('Clothing')).toBeInTheDocument(); expect(screen.getByText('Books')).toBeInTheDocument(); }); + + it('renders dropdown items with href as links', () => { + render( + + + Categories + + + ); + + fireEvent.click(screen.getByText('Categories')); + + const newTabLink = screen.getByText('New tab'); + expect(newTabLink.tagName).toBe('A'); + expect(newTabLink).toHaveAttribute('href', '/page'); + expect(newTabLink).toHaveAttribute('target', '_blank'); + expect(newTabLink).toHaveAttribute('rel', 'noopener noreferrer'); + + const sameTabLink = screen.getByText('Same tab'); + expect(sameTabLink.tagName).toBe('A'); + expect(sameTabLink).toHaveAttribute('href', '/other'); + }); }); describe('BreadcrumbSeparator', () => { diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 3d6668d35..78b2c5670 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -14,6 +14,9 @@ import styles from './breadcrumb.module.css'; export interface BreadcrumbDropdownItem { label: string; + href?: string; + target?: string; + rel?: string; onClick?: React.MouseEventHandler; } @@ -68,15 +71,32 @@ export const BreadcrumbItem = forwardRef< - {dropdownItems.map((dropdownItem, dropdownIndex) => ( - - {dropdownItem.label} - - ))} + {dropdownItems.map((dropdownItem, dropdownIndex) => + dropdownItem.href ? ( + + } + onClick={dropdownItem?.onClick} + > + {dropdownItem.label} + + ) : ( + + {dropdownItem.label} + + ) + )} ); diff --git a/packages/raystack/components/breadcrumb/breadcrumb.module.css b/packages/raystack/components/breadcrumb/breadcrumb.module.css index ad63a33ad..47b21edc1 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb.module.css +++ b/packages/raystack/components/breadcrumb/breadcrumb.module.css @@ -87,10 +87,23 @@ } .breadcrumb-dropdown-item { + display: block; + width: 100%; + padding: var(--rs-space-3); cursor: pointer; color: var(--rs-color-foreground-base-primary); + font-weight: var(--rs-font-weight-regular); + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + text-decoration: none; + border: none; + background: none; + text-align: left; + box-sizing: border-box; } .breadcrumb-dropdown-item:hover { background-color: var(--rs-color-background-base-primary-hover); + border-radius: var(--rs-radius-2); } From bd0de1b05be1f670cb39d023a61629d2a6578590 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 5 Mar 2026 20:29:48 +0530 Subject: [PATCH 2/4] feat: add data attributes --- .../docs/components/breadcrumb/index.mdx | 18 ++++++++++++++++++ .../breadcrumb/__tests__/breadcrumb.test.tsx | 2 ++ .../components/breadcrumb/breadcrumb-item.tsx | 2 ++ 3 files changed, 22 insertions(+) diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx index 3665d1455..308e9392c 100644 --- a/apps/www/src/content/docs/components/breadcrumb/index.mdx +++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx @@ -48,6 +48,24 @@ Groups all parts of the breadcrumb navigation. Renders an individual breadcrumb link. Use the `current` prop on the item that represents the current page so it is styled and exposed to assistive tech (e.g. `aria-current="page"`). Use the `disabled` prop for non-clickable, visually muted items (e.g. loading or no access). +Item elements expose data attributes for CSS state targeting so you can style current and disabled states without relying on internal class names: + +| Attribute | When present | +|-----------|----------------| +| `data-current="true"` | Item is the current page (`current` prop) | +| `data-disabled="true"` | Item is disabled (`disabled` prop) | + +Example: + +```css +[data-current="true"] { + color: var(--my-brand); +} +[data-disabled="true"] { + opacity: 0.6; +} +``` + ### Separator diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx index ce456c5fe..535464f04 100644 --- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -194,6 +194,7 @@ describe('Breadcrumb', () => { expect(span).toHaveClass(styles['breadcrumb-link']); expect(span).toHaveClass(styles['breadcrumb-link-active']); expect(span).toHaveAttribute('aria-current', 'page'); + expect(span).toHaveAttribute('data-current', 'true'); expect(span).toHaveTextContent('Current Page'); }); @@ -267,6 +268,7 @@ describe('Breadcrumb', () => { expect(span).toHaveClass(styles['breadcrumb-link']); expect(span).toHaveClass(styles['breadcrumb-link-disabled']); expect(span).toHaveAttribute('aria-disabled', 'true'); + expect(span).toHaveAttribute('data-disabled', 'true'); expect(span).toHaveTextContent('Loading…'); }); diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 78b2c5670..a6723bd45 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -111,6 +111,7 @@ export const BreadcrumbItem = forwardRef< styles['breadcrumb-link-disabled'] )} aria-disabled='true' + data-disabled='true' > {label} @@ -127,6 +128,7 @@ export const BreadcrumbItem = forwardRef< styles['breadcrumb-link-active'] )} aria-current='page' + data-current='true' > {label} From 8be7276cca853933fcdb2439cf37d8bbfa5a4c30 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Fri, 20 Mar 2026 10:09:40 +0530 Subject: [PATCH 3/4] refactor: merge react 19 --- .../components/breadcrumb/breadcrumb-item.tsx | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 8fa1e8176..73eb7a605 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -11,14 +11,17 @@ import React, { import { Menu } from '../menu'; import styles from './breadcrumb.module.css'; -export interface BreadcrumbDropdownItem { +export type BreadcrumbDropdownItem = { key?: string; - label: string; + label: ReactNode; + /** + * Convenience API for link-like menu items. If you need more control, + * pass `render` directly via the Menu.Item props supported below. + */ href?: string; target?: string; rel?: string; - onClick?: React.MouseEventHandler; -} +} & Omit, 'children'>; export interface BreadcrumbItemProps extends ComponentProps<'a'> { leadingIcon?: ReactNode; @@ -68,32 +71,50 @@ export const BreadcrumbItem = ({ - {dropdownItems.map((dropdownItem, dropdownIndex) => - dropdownItem.href ? ( - - } - onClick={dropdownItem?.onClick} - > - {dropdownItem.label} - - ) : ( + {dropdownItems.map((dropdownItem, dropdownIndex) => { + const { + key, + label: dropdownLabel, + href: dropdownHref, + target: dropdownTarget, + rel: dropdownRel, + render, + className: dropdownItemClassName, + ...dropdownItemProps + } = dropdownItem; + + const effectiveRender = + dropdownHref && !render ? ( + + ) : ( + render + ); + + return ( - {dropdownItem.label} + {dropdownLabel} - ) - )} + ); + })} ); From 5a871d90c343f5883d5e21d981394ca540d3adac Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Fri, 20 Mar 2026 15:18:51 +0530 Subject: [PATCH 4/4] refactor: pass all menuItem props --- .../playground/breadcrumb-examples.tsx | 4 +- .../docs/components/breadcrumb/demo.ts | 10 +-- .../docs/components/breadcrumb/index.mdx | 2 +- .../docs/components/breadcrumb/props.ts | 22 ++----- .../breadcrumb/__tests__/breadcrumb.test.tsx | 33 +++++----- .../components/breadcrumb/breadcrumb-item.tsx | 64 +++++-------------- 6 files changed, 50 insertions(+), 85 deletions(-) diff --git a/apps/www/src/components/playground/breadcrumb-examples.tsx b/apps/www/src/components/playground/breadcrumb-examples.tsx index 9d9fbb7f1..a02b4477b 100644 --- a/apps/www/src/components/playground/breadcrumb-examples.tsx +++ b/apps/www/src/components/playground/breadcrumb-examples.tsx @@ -13,13 +13,13 @@ export function BreadcrumbExamples() { { console.log('Clothes'); } }, { - label: 'Electronics', + children: 'Electronics', onClick: () => { console.log('Electronics'); } diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts index 113c0000c..9d19dec0d 100644 --- a/apps/www/src/content/docs/components/breadcrumb/demo.ts +++ b/apps/www/src/content/docs/components/breadcrumb/demo.ts @@ -94,8 +94,8 @@ export const dropdownDemo = { Category {console.log('Option 1')}}, - { label: 'Option 2', onClick: () => {console.log('Option 2')}} + { children: 'Option 1', onClick: () => {console.log('Option 1')}}, + { children: 'Option 2', onClick: () => {console.log('Option 2')}} ]}>Subcategory Current Page @@ -109,9 +109,9 @@ export const dropdownLinksDemo = { Home {console.log('Books')}} + { children: 'Electronics', render: }, + { children: 'Clothing', render: }, + { children: 'Books', onClick: () => {console.log('Books')}} ]}>Categories Current diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx index 8b57244eb..57333ab5e 100644 --- a/apps/www/src/content/docs/components/breadcrumb/index.mdx +++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx @@ -106,7 +106,7 @@ Breadcrumb items can include icons via `leadingIcon` (before the label) or `trai ### Dropdown -Breadcrumb items can include dropdown menus for additional navigation options. Specify the dropdown items using the `dropdownItems` prop. Each item can have `label`, optional `href` (renders as a link), optional `target` and `rel` (e.g. `target="_blank"` `rel="noopener noreferrer"` for new tab), and optional `onClick`. +Breadcrumb items can include dropdown menus for additional navigation options. Specify them with the `dropdownItems` prop: each entry is the same props as `` (e.g. `children` for the label, `onClick`, `disabled`, `render` for a link such as ``, etc.). You can also pass `key` for stable list keys. **Note:** When `dropdownItems` is provided, the `as` and `href` props on the breadcrumb item are ignored. diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts index ea3a9cb9f..075671adc 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 { ReactElement, ReactNode } from 'react'; export interface BreadcrumbItem { /** Text to display for the item */ @@ -26,24 +26,16 @@ export interface BreadcrumbItem { disabled?: boolean; /** - * Optional array of dropdown items + * Optional array of dropdown entries; each object is passed to `` + * (e.g. `children`, `onClick`, `render` for a link, etc.), plus optional `key` + * for React list reconciliation (not forwarded to `Menu.Item`). * * When `dropdownItems` is provided, the `as` and `href` props are ignored. */ - dropdownItems?: { - /** Optional stable key for list reconciliation. Falls back to index if omitted. */ + dropdownItems?: (Record & { key?: string; - /** Text to display for the dropdown item */ - label: string; - /** When set, the option renders as a link. Use with target and rel for new tab (e.g. target="_blank" rel="noopener noreferrer"). */ - href?: string; - /** Link target (e.g. "_blank" for new tab). */ - target?: string; - /** Link rel (e.g. "noopener noreferrer" when target="_blank"). */ - rel?: string; - /** Callback function when a dropdown item is clicked */ - onClick?: ReactEventHandler; - }[]; + children?: ReactNode; + })[]; /** * Custom element used to render the Item. diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx index 8bf431506..4eaa2dd7d 100644 --- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -290,8 +290,8 @@ describe('Breadcrumb', () => { it('disabled with dropdownItems renders as disabled span not dropdown', () => { const items = [ - { label: 'Option 1', onClick: vi.fn() }, - { label: 'Option 2', onClick: vi.fn() } + { children: 'Option 1', onClick: vi.fn() }, + { children: 'Option 2', onClick: vi.fn() } ]; const { container } = render( @@ -314,8 +314,8 @@ describe('Breadcrumb', () => { describe('BreadcrumbItem with Dropdown', () => { it('renders dropdown trigger when dropdownItems provided', () => { const items = [ - { label: 'Option 1', onClick: vi.fn() }, - { label: 'Option 2', onClick: vi.fn() } + { children: 'Option 1', onClick: vi.fn() }, + { children: 'Option 2', onClick: vi.fn() } ]; render( @@ -333,9 +333,9 @@ describe('Breadcrumb', () => { it('renders dropdown items on click', () => { const items = [ - { label: 'Electronics' }, - { label: 'Clothing' }, - { label: 'Books' } + { children: 'Electronics' }, + { children: 'Clothing' }, + { children: 'Books' } ]; render( @@ -359,12 +359,15 @@ describe('Breadcrumb', () => { + ) }, - { label: 'Same tab', href: '/other' } + { + children: 'Same tab', + render: + } ]} > Categories @@ -554,9 +557,9 @@ describe('Breadcrumb', () => { it('renders breadcrumb with icons and dropdown', () => { const categories = [ - { label: 'Electronics' }, - { label: 'Clothing' }, - { label: 'Books' } + { children: 'Electronics' }, + { children: 'Clothing' }, + { children: 'Books' } ]; render( diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 73eb7a605..b3bfad30e 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -11,17 +11,14 @@ import React, { import { Menu } from '../menu'; import styles from './breadcrumb.module.css'; -export type BreadcrumbDropdownItem = { +/** + * Each entry maps to ``. Use `children`, `render`, `onClick`, + * `disabled`, etc. — whatever `Menu.Item` supports. + */ +export type BreadcrumbDropdownItem = ComponentProps & { + /** Optional stable key for React list reconciliation (not passed to `Menu.Item`). */ key?: string; - label: ReactNode; - /** - * Convenience API for link-like menu items. If you need more control, - * pass `render` directly via the Menu.Item props supported below. - */ - href?: string; - target?: string; - rel?: string; -} & Omit, 'children'>; +}; export interface BreadcrumbItemProps extends ComponentProps<'a'> { leadingIcon?: ReactNode; @@ -74,45 +71,18 @@ export const BreadcrumbItem = ({ {dropdownItems.map((dropdownItem, dropdownIndex) => { const { key, - label: dropdownLabel, - href: dropdownHref, - target: dropdownTarget, - rel: dropdownRel, - render, - className: dropdownItemClassName, - ...dropdownItemProps + className: itemClassName, + ...menuItemProps } = dropdownItem; - - const effectiveRender = - dropdownHref && !render ? ( - - ) : ( - render - ); - return ( - {dropdownLabel} - + className={cx( + styles['breadcrumb-dropdown-item'], + itemClassName + )} + {...menuItemProps} + /> ); })} @@ -145,11 +115,11 @@ export const BreadcrumbItem = ({ {cloneElement( renderedElement, { - ref, className: styles['breadcrumb-link'], href, ...props, - ...renderedElement.props + ...renderedElement.props, + ref }, label )}