feat: add SectionAccordion component to shared/ui
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { SectionAccordion } from './ui/SectionAccordion'
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { SectionAccordion } from './SectionAccordion'
|
||||
|
||||
const defaultProps = {
|
||||
number: '01',
|
||||
title: 'About',
|
||||
id: 'about',
|
||||
isActive: false,
|
||||
onClick: vi.fn(),
|
||||
children: <p>Content here</p>,
|
||||
}
|
||||
|
||||
describe('SectionAccordion', () => {
|
||||
describe('collapsed state (isActive=false)', () => {
|
||||
it('renders a section element with the given id', () => {
|
||||
const { container } = render(<SectionAccordion {...defaultProps} />)
|
||||
expect(container.querySelector('section#about')).toBeInTheDocument()
|
||||
})
|
||||
it('renders a button with number and title', () => {
|
||||
render(<SectionAccordion {...defaultProps} />)
|
||||
expect(screen.getByRole('button', { name: /01.*About/i })).toBeInTheDocument()
|
||||
})
|
||||
it('does not render children', () => {
|
||||
render(<SectionAccordion {...defaultProps} />)
|
||||
expect(screen.queryByText('Content here')).not.toBeInTheDocument()
|
||||
})
|
||||
it('calls onClick when button is clicked', async () => {
|
||||
const onClick = vi.fn()
|
||||
render(<SectionAccordion {...defaultProps} onClick={onClick} />)
|
||||
await userEvent.click(screen.getByRole('button'))
|
||||
expect(onClick).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
describe('active state (isActive=true)', () => {
|
||||
const activeProps = { ...defaultProps, isActive: true }
|
||||
|
||||
it('renders an h1 with number and title', () => {
|
||||
render(<SectionAccordion {...activeProps} />)
|
||||
expect(screen.getByRole('heading', { level: 1, name: /01.*About/i })).toBeInTheDocument()
|
||||
})
|
||||
it('renders children', () => {
|
||||
render(<SectionAccordion {...activeProps} />)
|
||||
expect(screen.getByText('Content here')).toBeInTheDocument()
|
||||
})
|
||||
it('does not render a button', () => {
|
||||
render(<SectionAccordion {...activeProps} />)
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument()
|
||||
})
|
||||
it('content wrapper has animate-fadeIn class', () => {
|
||||
const { container } = render(<SectionAccordion {...activeProps} />)
|
||||
expect(container.querySelector('.animate-fadeIn')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface SectionAccordionProps {
|
||||
/**
|
||||
* Display number prefix (e.g. "01")
|
||||
*/
|
||||
number: string
|
||||
/**
|
||||
* Section title
|
||||
*/
|
||||
title: string
|
||||
/**
|
||||
* HTML id for anchor navigation
|
||||
*/
|
||||
id: string
|
||||
/**
|
||||
* Whether this section is expanded
|
||||
*/
|
||||
isActive: boolean
|
||||
/**
|
||||
* Called when the collapsed header is clicked
|
||||
*/
|
||||
onClick: () => void
|
||||
/**
|
||||
* Section content, shown when active
|
||||
*/
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
* Accordion-style section that collapses to a heading button when inactive.
|
||||
*/
|
||||
export function SectionAccordion({ number, title, id, isActive, onClick, children }: SectionAccordionProps) {
|
||||
return (
|
||||
<section id={id} className="scroll-mt-8">
|
||||
{isActive ? (
|
||||
<div className="mb-12">
|
||||
<div className="mb-16">
|
||||
<h1
|
||||
className="font-heading font-black text-5xl leading-[1.2] mb-0"
|
||||
style={{ fontVariationSettings: "'WONK' 1, 'SOFT' 0" }}
|
||||
>
|
||||
{number}. {title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="animate-fadeIn">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="w-full text-left mb-3 py-3 transition-all duration-200 hover:opacity-60 group"
|
||||
>
|
||||
<h2
|
||||
className="font-heading font-black text-2xl sm:text-3xl opacity-30 group-hover:opacity-50 transition-opacity"
|
||||
style={{ fontVariationSettings: "'WONK' 1, 'SOFT' 0" }}
|
||||
>
|
||||
{number}. {title}
|
||||
</h2>
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user