Skip to content
Draft
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
56 changes: 56 additions & 0 deletions src/components/Breadcrumbs/Breadcrumbs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Meta, StoryObj } from '@storybook/react-vite';
import { Breadcrumbs, BreadcrumbItem, BreadcrumbSeparator } from './Breadcrumbs';

const meta: Meta<typeof Breadcrumbs> = {
component: Breadcrumbs,
title: 'Navigation/Breadcrumbs',
tags: ['breadcrumbs', 'autodocs'],
};

export default meta;

type Story = StoryObj<typeof Breadcrumbs>;

export const Playground: Story = {
render: () => (
<Breadcrumbs>
<BreadcrumbItem
icon="home"
href="#"
>
Home
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem href="#">Data sources</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem href="#">ClickPipes</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem active>GCS Unordered mode with service account</BreadcrumbItem>
</Breadcrumbs>
),
};

export const WithIcon: Story = {
render: () => (
<Breadcrumbs>
<BreadcrumbItem
icon="home"
href="#"
>
Data sources
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem active>Settings</BreadcrumbItem>
</Breadcrumbs>
),
};

export const TwoLevels: Story = {
render: () => (
<Breadcrumbs>
<BreadcrumbItem href="#">Home</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem active>Current page</BreadcrumbItem>
</Breadcrumbs>
),
};
51 changes: 51 additions & 0 deletions src/components/Breadcrumbs/Breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Breadcrumbs, BreadcrumbItem, BreadcrumbSeparator } from './Breadcrumbs';
import { renderCUI } from '@/utils/test-utils';

describe('Breadcrumbs', () => {
const renderBreadcrumbs = () =>
renderCUI(
<Breadcrumbs>
<BreadcrumbItem
icon="home"
href="#"
>
Home
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem href="#">Data sources</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem active>Current page</BreadcrumbItem>
</Breadcrumbs>
);

it('should render all breadcrumb items', () => {
const { getByText } = renderBreadcrumbs();
expect(getByText('Home')).toBeInTheDocument();
expect(getByText('Data sources')).toBeInTheDocument();
expect(getByText('Current page')).toBeInTheDocument();
});

it('should have a navigation landmark with accessible label', () => {
const { getByRole } = renderBreadcrumbs();
const nav = getByRole('navigation');
expect(nav).toHaveAccessibleName('Breadcrumb');
});

it('should mark the active item with aria-current="page"', () => {
const { getByText } = renderBreadcrumbs();
const activeItem = getByText('Current page').closest('li');
expect(activeItem).toHaveAttribute('aria-current', 'page');
});

it('should render links for non-active items with href', () => {
const { getByText } = renderBreadcrumbs();
const link = getByText('Data sources').closest('a');
expect(link).toHaveAttribute('href', '#');
});

it('should not render a link for the active item', () => {
const { getByText } = renderBreadcrumbs();
const activeItem = getByText('Current page');
expect(activeItem.closest('a')).toBeNull();
});
});
129 changes: 129 additions & 0 deletions src/components/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { styled } from 'styled-components';
import { forwardRef, HTMLAttributes, ReactNode } from 'react';
import { Icon } from '@/components/Icon/Icon';
import type { IconName } from '@/components/Icon/types';

export interface BreadcrumbItemProps extends HTMLAttributes<HTMLLIElement> {
/** The text label of the breadcrumb */
children: ReactNode;
/** Optional icon displayed before the label */
icon?: IconName;
/** Whether this is the current/active page (last item) */
active?: boolean;
/** Optional href β€” when provided, the item renders as a link */
href?: string;
}

export interface BreadcrumbsProps extends HTMLAttributes<HTMLElement> {
/** Breadcrumb items to render */
children: ReactNode;
}

const Nav = styled.nav`
display: flex;
align-items: center;
`;

const List = styled.ol`
display: flex;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
`;

