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
166 changes: 166 additions & 0 deletions src/components/Common/DetailViewLayout/DetailViewLayout.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { useState } from 'react'
import { DetailViewLayout } from './DetailViewLayout'
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
import UserIcon from '@/assets/icons/user-02.svg?react'
import EditIcon from '@/assets/icons/edit-02.svg?react'

export default {
title: 'Common/DetailViewLayout',
}

function DetailsTabContent() {
const Components = useComponentContext()

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Components.DescriptionList
items={[
{ term: 'Type', description: 'Based on hours worked' },
{
term: 'Rate',
description: '1.0 hour(s) for every 10.0 hour(s) worked, including overtime',
},
{ term: 'Reset date', description: 'January 1' },
]}
/>
</div>
)
}

function EmployeesTabContent() {
const Components = useComponentContext()

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<Components.Heading as="h3" styledAs="h4">
3 employees enrolled
</Components.Heading>
<Components.Text variant="supporting">
Employee table content goes here (built in a separate ticket).
</Components.Text>
</div>
)
}

function PolicyActions() {
const Components = useComponentContext()

return (
<>
<Components.Button variant="secondary" icon={<UserIcon />}>
Add employees
</Components.Button>
<Components.Button variant="secondary" icon={<EditIcon />}>
Edit policy
</Components.Button>
</>
)
}

export const Default = () => {
const [selectedTabId, setSelectedTabId] = useState('details')

const tabs = [
{ id: 'details', label: 'Details', content: <DetailsTabContent /> },
{ id: 'employees', label: 'Employees', content: <EmployeesTabContent /> },
]

return (
<DetailViewLayout
title="Company PTO"
subtitle="Paid time off policy"
onBack={() => {}}
backLabel="Time off policies"
actions={<PolicyActions />}
tabs={tabs}
selectedTabId={selectedTabId}
onTabChange={setSelectedTabId}
/>
)
}

export const WithoutBackButton = () => {
const [selectedTabId, setSelectedTabId] = useState('details')

const tabs = [
{ id: 'details', label: 'Details', content: <DetailsTabContent /> },
{ id: 'employees', label: 'Employees', content: <EmployeesTabContent /> },
]

return (
<DetailViewLayout
title="Sick Leave"
subtitle="Sick leave policy"
actions={<PolicyActions />}
tabs={tabs}
selectedTabId={selectedTabId}
onTabChange={setSelectedTabId}
/>
)
}

export const WithoutActions = () => {
const [selectedTabId, setSelectedTabId] = useState('details')

const tabs = [
{ id: 'details', label: 'Details', content: <DetailsTabContent /> },
{ id: 'employees', label: 'Employees', content: <EmployeesTabContent /> },
]

return (
<DetailViewLayout
title="Company PTO"
subtitle="Paid time off policy"
onBack={() => {}}
backLabel="Time off policies"
tabs={tabs}
selectedTabId={selectedTabId}
onTabChange={setSelectedTabId}
/>
)
}

export const MinimalConfig = () => {
const Components = useComponentContext()
const [selectedTabId, setSelectedTabId] = useState('overview')

const tabs = [
{
id: 'overview',
label: 'Overview',
content: <Components.Text>Overview content</Components.Text>,
},
{
id: 'history',
label: 'History',
content: <Components.Text>History content</Components.Text>,
},
]

return (
<DetailViewLayout
title="Policy Name"
tabs={tabs}
selectedTabId={selectedTabId}
onTabChange={setSelectedTabId}
/>
)
}

export const SingleTab = () => {
const [selectedTabId, setSelectedTabId] = useState('details')

const tabs = [{ id: 'details', label: 'Details', content: <DetailsTabContent /> }]

return (
<DetailViewLayout
title="Holiday Pay"
subtitle="Holiday pay policy"
onBack={() => {}}
backLabel="Time off policies"
tabs={tabs}
selectedTabId={selectedTabId}
onTabChange={setSelectedTabId}
/>
)
}
220 changes: 220 additions & 0 deletions src/components/Common/DetailViewLayout/DetailViewLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { DetailViewLayout } from './DetailViewLayout'
import { renderWithProviders } from '@/test-utils/renderWithProviders'

