Skip to content
Merged
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
21 changes: 15 additions & 6 deletions src/components/Common/FlowBreadcrumbs/FlowBreadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement | null>(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<Array<keyof CustomTypeOptions['resources']>>(
(acc, breadcrumb) => {
Expand Down Expand Up @@ -56,10 +62,13 @@ export function FlowBreadcrumbs({
}

return (
<Breadcrumbs
breadcrumbs={parsedBreadcrumbs}
currentBreadcrumbId={currentBreadcrumbId}
onClick={handleBreadcrumbClick}
/>
<div ref={breadcrumbContainerRef}>
<Breadcrumbs
isSmallContainer={isSmallContainer}
breadcrumbs={parsedBreadcrumbs}
currentBreadcrumbId={currentBreadcrumbId}
onClick={handleBreadcrumbClick}
/>
</div>
)
}
35 changes: 28 additions & 7 deletions src/components/Common/UI/Breadcrumbs/Breadcrumbs.module.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
@use '../../../../styles/Helpers' as *;

:global(.GSDK) {
.root {
margin-bottom: toRem(24);
}

.list {
display: flex;
align-items: center;
Expand Down Expand Up @@ -35,6 +29,10 @@
}
}

.clickable {
cursor: pointer;
}

.link {
all: unset;
cursor: pointer;
Expand All @@ -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;
}
}
}
116 changes: 116 additions & 0 deletions src/components/Common/UI/Breadcrumbs/Breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Breadcrumbs
breadcrumbs={mockBreadcrumbs}
currentBreadcrumbId="step-three"
onClick={onClick}
isSmallContainer={true}
/>,
)

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(
<Breadcrumbs
breadcrumbs={mockBreadcrumbs}
currentBreadcrumbId="step-four"
onClick={onClick}
isSmallContainer={true}
/>,
)

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(
<Breadcrumbs
breadcrumbs={mockBreadcrumbs}
currentBreadcrumbId="step-three"
onClick={onClick}
isSmallContainer={true}
/>,
)

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(
<Breadcrumbs
breadcrumbs={mockBreadcrumbs}
currentBreadcrumbId="step-one"
onClick={onClick}
isSmallContainer={true}
/>,
)

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(
<Breadcrumbs
breadcrumbs={mockBreadcrumbs}
currentBreadcrumbId="step-three"
isSmallContainer={true}
/>,
)

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(
<Breadcrumbs
breadcrumbs={mockBreadcrumbs}
currentBreadcrumbId="step-three"
onClick={onClick}
isSmallContainer={false}
/>,
)

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(
<Breadcrumbs
breadcrumbs={mockBreadcrumbs}
currentBreadcrumbId="step-three"
onClick={onClick}
isSmallContainer={true}
/>,
)

const nav = screen.getByRole('navigation')
expect(nav).toHaveAttribute('aria-label', 'Breadcrumbs')
})
})

describe('Accessibility', () => {
const testCases = [
{
Expand Down
44 changes: 35 additions & 9 deletions src/components/Common/UI/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Flex flexDirection="column">
<nav aria-label={ariaLabel} className={className}>
<button
type="button"
className={styles.smallBack}
onClick={() => {
onClick(previousBreadcrumb.id)
}}
>
{previousBreadcrumb.label}
</button>
</nav>
</Flex>
)
}

export function Breadcrumbs({
className,
breadcrumbs,
currentBreadcrumbId,
'aria-label': ariaLabel = 'Breadcrumbs',
onClick,
}: BreadcrumbsProps) {
return (
<Flex flexDirection="column">
<nav aria-label={ariaLabel} className={classnames(styles.root, className)}>
<nav aria-label={ariaLabel} className={className}>
<ol className={styles.list}>
{breadcrumbs.map(breadcrumb => {
const isCurrentbreadcrumb = breadcrumb.id === currentBreadcrumbId
Expand Down
15 changes: 15 additions & 0 deletions src/components/Common/UI/Breadcrumbs/BreadcrumbsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,19 @@ export interface BreadcrumbsProps {
* Event handler for breadcrumb navigation
*/
onClick?: (id: string) => void
/**
* Passed to the breadcrumbs when the container size is small (640px and below)
* At this size, the breadcrumb typically does not have sufficient size to render
* completely. In our implementation, we switch to a condensed mobile version of
* the breadcrumbs
*/
isSmallContainer?: boolean
}

/**
* Default prop values for Breadcrumbs component.
*/
export const BreadcrumbsDefaults = {
isSmallContainer: false,
'aria-label': 'Breadcrumbs',
} as const satisfies Partial<BreadcrumbsProps>
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const PayrollOverviewPresentation = ({
taxes,
isSubmitting = false,
isProcessed,
alerts,
alerts = [],
}: PayrollOverviewProps) => {
const { Alert, Button, ButtonIcon, Dialog, Heading, Text, Tabs, LoadingSpinner } =
useComponentContext()
Expand Down Expand Up @@ -561,7 +561,7 @@ export const PayrollOverviewPresentation = ({
</LoadingIndicator>
) : (
<>
{alerts?.length && alerts.length > 0 && (
{alerts.length > 0 && (
<Flex flexDirection={'column'} gap={16}>
{alerts.map((alert, index) => (
<Alert key={`${alert.type}-${alert.title}`} label={alert.title} status={alert.type}>
Expand Down