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