const Separator = styled.li`
display: flex;
align-items: center;
color: ${({ theme }) => theme.click.global.color.text.muted};
`;

const ItemWrapper = styled.li<{ $active?: boolean }>`
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;

${({ $active, theme }) => `
font: ${
$active
? theme.click.docs.typography.breadcrumbs.active
: theme.click.docs.typography.breadcrumbs.default
};
color: ${
$active
? theme.click.global.color.text.default
: theme.click.global.color.text.muted
};
`}
`;

const StyledLink = styled.a`
&& {
text-decoration: none;
color: ${({ theme }) => theme.click.global.color.text.muted};
cursor: pointer;
}

&&:hover {
text-decoration: underline;
}
`;

const ItemIcon = styled(Icon)`
flex-shrink: 0;
`;

const BreadcrumbItem = forwardRef<HTMLLIElement, BreadcrumbItemProps>(
({ children, icon, active = false, href, ...props }, ref) => (
<ItemWrapper
ref={ref}
$active={active}
aria-current={active ? 'page' : undefined}
{...props}
>
{icon && (
<ItemIcon
name={icon}
size="xs"
aria-hidden
/>
)}
{href && !active ? (
<StyledLink href={href}>{children}</StyledLink>
) : (
<span>{children}</span>
)}
</ItemWrapper>
)
);

BreadcrumbItem.displayName = 'Breadcrumbs.Item';

const BreadcrumbSeparator = () => (
<Separator
role="presentation"
aria-hidden
>
<Icon
name="chevron-right"
size="sm"
/>
</Separator>
);

export const Breadcrumbs = forwardRef<HTMLElement, BreadcrumbsProps>(
({ children, ...props }, ref) => (
<Nav
ref={ref}
aria-label="Breadcrumb"
{...props}
>
<List>{children}</List>
</Nav>
)
);

Breadcrumbs.displayName = 'Breadcrumbs';

export { BreadcrumbItem, BreadcrumbSeparator };
5 changes: 5 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export {
export { AutoComplete } from '@/components/AutoComplete/AutoComplete';
export { Avatar } from '@/components/Avatar/Avatar';
export { Badge } from '@/components/Badge/Badge';
export {
Breadcrumbs,
BreadcrumbItem,
BreadcrumbSeparator,
} from '@/components/Breadcrumbs/Breadcrumbs';
export { BigStat } from '@/components/BigStat/BigStat';
export { ButtonGroup } from '@/components/ButtonGroup/ButtonGroup';
export { Button } from '@/components/Button/Button';
Expand Down
2 changes: 2 additions & 0 deletions src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import { ButtonProps, ButtonType } from './Button/Button';
import { ButtonGroupProps } from './ButtonGroup/ButtonGroup';
import { BadgeProps } from './Badge/Badge';
import { BreadcrumbsProps, BreadcrumbItemProps } from './Breadcrumbs/Breadcrumbs';
import { AvatarProps } from './Avatar/Avatar';
import { AlertProps } from './Alert/Alert';
import { IconButtonProps } from './IconButton/IconButton';
Expand All @@ -43,7 +44,7 @@
TableColumnConfigProps,
TableRowType,
TableProps,
TableHeaderType,

Check warning on line 47 in src/components/types.ts

View workflow job for this annotation

GitHub Actions / code-quality-checks

`TableHeaderType` is deprecated. The TableHeaderType field have been deprecated to favour TableColumnConfigProps
} from './Table/Table';
export type { BigStatProps } from './BigStat/BigStat';
export type { TextAreaFieldProps } from './Input/TextArea';
Expand Down Expand Up @@ -88,6 +89,7 @@
export type { AlertProps };
export type { AvatarProps };
export type { BadgeProps };
export type { BreadcrumbsProps, BreadcrumbItemProps };
export type { ButtonGroupProps };
export type { ButtonProps, ButtonType };
export type { CardSecondaryProps, BadgeState };
Expand Down
Loading