Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions apps/www/src/app/examples/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,36 @@ const Page = () => {
</Breadcrumb.Item>
</Breadcrumb>
</Flex>
<Flex direction='column' gap={2}>
<Text size='small' weight='medium'>
11. Dropdown with links (href, target, rel)
</Text>
<Breadcrumb>
<Breadcrumb.Item href='/'>Home</Breadcrumb.Item>
<Breadcrumb.Separator />
<Breadcrumb.Item
dropdownItems={[
{
label: 'Electronics',
href: '/electronics',
target: '_blank',
rel: 'noopener noreferrer'
},
{ label: 'Clothing', href: '/clothing' },
{
label: 'Books',
onClick: () => console.log('Books clicked')
}
]}
>
Categories
</Breadcrumb.Item>
<Breadcrumb.Separator />
<Breadcrumb.Item href='/current' current>
Current
</Breadcrumb.Item>
</Breadcrumb>
</Flex>
<Flex direction='column' gap={4} style={{ maxWidth: '550px' }}>
<Search
placeholder='Default large search'
Expand Down
17 changes: 17 additions & 0 deletions apps/www/src/content/docs/components/breadcrumb/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,23 @@ export const dropdownDemo = {
<Breadcrumb.Item href="/category/subcategory/current">Current Page</Breadcrumb.Item>
</Breadcrumb>`
};

export const dropdownLinksDemo = {
type: 'code',
code: `
<Breadcrumb>
<Breadcrumb.Item href="/">Home</Breadcrumb.Item>
<Breadcrumb.Separator/>
<Breadcrumb.Item dropdownItems={[
{ label: 'Electronics', href: '/electronics', target: '_blank', rel: 'noopener noreferrer' },
{ label: 'Clothing', href: '/clothing' },
{ label: 'Books', onClick: () => {console.log('Books')}}
]}>Categories</Breadcrumb.Item>
<Breadcrumb.Separator/>
<Breadcrumb.Item href="/current" current>Current</Breadcrumb.Item>
</Breadcrumb>`
};

export const asDemo = {
type: 'code',
code: `
Expand Down
25 changes: 23 additions & 2 deletions apps/www/src/content/docs/components/breadcrumb/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
maxItemsDemo,
itemsBeforeCollapseDemo,
dropdownDemo,
dropdownLinksDemo,
asDemo,
disabledDemo,
} from "./demo.ts";
Expand Down Expand Up @@ -47,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;
}
```

<auto-type-table path="./props.ts" name="BreadcrumbItem" />

### Separator
Expand Down Expand Up @@ -101,12 +120,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.

<Demo data={dropdownDemo} />

<Demo data={dropdownLinksDemo} />

### As

Use the `as` prop to render the breadcrumb item as a custom component. By default, breadcrumb items are rendered as `a` tags.
Expand Down
6 changes: 6 additions & 0 deletions apps/www/src/content/docs/components/breadcrumb/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>;
}[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down Expand Up @@ -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…');
});

Expand Down Expand Up @@ -350,6 +352,38 @@ describe('Breadcrumb', () => {
expect(screen.getByText('Clothing')).toBeInTheDocument();
expect(screen.getByText('Books')).toBeInTheDocument();
});

it('renders dropdown items with href as links', () => {
render(
<Breadcrumb>
<Breadcrumb.Item
dropdownItems={[
{
label: 'New tab',
href: '/page',
target: '_blank',
rel: 'noopener noreferrer'
},
{ label: 'Same tab', href: '/other' }
]}
>
Categories
</Breadcrumb.Item>
</Breadcrumb>
);

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', () => {
Expand Down
40 changes: 31 additions & 9 deletions packages/raystack/components/breadcrumb/breadcrumb-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import styles from './breadcrumb.module.css';

export interface BreadcrumbDropdownItem {
label: string;
href?: string;
target?: string;
rel?: string;
onClick?: React.MouseEventHandler<HTMLElement>;
}

Expand Down Expand Up @@ -68,15 +71,32 @@ export const BreadcrumbItem = forwardRef<
<ChevronDownIcon className={styles['breadcrumb-dropdown-icon']} />
</Menu.Trigger>
<Menu.Content className={styles['breadcrumb-dropdown-content']}>
{dropdownItems.map((dropdownItem, dropdownIndex) => (
<Menu.Item
key={dropdownIndex}
className={styles['breadcrumb-dropdown-item']}
onClick={dropdownItem?.onClick}
>
{dropdownItem.label}
</Menu.Item>
))}
{dropdownItems.map((dropdownItem, dropdownIndex) =>
dropdownItem.href ? (
<Menu.Item
key={dropdownIndex}
render={
<a
href={dropdownItem.href}
target={dropdownItem.target}
rel={dropdownItem.rel}
className={styles['breadcrumb-dropdown-item']}
/>
}
onClick={dropdownItem?.onClick}
>
{dropdownItem.label}
</Menu.Item>
) : (
<Menu.Item
key={dropdownIndex}
className={styles['breadcrumb-dropdown-item']}
onClick={dropdownItem?.onClick}
>
{dropdownItem.label}
</Menu.Item>
)
)}
</Menu.Content>
</Menu>
);
Expand All @@ -91,6 +111,7 @@ export const BreadcrumbItem = forwardRef<
styles['breadcrumb-link-disabled']
)}
aria-disabled='true'
data-disabled='true'
>
{label}
</span>
Expand All @@ -107,6 +128,7 @@ export const BreadcrumbItem = forwardRef<
styles['breadcrumb-link-active']
)}
aria-current='page'
data-current='true'
>
{label}
</span>
Expand Down
13 changes: 13 additions & 0 deletions packages/raystack/components/breadcrumb/breadcrumb.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}