vi.mock('@/hooks/useContainerBreakpoints/useContainerBreakpoints')

describe('DetailViewLayout', () => {
const tabs = [
{ id: 'details', label: 'Details', content: <div>Details tab content</div> },
{ id: 'employees', label: 'Employees', content: <div>Employees tab content</div> },
]
beforeEach(async () => {
const { useContainerBreakpoints } =
await import('@/hooks/useContainerBreakpoints/useContainerBreakpoints')
vi.mocked(useContainerBreakpoints).mockReturnValue(['base', 'small', 'medium'])
})

describe('Rendering', () => {
it('renders title as a heading', async () => {
renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
/>,
)

expect(await screen.findByRole('heading', { name: 'Company PTO' })).toBeInTheDocument()
})

it('renders subtitle when provided', async () => {
renderWithProviders(
<DetailViewLayout
title="Company PTO"
subtitle="Paid time off policy"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
/>,
)

expect(await screen.findByText('Paid time off policy')).toBeInTheDocument()
})

it('does not render subtitle when omitted', async () => {
renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
/>,
)

await screen.findByRole('heading', { name: 'Company PTO' })
expect(screen.queryByText('Paid time off policy')).not.toBeInTheDocument()
})

it('renders tab buttons matching provided labels', async () => {
renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
/>,
)

expect(await screen.findByRole('tab', { name: 'Details' })).toBeInTheDocument()
expect(await screen.findByRole('tab', { name: 'Employees' })).toBeInTheDocument()
})

it('renders the selected tab content', async () => {
renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
/>,
)

expect(await screen.findByText('Details tab content')).toBeInTheDocument()
})

it('renders actions when provided', async () => {
renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
actions={<button>Edit policy</button>}
/>,
)

expect(await screen.findByRole('button', { name: 'Edit policy' })).toBeInTheDocument()
})

it('does not render actions wrapper when actions are omitted', async () => {
const { container } = renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
/>,
)

await screen.findByRole('heading', { name: 'Company PTO' })
expect(container.querySelector('[class*="actions"]')).not.toBeInTheDocument()
})
})

describe('Back navigation', () => {
it('renders back button with backLabel when onBack is provided', async () => {
renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
onBack={vi.fn()}
backLabel="Time off policies"
/>,
)

expect(await screen.findByRole('button', { name: /Time off policies/i })).toBeInTheDocument()
})

it('does not render back button when onBack is omitted', async () => {
renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
/>,
)

await screen.findByRole('heading', { name: 'Company PTO' })
expect(screen.queryByRole('button', { name: /Time off policies/i })).not.toBeInTheDocument()
})

it('calls onBack when back button is clicked', async () => {
const user = userEvent.setup()
const handleBack = vi.fn()

renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
onBack={handleBack}
backLabel="Time off policies"
/>,
)

await user.click(await screen.findByRole('button', { name: /Time off policies/i }))
expect(handleBack).toHaveBeenCalledTimes(1)
})
})

describe('Tab interaction', () => {
it('calls onTabChange when a different tab is clicked', async () => {
const user = userEvent.setup()
const handleTabChange = vi.fn()

renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={handleTabChange}
/>,
)

await user.click(await screen.findByRole('tab', { name: 'Employees' }))
expect(handleTabChange).toHaveBeenCalledWith('employees')
})
})

describe('Accessibility', () => {
it('should not have any accessibility violations with default config', async () => {
const { container } = renderWithProviders(
<DetailViewLayout
title="Company PTO"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
/>,
)

await screen.findByRole('heading', { name: 'Company PTO' })
await expectNoAxeViolations(container)
})

it('should not have any accessibility violations with back button and actions', async () => {
const { container } = renderWithProviders(
<DetailViewLayout
title="Company PTO"
subtitle="Paid time off policy"
tabs={tabs}
selectedTabId="details"
onTabChange={vi.fn()}
onBack={vi.fn()}
backLabel="Time off policies"
actions={<button>Edit policy</button>}
/>,
)

await screen.findByRole('heading', { name: 'Company PTO' })
await expectNoAxeViolations(container)
})
})
})
Loading