diff --git a/src/components/Common/FlowBreadcrumbs/FlowBreadcrumbs.tsx b/src/components/Common/FlowBreadcrumbs/FlowBreadcrumbs.tsx index 29dfdb39..7782858f 100644 --- a/src/components/Common/FlowBreadcrumbs/FlowBreadcrumbs.tsx +++ b/src/components/Common/FlowBreadcrumbs/FlowBreadcrumbs.tsx @@ -1,16 +1,22 @@ import type { CustomTypeOptions } from 'i18next' import { useTranslation } from 'react-i18next' -import { useMemo } from 'react' +import { useMemo, useRef } from 'react' import type { FlowBreadcrumbsProps } from './FlowBreadcrumbsTypes' import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' import { componentEvents } from '@/shared/constants' import { useI18n } from '@/i18n/I18n' +import { useContainerBreakpoints } from '@/hooks/useContainerBreakpoints/useContainerBreakpoints' export function FlowBreadcrumbs({ breadcrumbs, currentBreadcrumbId, onEvent, }: FlowBreadcrumbsProps) { + const breadcrumbContainerRef = useRef(null) + const breakpoints = useContainerBreakpoints({ ref: breadcrumbContainerRef }) + // Small if we only contain the base breakpoint + const isSmallContainer = breakpoints.length === 1 + const { Breadcrumbs } = useComponentContext() const namespaces = breadcrumbs.reduce>( (acc, breadcrumb) => { @@ -56,10 +62,13 @@ export function FlowBreadcrumbs({ } return ( - +
+ +
) } diff --git a/src/components/Common/UI/Breadcrumbs/Breadcrumbs.module.scss b/src/components/Common/UI/Breadcrumbs/Breadcrumbs.module.scss index fce7219e..0ebf3b7b 100644 --- a/src/components/Common/UI/Breadcrumbs/Breadcrumbs.module.scss +++ b/src/components/Common/UI/Breadcrumbs/Breadcrumbs.module.scss @@ -1,10 +1,4 @@ -@use '../../../../styles/Helpers' as *; - :global(.GSDK) { - .root { - margin-bottom: toRem(24); - } - .list { display: flex; align-items: center; @@ -35,6 +29,10 @@ } } + .clickable { + cursor: pointer; + } + .link { all: unset; cursor: pointer; @@ -52,7 +50,30 @@ } } - .clickable { + .smallBack { + all: unset; + display: flex; + align-items: center; + gap: toRem(8); cursor: pointer; + font-size: var(--g-fontSizeRegular); + font-weight: var(--g-fontWeightMedium); + color: var(--g-colorBodyContent); + border-radius: toRem(4); + + &::before { + content: '‹'; + font-size: var(--g-fontSizeLarge); + color: var(--g-colorBodySubContent); + } + + &:hover { + color: var(--g-colorPrimaryAccent); + } + + &:focus-visible { + outline: var(--g-focusRingWidth) solid var(--g-focusRingColor); + outline-offset: 2px; + } } } diff --git a/src/components/Common/UI/Breadcrumbs/Breadcrumbs.test.tsx b/src/components/Common/UI/Breadcrumbs/Breadcrumbs.test.tsx index fd5f4023..b7764937 100644 --- a/src/components/Common/UI/Breadcrumbs/Breadcrumbs.test.tsx +++ b/src/components/Common/UI/Breadcrumbs/Breadcrumbs.test.tsx @@ -176,6 +176,122 @@ describe('Breadcrumbs', () => { expect(buttons).toHaveLength(0) }) + describe('Small Container View', () => { + it('renders small back button when isSmallContainer is true', () => { + const onClick = vi.fn() + renderWithProviders( + , + ) + + const button = screen.getByRole('button') + expect(button).toBeInTheDocument() + expect(button).toHaveTextContent('Step Two') + }) + + it('shows previous breadcrumb label in small container view', () => { + const onClick = vi.fn() + renderWithProviders( + , + ) + + expect(screen.getByText('Step Three')).toBeInTheDocument() + expect(screen.queryByText('Step One')).not.toBeInTheDocument() + expect(screen.queryByText('Step Two')).not.toBeInTheDocument() + expect(screen.queryByText('Step Four')).not.toBeInTheDocument() + }) + + it('calls onClick with previous breadcrumb id when small back button is clicked', async () => { + const user = userEvent.setup() + const onClick = vi.fn() + renderWithProviders( + , + ) + + const button = screen.getByRole('button') + await user.click(button) + + expect(onClick).toHaveBeenCalledWith('step-two') + expect(onClick).toHaveBeenCalledTimes(1) + }) + + it('does not render small view when at first breadcrumb', () => { + const onClick = vi.fn() + renderWithProviders( + , + ) + + const list = screen.getByRole('list') + expect(list).toBeInTheDocument() + expect(screen.getAllByRole('listitem')).toHaveLength(4) + }) + + it('does not render small view when onClick is not provided', () => { + renderWithProviders( + , + ) + + const list = screen.getByRole('list') + expect(list).toBeInTheDocument() + expect(screen.queryByRole('button')).not.toBeInTheDocument() + }) + + it('renders full breadcrumb list when isSmallContainer is false', () => { + const onClick = vi.fn() + renderWithProviders( + , + ) + + const listItems = screen.getAllByRole('listitem') + expect(listItems).toHaveLength(4) + const buttons = screen.getAllByRole('button') + expect(buttons).toHaveLength(3) + }) + + it('maintains navigation accessibility in small container view', () => { + const onClick = vi.fn() + renderWithProviders( + , + ) + + const nav = screen.getByRole('navigation') + expect(nav).toHaveAttribute('aria-label', 'Breadcrumbs') + }) + }) + describe('Accessibility', () => { const testCases = [ { diff --git a/src/components/Common/UI/Breadcrumbs/Breadcrumbs.tsx b/src/components/Common/UI/Breadcrumbs/Breadcrumbs.tsx index 86a7330d..fa98441c 100644 --- a/src/components/Common/UI/Breadcrumbs/Breadcrumbs.tsx +++ b/src/components/Common/UI/Breadcrumbs/Breadcrumbs.tsx @@ -1,18 +1,44 @@ import classnames from 'classnames' import { Flex } from '../../Flex' -import type { BreadcrumbsProps } from './BreadcrumbsTypes' +import { type BreadcrumbsProps, BreadcrumbsDefaults } from './BreadcrumbsTypes' import styles from './Breadcrumbs.module.scss' +import { applyMissingDefaults } from '@/helpers/applyMissingDefaults' + +export function Breadcrumbs(rawProps: BreadcrumbsProps) { + const resolvedProps = applyMissingDefaults(rawProps, BreadcrumbsDefaults) + const { + className, + breadcrumbs, + currentBreadcrumbId, + 'aria-label': ariaLabel, + onClick, + isSmallContainer, + } = resolvedProps + + const currentIndex = breadcrumbs.findIndex(b => b.id === currentBreadcrumbId) + const previousBreadcrumb = currentIndex > 0 ? breadcrumbs[currentIndex - 1] : null + + if (isSmallContainer && previousBreadcrumb && onClick) { + return ( + + + + ) + } -export function Breadcrumbs({ - className, - breadcrumbs, - currentBreadcrumbId, - 'aria-label': ariaLabel = 'Breadcrumbs', - onClick, -}: BreadcrumbsProps) { return ( -