From 0207b19b4540046fb8d979f99e40a299000528ff Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Thu, 5 Mar 2026 13:06:21 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=F0=9F=9A=80=20scaffold=20Breadcrum?= =?UTF-8?q?bs=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add initial Breadcrumbs component skeleton with proposed API, Storybook stories, and basic tests for RFC review. Made-with: Cursor --- .../Breadcrumbs/Breadcrumbs.stories.tsx | 56 ++++++++ .../Breadcrumbs/Breadcrumbs.test.tsx | 51 +++++++ src/components/Breadcrumbs/Breadcrumbs.tsx | 127 ++++++++++++++++++ src/components/index.ts | 5 + src/components/types.ts | 2 + 5 files changed, 241 insertions(+) create mode 100644 src/components/Breadcrumbs/Breadcrumbs.stories.tsx create mode 100644 src/components/Breadcrumbs/Breadcrumbs.test.tsx create mode 100644 src/components/Breadcrumbs/Breadcrumbs.tsx diff --git a/src/components/Breadcrumbs/Breadcrumbs.stories.tsx b/src/components/Breadcrumbs/Breadcrumbs.stories.tsx new file mode 100644 index 000000000..5f60e860c --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.stories.tsx @@ -0,0 +1,56 @@ +import { Meta, StoryObj } from '@storybook/react-vite'; +import { Breadcrumbs, BreadcrumbItem, BreadcrumbSeparator } from './Breadcrumbs'; + +const meta: Meta = { + component: Breadcrumbs, + title: 'Navigation/Breadcrumbs', + tags: ['breadcrumbs', 'autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + render: () => ( + + + Home + + + Data sources + + ClickPipes + + GCS Unordered mode with service account + + ), +}; + +export const WithIcon: Story = { + render: () => ( + + + Data sources + + + Settings + + ), +}; + +export const TwoLevels: Story = { + render: () => ( + + Home + + Current page + + ), +}; diff --git a/src/components/Breadcrumbs/Breadcrumbs.test.tsx b/src/components/Breadcrumbs/Breadcrumbs.test.tsx new file mode 100644 index 000000000..1ab5342d2 --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.test.tsx @@ -0,0 +1,51 @@ +import { Breadcrumbs, BreadcrumbItem, BreadcrumbSeparator } from './Breadcrumbs'; +import { renderCUI } from '@/utils/test-utils'; + +describe('Breadcrumbs', () => { + const renderBreadcrumbs = () => + renderCUI( + + + Home + + + Data sources + + Current page + + ); + + 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(); + }); +}); diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 000000000..d49c2b14f --- /dev/null +++ b/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,127 @@ +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 { + /** 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 { + /** 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: inherit; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +`; + +const ItemIcon = styled(Icon)` + flex-shrink: 0; +`; + +const BreadcrumbItem = forwardRef( + ({ children, icon, active = false, href, ...props }, ref) => ( + + {icon && ( + + )} + {href && !active ? ( + {children} + ) : ( + {children} + )} + + ) +); + +BreadcrumbItem.displayName = 'Breadcrumbs.Item'; + +const BreadcrumbSeparator = () => ( + + + +); + +export const Breadcrumbs = forwardRef( + ({ children, ...props }, ref) => ( + + ) +); + +Breadcrumbs.displayName = 'Breadcrumbs'; + +export { BreadcrumbItem, BreadcrumbSeparator }; diff --git a/src/components/index.ts b/src/components/index.ts index 0978e31b7..6f1113143 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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'; diff --git a/src/components/types.ts b/src/components/types.ts index 21d89d830..6fd7c66f6 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -21,6 +21,7 @@ import { CardSecondaryProps, BadgeState } from './CardSecondary/CardSecondary'; 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'; @@ -88,6 +89,7 @@ export type { IconButtonProps }; export type { AlertProps }; export type { AvatarProps }; export type { BadgeProps }; +export type { BreadcrumbsProps, BreadcrumbItemProps }; export type { ButtonGroupProps }; export type { ButtonProps, ButtonType }; export type { CardSecondaryProps, BadgeState }; From 1f224f46bba00069115ae06a1fa89a7d84b85442 Mon Sep 17 00:00:00 2001 From: Elizabet Oliveira Date: Thu, 5 Mar 2026 13:31:36 +0000 Subject: [PATCH 2/2] =?UTF-8?q?style:=20=F0=9F=8E=A8=20update=20Breadcrumb?= =?UTF-8?q?s=20component=20styling=20for=20link=20hover=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the StyledLink component to enhance hover effects and ensure consistent text color based on theme. This improves accessibility and visual feedback for users. --- src/components/Breadcrumbs/Breadcrumbs.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/Breadcrumbs/Breadcrumbs.tsx b/src/components/Breadcrumbs/Breadcrumbs.tsx index d49c2b14f..5da9fb9f7 100644 --- a/src/components/Breadcrumbs/Breadcrumbs.tsx +++ b/src/components/Breadcrumbs/Breadcrumbs.tsx @@ -59,11 +59,13 @@ const ItemWrapper = styled.li<{ $active?: boolean }>` `; const StyledLink = styled.a` - text-decoration: none; - color: inherit; - cursor: pointer; + && { + text-decoration: none; + color: ${({ theme }) => theme.click.global.color.text.muted}; + cursor: pointer; + } - &:hover { + &&:hover { text-decoration: underline; } `;