From 1b0dd422abc3163704c7f65a4c049d69aaa0e0ef Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 5 Mar 2026 18:52:30 +0530 Subject: [PATCH 1/6] feat: add disabled prop support for item --- apps/www/src/app/examples/page.tsx | 40 +++++++++++++ .../docs/components/breadcrumb/demo.ts | 12 ++++ .../docs/components/breadcrumb/index.mdx | 10 +++- .../docs/components/breadcrumb/props.ts | 6 ++ .../breadcrumb/__tests__/breadcrumb.test.tsx | 58 +++++++++++++++++++ .../components/breadcrumb/breadcrumb-item.tsx | 21 ++++++- .../breadcrumb/breadcrumb.module.css | 9 ++- 7 files changed, 153 insertions(+), 3 deletions(-) diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx index 25815bc20..eb523cdd2 100644 --- a/apps/www/src/app/examples/page.tsx +++ b/apps/www/src/app/examples/page.tsx @@ -349,6 +349,46 @@ const Page = () => { {breadcrumbTrail} + + + 8. Long labels (no truncation) – layout pushed / overflow + +
+ + Home + + Products + + + Very Long Category Name That Pushes The Layout + + +
+
+ + + 9. Disabled item (e.g. loading or no access) + + + Home + + Loading… + + + Products + + + diff --git a/apps/www/src/content/docs/components/breadcrumb/demo.ts b/apps/www/src/content/docs/components/breadcrumb/demo.ts index 8dc244291..f2b3019e1 100644 --- a/apps/www/src/content/docs/components/breadcrumb/demo.ts +++ b/apps/www/src/content/docs/components/breadcrumb/demo.ts @@ -149,6 +149,18 @@ export const asDemo = { ` }; +export const disabledDemo = { + type: 'code', + code: ` + + Home + + Loading… + + Products + ` +}; + export const iconsDemo = { type: 'code', tabs: [ diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx index 7cc54e572..c37cb9d5b 100644 --- a/apps/www/src/content/docs/components/breadcrumb/index.mdx +++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx @@ -14,6 +14,7 @@ import { itemsBeforeCollapseDemo, dropdownDemo, asDemo, + disabledDemo, } from "./demo.ts"; @@ -44,7 +45,7 @@ Groups all parts of the breadcrumb navigation. ### Item -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"`). +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). @@ -114,8 +115,15 @@ When a custom component is provided, the props are merged, with the custom compo +### Disabled + +Use the `disabled` prop for non-clickable, visually muted items—for example, loading states or segments the user does not have access to. Disabled items render as a span with `aria-disabled="true"` and do not navigate. + + + ## Accessibility - Uses `nav` element with `aria-label="Breadcrumb"` for proper landmark identification - Current page is indicated with `aria-current="page"` +- Disabled items use `aria-disabled="true"` - Separator elements are hidden from screen readers with `aria-hidden` diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts index 5305722f6..504a386b1 100644 --- a/apps/www/src/content/docs/components/breadcrumb/props.ts +++ b/apps/www/src/content/docs/components/breadcrumb/props.ts @@ -16,6 +16,12 @@ export interface BreadcrumbItem { */ current?: boolean; + /** + * When true, the item is non-clickable and visually muted (e.g. loading or no access). + * @defaultValue false + */ + disabled?: boolean; + /** * Optional array of dropdown items * diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx index 5671d87d7..5b7819efb 100644 --- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -191,6 +191,64 @@ describe('Breadcrumb', () => { expect(link).toHaveAttribute('aria-label', 'Products'); expect(link).toHaveAttribute('data-testid', 'item'); }); + + it('renders as span with disabled styles when disabled', () => { + const { container } = render( + + Loading… + + ); + + const link = container.querySelector('a'); + expect(link).not.toBeInTheDocument(); + + const span = container.querySelector( + `span.${styles['breadcrumb-link-disabled']}` + ); + expect(span).toBeInTheDocument(); + expect(span).toHaveClass(styles['breadcrumb-link']); + expect(span).toHaveClass(styles['breadcrumb-link-disabled']); + expect(span).toHaveAttribute('aria-disabled', 'true'); + expect(span).toHaveTextContent('Loading…'); + }); + + it('disabled item has no href and is not focusable as link', () => { + const { container } = render( + + + No access + + + ); + + const span = container.querySelector( + `span.${styles['breadcrumb-link-disabled']}` + ); + expect(span).toBeInTheDocument(); + expect(container.querySelector('a')).not.toBeInTheDocument(); + }); + + it('disabled with dropdownItems renders as disabled span not dropdown', () => { + const items = [ + { label: 'Option 1', onClick: vi.fn() }, + { label: 'Option 2', onClick: vi.fn() } + ]; + const { container } = render( + + + Categories + + + ); + + const span = container.querySelector( + `span.${styles['breadcrumb-link-disabled']}` + ); + expect(span).toBeInTheDocument(); + expect(span).toHaveTextContent('Categories'); + fireEvent.click(span!); + expect(screen.queryByText('Option 1')).not.toBeInTheDocument(); + }); }); describe('BreadcrumbItem with Dropdown', () => { diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 0b3471ac2..6170a9ac5 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -20,6 +20,8 @@ export interface BreadcrumbDropdownItem { export interface BreadcrumbItemProps extends HTMLAttributes { leadingIcon?: ReactNode; current?: boolean; + /** When true, the item is non-clickable and visually muted (e.g. loading or no access). */ + disabled?: boolean; dropdownItems?: BreadcrumbDropdownItem[]; href?: string; as?: ReactElement; @@ -36,6 +38,7 @@ export const BreadcrumbItem = forwardRef< className, leadingIcon, current, + disabled, href, dropdownItems, ...props @@ -52,7 +55,7 @@ export const BreadcrumbItem = forwardRef< ); - if (dropdownItems) { + if (dropdownItems && !disabled) { return ( @@ -73,6 +76,22 @@ export const BreadcrumbItem = forwardRef< ); } + if (disabled) { + return ( +
  • + } + className={cx( + styles['breadcrumb-link'], + styles['breadcrumb-link-disabled'] + )} + aria-disabled='true' + > + {label} + +
  • + ); + } return (
  • {cloneElement( diff --git a/packages/raystack/components/breadcrumb/breadcrumb.module.css b/packages/raystack/components/breadcrumb/breadcrumb.module.css index fa93de192..e11e8702d 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb.module.css +++ b/packages/raystack/components/breadcrumb/breadcrumb.module.css @@ -48,6 +48,13 @@ pointer-events: none; } +.breadcrumb-link-disabled { + color: var(--rs-color-foreground-base-tertiary); + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + .breadcrumb-icon { display: flex; align-items: center; @@ -82,4 +89,4 @@ .breadcrumb-dropdown-item:hover { background-color: var(--rs-color-background-base-primary-hover); -} +} \ No newline at end of file From 4b6b6fb70dc21f918edf65a119b6ff51f72346e6 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 5 Mar 2026 19:03:03 +0530 Subject: [PATCH 2/6] feat: add trailing item support from item --- apps/www/src/app/examples/page.tsx | 26 ++++++++++ .../docs/components/breadcrumb/demo.ts | 24 ++++++++- .../docs/components/breadcrumb/index.mdx | 2 +- .../docs/components/breadcrumb/props.ts | 5 +- .../breadcrumb/__tests__/breadcrumb.test.tsx | 49 +++++++++++++++++++ .../components/breadcrumb/breadcrumb-item.tsx | 5 ++ .../breadcrumb/breadcrumb.module.css | 2 +- 7 files changed, 109 insertions(+), 4 deletions(-) diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx index eb523cdd2..0aa9be777 100644 --- a/apps/www/src/app/examples/page.tsx +++ b/apps/www/src/app/examples/page.tsx @@ -391,6 +391,32 @@ const Page = () => { + + + 10. Leading and trailing icons + + + }> + Home + + + } + trailingIcon={} + > + Documents + + + } + current + > + Settings + + + }>Home @@ -175,6 +175,28 @@ export const iconsDemo = { }>Settings ` }, + { + name: 'Trailing Icon', + code: ` + + }>Home + + }>Documents + + }>Settings + ` + }, + { + name: 'Both Icons', + code: ` + + } trailingIcon={}>Home + + } trailingIcon={}>Documents + + } trailingIcon={}>Settings + ` + }, { name: 'Only Icon', code: ` diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx index c37cb9d5b..798dc8dbf 100644 --- a/apps/www/src/content/docs/components/breadcrumb/index.mdx +++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx @@ -95,7 +95,7 @@ Control how many items appear before the ellipsis when collapsed. With `maxItems ### Icons -Breadcrumb items can include icons either alongside text or as standalone elements. +Breadcrumb items can include icons via `leadingIcon` (before the label) or `trailingIcon` (after the label), either alongside text or as standalone elements. diff --git a/apps/www/src/content/docs/components/breadcrumb/props.ts b/apps/www/src/content/docs/components/breadcrumb/props.ts index 504a386b1..3ef2eedbf 100644 --- a/apps/www/src/content/docs/components/breadcrumb/props.ts +++ b/apps/www/src/content/docs/components/breadcrumb/props.ts @@ -7,9 +7,12 @@ export interface BreadcrumbItem { /** URL for the item link */ href?: string; - /** Optional icon element to display */ + /** Optional icon element to display before the label */ leadingIcon?: ReactNode; + /** Optional icon element to display after the label */ + trailingIcon?: ReactNode; + /** * Whether the item is the current page * @defaultValue false diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx index 5b7819efb..4e9bbb78c 100644 --- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -128,6 +128,55 @@ describe('Breadcrumb', () => { expect(screen.getByText('Home')).toBeInTheDocument(); }); + it('renders with trailing icon', () => { + render( + + ▶} + > + Next + + + ); + + const icon = screen.getByTestId('trailing-icon'); + expect(icon).toBeInTheDocument(); + expect(icon.parentElement).toHaveClass(styles['breadcrumb-icon']); + expect(screen.getByText('Next')).toBeInTheDocument(); + }); + + it('renders with both leading and trailing icons', () => { + const { container } = render( + + L} + trailingIcon={T} + > + Label + + + ); + + const leading = screen.getByTestId('leading'); + const trailing = screen.getByTestId('trailing'); + const label = screen.getByText('Label'); + + expect(leading).toBeInTheDocument(); + expect(trailing).toBeInTheDocument(); + expect(label).toBeInTheDocument(); + expect(leading.parentElement).toHaveClass(styles['breadcrumb-icon']); + expect(trailing.parentElement).toHaveClass(styles['breadcrumb-icon']); + + const link = container.querySelector(`.${styles['breadcrumb-link']}`); + const iconWrappers = link?.querySelectorAll( + `.${styles['breadcrumb-icon']}` + ); + expect(iconWrappers).toHaveLength(2); + expect(iconWrappers?.[0]).toContainElement(leading); + expect(iconWrappers?.[1]).toContainElement(trailing); + expect(link?.textContent).toMatch(/L\s*Label\s*T/); + }); + it('applies current/active state', () => { const { container } = render( diff --git a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx index 6170a9ac5..01f8dad44 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-item.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-item.tsx @@ -19,6 +19,7 @@ export interface BreadcrumbDropdownItem { export interface BreadcrumbItemProps extends HTMLAttributes { leadingIcon?: ReactNode; + trailingIcon?: ReactNode; current?: boolean; /** When true, the item is non-clickable and visually muted (e.g. loading or no access). */ disabled?: boolean; @@ -37,6 +38,7 @@ export const BreadcrumbItem = forwardRef< children, className, leadingIcon, + trailingIcon, current, disabled, href, @@ -52,6 +54,9 @@ export const BreadcrumbItem = forwardRef< {leadingIcon} )} {children && {children}} + {trailingIcon && ( + {trailingIcon} + )} ); diff --git a/packages/raystack/components/breadcrumb/breadcrumb.module.css b/packages/raystack/components/breadcrumb/breadcrumb.module.css index e11e8702d..886da5f12 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb.module.css +++ b/packages/raystack/components/breadcrumb/breadcrumb.module.css @@ -89,4 +89,4 @@ .breadcrumb-dropdown-item:hover { background-color: var(--rs-color-background-base-primary-hover); -} \ No newline at end of file +} From b708a480a0f302e7f379189f4b2470cc44dbc6fe Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 5 Mar 2026 19:11:57 +0530 Subject: [PATCH 3/6] feat(breadcrumb): update separator accessibility attributes to use role="presentation" and aria-hidden="true" --- .../content/docs/components/breadcrumb/index.mdx | 2 +- .../breadcrumb/__tests__/breadcrumb.test.tsx | 16 ++++++++++++++++ .../components/breadcrumb/breadcrumb-misc.tsx | 4 +++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/www/src/content/docs/components/breadcrumb/index.mdx b/apps/www/src/content/docs/components/breadcrumb/index.mdx index 798dc8dbf..4fa34fdb8 100644 --- a/apps/www/src/content/docs/components/breadcrumb/index.mdx +++ b/apps/www/src/content/docs/components/breadcrumb/index.mdx @@ -126,4 +126,4 @@ Use the `disabled` prop for non-clickable, visually muted items—for example, l - Uses `nav` element with `aria-label="Breadcrumb"` for proper landmark identification - Current page is indicated with `aria-current="page"` - Disabled items use `aria-disabled="true"` -- Separator elements are hidden from screen readers with `aria-hidden` +- Separator elements are decorative and use `role="presentation"` and `aria-hidden="true"` so screen readers skip them diff --git a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx index 4e9bbb78c..23fa11bd9 100644 --- a/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx +++ b/packages/raystack/components/breadcrumb/__tests__/breadcrumb.test.tsx @@ -389,6 +389,22 @@ describe('Breadcrumb', () => { ); expect(ref).toHaveBeenCalled(); }); + + it('has role="presentation" and aria-hidden="true" for screen readers', () => { + const { container } = render( + + Home + + Products + + ); + + const separator = container.querySelector( + `.${styles['breadcrumb-separator']}` + ); + expect(separator).toHaveAttribute('role', 'presentation'); + expect(separator).toHaveAttribute('aria-hidden', 'true'); + }); }); describe('BreadcrumbEllipsis', () => { diff --git a/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx b/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx index 52bc07c90..5901e36c3 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx +++ b/packages/raystack/components/breadcrumb/breadcrumb-misc.tsx @@ -2,7 +2,7 @@ import { ChevronRightIcon, DotsHorizontalIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import { HTMLAttributes, forwardRef } from 'react'; +import { forwardRef, HTMLAttributes } from 'react'; import styles from './breadcrumb.module.css'; export interface BreadcrumbEllipsisProps @@ -53,6 +53,8 @@ export const BreadcrumbSeparator = forwardRef<
  • ); } + if (current) { + return ( +
  • + } + className={cx( + styles['breadcrumb-link'], + styles['breadcrumb-link-active'] + )} + aria-current='page' + > + {label} + +
  • + ); + } return (
  • {cloneElement( renderedElement, { - className: cx( - styles['breadcrumb-link'], - current && styles['breadcrumb-link-active'] - ), + className: styles['breadcrumb-link'], href, ...props, ...renderedElement.props diff --git a/packages/raystack/components/breadcrumb/breadcrumb.module.css b/packages/raystack/components/breadcrumb/breadcrumb.module.css index 886da5f12..82ba47e45 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb.module.css +++ b/packages/raystack/components/breadcrumb/breadcrumb.module.css @@ -45,7 +45,6 @@ .breadcrumb-link-active { color: var(--rs-color-foreground-base-primary); font-weight: var(--rs-font-weight-medium); - pointer-events: none; } .breadcrumb-link-disabled { From 8df91dbae231a1421803cbdb65ee49e406bc4602 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 5 Mar 2026 19:26:47 +0530 Subject: [PATCH 5/6] style(breadcrumb): enhance active link styling with hover effect and default cursor --- .../raystack/components/breadcrumb/breadcrumb.module.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/raystack/components/breadcrumb/breadcrumb.module.css b/packages/raystack/components/breadcrumb/breadcrumb.module.css index 82ba47e45..ad63a33ad 100644 --- a/packages/raystack/components/breadcrumb/breadcrumb.module.css +++ b/packages/raystack/components/breadcrumb/breadcrumb.module.css @@ -45,6 +45,11 @@ .breadcrumb-link-active { color: var(--rs-color-foreground-base-primary); font-weight: var(--rs-font-weight-medium); + cursor: default; +} + +.breadcrumb-link-active:hover { + color: var(--rs-color-foreground-base-primary); } .breadcrumb-link-disabled { From 951bd1f8c2e755c99148ea7645c727e834f2150d Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Mon, 9 Mar 2026 16:09:16 +0530 Subject: [PATCH 6/6] chore: Remove breadcrumb examples --- apps/www/src/app/examples/page.tsx | 164 ----------------------------- 1 file changed, 164 deletions(-) diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx index 9be7f88cc..076e26463 100644 --- a/apps/www/src/app/examples/page.tsx +++ b/apps/www/src/app/examples/page.tsx @@ -227,170 +227,6 @@ const Page = () => { -<<<<<<< feat/trailing-icon-and-disabled-item - {/* Breadcrumb Examples */} - - Breadcrumb Examples - - - - - 1. (no props) – manual ellipsis - - - Home - - - - - Footwear - - - - - - 2. maxItems=8 - - {breadcrumbTrail} - - - - 3. maxItems=5 - - {breadcrumbTrail} - - - - 4. maxItems=3 - - {breadcrumbTrail} - - - - 5. maxItems=5 itemsBeforeCollapse=2 - - - {breadcrumbTrail} - - - - - 6. maxItems=5 itemsBeforeCollapse=5 (4 before + 1 after) - - - Home - - Products - - - Electronics - - - - Laptops - - - - Gaming - - - - Accessories - - - - Footwear - - - - - - 7. size=small maxItems=4 - - - {breadcrumbTrail} - - - - - 8. Long labels (no truncation) – layout pushed / overflow - -
    - - Home - - Products - - - Very Long Category Name That Pushes The Layout - - -
    -
    - - - 9. Disabled item (e.g. loading or no access) - - - Home - - Loading… - - - Products - - - -
    - - - - 10. Leading and trailing icons - - - }> - Home - - - } - trailingIcon={} - > - Documents - - - } - current - > - Settings - - - -======= ->>>>>>> feat/breadcrumb-auto-ellipsis