refactor: rename section item to SectionPanel, list to SectionAccordion

This commit is contained in:
Ilia Mashkov
2026-07-14 17:05:31 +03:00
parent c3e8e38a58
commit 702be45ce3
11 changed files with 43 additions and 43 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
import { notFound } from 'next/navigation';
import type { SectionRecord } from '$entities/Section';
import { getCollection } from '$shared/api';
import { SectionAccordion } from '$widgets/SectionAccordion';
import { SectionFactory } from '$widgets/SectionFactory';
import { SectionsAccordion } from '$widgets/SectionsAccordion';
/**
* Optional catchall: `/` → first section, `/:slug` → that section.
@@ -46,11 +46,11 @@ export default async function SectionPage({ params }: Props) {
return (
<main className="px-4 py-6 sm:px-8 sm:py-12 lg:py-16 lg:px-16">
<SectionsAccordion sections={sections} activeSlug={activeSlug}>
<SectionAccordion sections={sections} activeSlug={activeSlug}>
{sections.map((s) => (
<SectionFactory key={s.slug} slug={s.slug} />
))}
</SectionsAccordion>
</SectionAccordion>
</main>
);
}
@@ -1 +0,0 @@
export * from './SectionAccordion';
@@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import { SectionAccordion } from './SectionAccordion';
import { SectionPanel } from './SectionPanel';
const meta: Meta<typeof SectionAccordion> = {
title: 'Shared/SectionAccordion',
component: SectionAccordion,
const meta: Meta<typeof SectionPanel> = {
title: 'Shared/SectionPanel',
component: SectionPanel,
decorators: [
(Story) => (
<div className="p-8 bg-ochre-clay">
@@ -15,7 +15,7 @@ const meta: Meta<typeof SectionAccordion> = {
export default meta;
type Story = StoryObj<typeof SectionAccordion>;
type Story = StoryObj<typeof SectionPanel>;
export const Active: Story = {
args: {
@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import { SectionAccordion } from './SectionAccordion';
import { SectionPanel } from './SectionPanel';
const defaultProps = {
number: '01',
@@ -10,30 +10,30 @@ const defaultProps = {
children: <p>Content here</p>,
};
describe('SectionAccordion', () => {
describe('SectionPanel', () => {
describe('collapsed state (isActive=false)', () => {
it('renders a section element with the given id', () => {
const { container } = render(<SectionAccordion {...defaultProps} />);
const { container } = render(<SectionPanel {...defaultProps} />);
expect(container.querySelector('section#about')).toBeInTheDocument();
});
it('renders a link with number and title', () => {
render(<SectionAccordion {...defaultProps} />);
render(<SectionPanel {...defaultProps} />);
expect(screen.getByRole('link', { name: /01.*About/i })).toBeInTheDocument();
});
it('link points to the correct href', () => {
render(<SectionAccordion {...defaultProps} />);
render(<SectionPanel {...defaultProps} />);
expect(screen.getByRole('link', { name: /01.*About/i })).toHaveAttribute('href', '/about');
});
it('does not render children', () => {
render(<SectionAccordion {...defaultProps} />);
render(<SectionPanel {...defaultProps} />);
expect(screen.queryByText('Content here')).not.toBeInTheDocument();
});
it('does not render a button', () => {
render(<SectionAccordion {...defaultProps} />);
render(<SectionPanel {...defaultProps} />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
});
@@ -42,17 +42,17 @@ describe('SectionAccordion', () => {
const activeProps = { ...defaultProps, isActive: true };
it('renders an h1 with number and title', () => {
render(<SectionAccordion {...activeProps} />);
render(<SectionPanel {...activeProps} />);
expect(screen.getByRole('heading', { level: 1, name: /01.*About/i })).toBeInTheDocument();
});
it('renders children', () => {
render(<SectionAccordion {...activeProps} />);
render(<SectionPanel {...activeProps} />);
expect(screen.getByText('Content here')).toBeInTheDocument();
});
it('does not render a link', () => {
render(<SectionAccordion {...activeProps} />);
render(<SectionPanel {...activeProps} />);
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
});
@@ -2,7 +2,7 @@ import Link from 'next/link';
import type { ReactNode } from 'react';
import { ViewTransitionWrapper } from '$shared/ui';
interface SectionAccordionProps {
interface SectionPanelProps {
/**
* Display number prefix (e.g. "01")
*/
@@ -32,7 +32,7 @@ interface SectionAccordionProps {
/**
* Accordion-style section that collapses to a navigation link when inactive.
*/
export function SectionAccordion({ number, title, id, isActive, href, children }: SectionAccordionProps) {
export function SectionPanel({ number, title, id, isActive, href, children }: SectionPanelProps) {
const heading = `${number}. ${title}`;
return (
@@ -0,0 +1 @@
export * from './SectionPanel';
+1 -1
View File
@@ -1 +1 @@
export * from './SectionAccordion';
export * from './SectionPanel';
+1
View File
@@ -0,0 +1 @@
export { SectionAccordion } from './ui/SectionAccordion/SectionAccordion';
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import type { SectionRecord } from '$entities/Section';
import { SectionsAccordion } from './SectionsAccordion';
import { SectionAccordion } from './SectionAccordion';
const baseRecord = { collectionId: 'c1', collectionName: 'sections', created: '', updated: '' };
@@ -10,26 +10,26 @@ const sections: SectionRecord[] = [
{ ...baseRecord, id: '3', slug: 'skills', title: 'Skills', order: 3 },
];
describe('SectionsAccordion', () => {
describe('SectionAccordion', () => {
describe('active section rendering', () => {
it('renders the active section as an h1', () => {
render(
<SectionsAccordion sections={sections} activeSlug="intro">
<SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div>
<div>Bio content</div>
<div>Skills content</div>
</SectionsAccordion>,
</SectionAccordion>,
);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('01. Intro');
});
it('renders inactive sections as links', () => {
render(
<SectionsAccordion sections={sections} activeSlug="intro">
<SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div>
<div>Bio content</div>
<div>Skills content</div>
</SectionsAccordion>,
</SectionAccordion>,
);
const links = screen.getAllByRole('link');
expect(links).toHaveLength(2);
@@ -37,11 +37,11 @@ describe('SectionsAccordion', () => {
it('inactive section links point to correct hrefs', () => {
render(
<SectionsAccordion sections={sections} activeSlug="intro">
<SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div>
<div>Bio content</div>
<div>Skills content</div>
</SectionsAccordion>,
</SectionAccordion>,
);
expect(screen.getByRole('link', { name: /02.*Bio/i })).toHaveAttribute('href', '/bio');
expect(screen.getByRole('link', { name: /03.*Skills/i })).toHaveAttribute('href', '/skills');
@@ -49,22 +49,22 @@ describe('SectionsAccordion', () => {
it('renders the correct active section for a given activeSlug', () => {
render(
<SectionsAccordion sections={sections} activeSlug="bio">
<SectionAccordion sections={sections} activeSlug="bio">
<div>Intro content</div>
<div>Bio content</div>
<div>Skills content</div>
</SectionsAccordion>,
</SectionAccordion>,
);
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('02. Bio');
});
it('only one section is active at a time', () => {
render(
<SectionsAccordion sections={sections} activeSlug="skills">
<SectionAccordion sections={sections} activeSlug="skills">
<div>Intro content</div>
<div>Bio content</div>
<div>Skills content</div>
</SectionsAccordion>,
</SectionAccordion>,
);
expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1);
});
@@ -73,22 +73,22 @@ describe('SectionsAccordion', () => {
describe('content slots', () => {
it('shows active section content', () => {
render(
<SectionsAccordion sections={sections} activeSlug="intro">
<SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div>
<div>Bio content</div>
<div>Skills content</div>
</SectionsAccordion>,
</SectionAccordion>,
);
expect(screen.getByText('Intro content')).toBeInTheDocument();
});
it('does not show inactive section content', () => {
render(
<SectionsAccordion sections={sections} activeSlug="intro">
<SectionAccordion sections={sections} activeSlug="intro">
<div>Intro content</div>
<div>Bio content</div>
<div>Skills content</div>
</SectionsAccordion>,
</SectionAccordion>,
);
expect(screen.queryByText('Bio content')).not.toBeInTheDocument();
expect(screen.queryByText('Skills content')).not.toBeInTheDocument();
@@ -1,7 +1,7 @@
import type { ReactNode } from 'react';
import { Children } from 'react';
import type { SectionRecord } from '$entities/Section';
import { SectionAccordion } from '$entities/Section';
import { SectionPanel } from '$entities/Section';
export interface Props {
/**
@@ -24,13 +24,13 @@ export interface Props {
* Active section is determined by the URL (activeSlug prop); inactive sections
* render as navigation links so the browser handles routing.
*/
export function SectionsAccordion({ sections, activeSlug, children }: Props) {
export function SectionAccordion({ sections, activeSlug, children }: Props) {
const slots = Children.toArray(children);
return (
<div>
{sections.map((section, i) => (
<SectionAccordion
<SectionPanel
key={section.slug}
id={section.slug}
number={String(section.order).padStart(2, '0')}
@@ -39,7 +39,7 @@ export function SectionsAccordion({ sections, activeSlug, children }: Props) {
href={`/${section.slug}`}
>
{slots[i]}
</SectionAccordion>
</SectionPanel>
))}
</div>
);
-1
View File
@@ -1 +0,0 @@
export { SectionsAccordion } from './ui/SectionsAccordion/SectionsAccordion';