From ba793e043e4d8515b1a9ea490ee2c5f92b1fd6c2 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Wed, 27 Sep 2023 15:40:16 +0200 Subject: refactor(components): rewrite Section component * Make it compliant with ESlint rules * Remove mandatory heading, it now depends on the consumer * Change defaults for hasBorder and variant --- src/components/atoms/layout/section.module.scss | 25 ----- src/components/atoms/layout/section.stories.tsx | 102 --------------------- src/components/atoms/layout/section.test.tsx | 18 ---- src/components/atoms/layout/section.tsx | 54 ----------- src/components/atoms/layout/section/index.ts | 1 + .../atoms/layout/section/section.module.scss | 25 +++++ .../atoms/layout/section/section.stories.tsx | 85 +++++++++++++++++ .../atoms/layout/section/section.test.tsx | 27 ++++++ src/components/atoms/layout/section/section.tsx | 50 ++++++++++ .../sectioned/sectioned-layout.fixtures.tsx | 59 ++++++++++++ .../sectioned/sectioned-layout.stories.tsx | 26 +----- .../templates/sectioned/sectioned-layout.test.tsx | 33 ++----- .../templates/sectioned/sectioned-layout.tsx | 22 ++--- src/pages/index.tsx | 29 ++---- 14 files changed, 274 insertions(+), 282 deletions(-) delete mode 100644 src/components/atoms/layout/section.module.scss delete mode 100644 src/components/atoms/layout/section.stories.tsx delete mode 100644 src/components/atoms/layout/section.test.tsx delete mode 100644 src/components/atoms/layout/section.tsx create mode 100644 src/components/atoms/layout/section/index.ts create mode 100644 src/components/atoms/layout/section/section.module.scss create mode 100644 src/components/atoms/layout/section/section.stories.tsx create mode 100644 src/components/atoms/layout/section/section.test.tsx create mode 100644 src/components/atoms/layout/section/section.tsx create mode 100644 src/components/templates/sectioned/sectioned-layout.fixtures.tsx diff --git a/src/components/atoms/layout/section.module.scss b/src/components/atoms/layout/section.module.scss deleted file mode 100644 index 8a33848..0000000 --- a/src/components/atoms/layout/section.module.scss +++ /dev/null @@ -1,25 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; -@use "../../../styles/abstracts/placeholders"; - -.wrapper { - @extend %grid; - - padding: var(--spacing-md) 0; - - &--borders { - border-bottom: fun.convert-px(1) solid var(--color-border); - } - - &--dark { - background: var(--color-bg-secondary); - } - - &--light { - background: var(--color-bg); - } -} - -.body, -.title { - grid-column: 2; -} diff --git a/src/components/atoms/layout/section.stories.tsx b/src/components/atoms/layout/section.stories.tsx deleted file mode 100644 index 8ab2729..0000000 --- a/src/components/atoms/layout/section.stories.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Section } from './section'; - -/** - * Section - Storybook Meta - */ -export default { - title: 'Atoms/Layout/Section', - component: Section, - args: { - variant: 'dark', - withBorder: true, - }, - argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the section element.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - content: { - control: { - type: 'text', - }, - description: 'The section content.', - type: { - name: 'string', - required: true, - }, - }, - title: { - control: { - type: 'text', - }, - description: 'The section title.', - type: { - name: 'string', - required: true, - }, - }, - variant: { - control: { - type: 'select', - }, - description: 'The section variant.', - options: ['light', 'dark'], - table: { - category: 'Styles', - defaultValue: { summary: 'dark' }, - }, - type: { - name: 'string', - required: false, - }, - }, - withBorder: { - control: { - type: 'boolean', - }, - description: 'Add a border at the bottom of the section.', - table: { - category: 'Styles', - defaultValue: { summary: true }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - }, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( -
-); - -/** - * Section Stories - Light - */ -export const Light = Template.bind({}); -Light.args = { - title: 'A title', - content: 'The content.', - variant: 'light', -}; - -/** - * Section Stories - Dark - */ -export const Dark = Template.bind({}); -Dark.args = { - title: 'A title', - content: 'The content.', - variant: 'dark', -}; diff --git a/src/components/atoms/layout/section.test.tsx b/src/components/atoms/layout/section.test.tsx deleted file mode 100644 index 6a2805d..0000000 --- a/src/components/atoms/layout/section.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { Section } from './section'; - -const title = 'Section title'; -const content = 'Section content.'; - -describe('Section', () => { - it('renders a title (h2)', () => { - render(
); - expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(title); - }); - - it('renders a content', () => { - render(
); - expect(screen.getByText(content)).toBeInTheDocument(); - }); -}); diff --git a/src/components/atoms/layout/section.tsx b/src/components/atoms/layout/section.tsx deleted file mode 100644 index 107e80a..0000000 --- a/src/components/atoms/layout/section.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { FC, HTMLAttributes, ReactNode } from 'react'; -import { Heading } from '../headings'; -import styles from './section.module.scss'; - -export type SectionVariant = 'dark' | 'light'; - -export type SectionProps = Omit< - HTMLAttributes, - 'children' | 'content' -> & { - /** - * The section content. - */ - content: ReactNode | ReactNode[]; - /** - * The section title. - */ - title: string; - /** - * The section variant. - */ - variant?: SectionVariant; - /** - * Add a border at the bottom of the section. Default: true. - */ - withBorder?: boolean; -}; - -/** - * Section component - * - * Render a section element. - */ -export const Section: FC = ({ - className = '', - content, - title, - variant = 'dark', - withBorder = true, - ...props -}) => { - const borderClass = withBorder ? styles[`wrapper--borders`] : ''; - const variantClass = styles[`wrapper--${variant}`]; - const sectionClass = `${styles.wrapper} ${borderClass} ${variantClass} ${className}`; - - return ( -
- - {title} - -
{content}
-
- ); -}; diff --git a/src/components/atoms/layout/section/index.ts b/src/components/atoms/layout/section/index.ts new file mode 100644 index 0000000..2786cf0 --- /dev/null +++ b/src/components/atoms/layout/section/index.ts @@ -0,0 +1 @@ +export * from './section'; diff --git a/src/components/atoms/layout/section/section.module.scss b/src/components/atoms/layout/section/section.module.scss new file mode 100644 index 0000000..771b8e3 --- /dev/null +++ b/src/components/atoms/layout/section/section.module.scss @@ -0,0 +1,25 @@ +@use "../../../../styles/abstracts/functions" as fun; +@use "../../../../styles/abstracts/placeholders"; + +.wrapper { + @extend %grid; + + row-gap: var(--spacing-sm); + padding: var(--spacing-md) 0; + + &--borders { + border-bottom: fun.convert-px(1) solid var(--color-border); + } + + &--dark { + background: var(--color-bg-secondary); + } + + &--light { + background: var(--color-bg); + } + + > * { + grid-column: 2; + } +} diff --git a/src/components/atoms/layout/section/section.stories.tsx b/src/components/atoms/layout/section/section.stories.tsx new file mode 100644 index 0000000..0a3388b --- /dev/null +++ b/src/components/atoms/layout/section/section.stories.tsx @@ -0,0 +1,85 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading } from '../../headings'; +import { Section } from './section'; + +/** + * Section - Storybook Meta + */ +export default { + title: 'Atoms/Layout/Section', + component: Section, + args: { + hasBorder: true, + variant: 'light', + }, + argTypes: { + children: { + description: 'The section content.', + type: { + name: 'function', + required: true, + }, + }, + hasBorder: { + control: { + type: 'boolean', + }, + description: 'Add a border at the bottom of the section.', + table: { + category: 'Styles', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + variant: { + control: { + type: 'select', + }, + description: 'The section variant.', + options: ['light', 'dark'], + table: { + category: 'Styles', + defaultValue: { summary: 'dark' }, + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( +
+); + +/** + * Section Stories - Light + */ +export const Light = Template.bind({}); +Light.args = { + children: ( + <> + A section title +
The body
+ + ), + variant: 'light', +}; + +/** + * Section Stories - Dark + */ +export const Dark = Template.bind({}); +Dark.args = { + children: ( + <> + A section title +
The body
+ + ), + variant: 'dark', +}; diff --git a/src/components/atoms/layout/section/section.test.tsx b/src/components/atoms/layout/section/section.test.tsx new file mode 100644 index 0000000..85305c0 --- /dev/null +++ b/src/components/atoms/layout/section/section.test.tsx @@ -0,0 +1,27 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Section } from './section'; + +const content = 'Section content.'; + +describe('Section', () => { + it('renders its body', () => { + render(
{content}
); + expect(rtlScreen.getByText(content)).toBeInTheDocument(); + }); + + it('renders a section with border', () => { + render(
{content}
); + expect(rtlScreen.getByText(content)).toHaveClass('wrapper--borders'); + }); + + it('renders a light section', () => { + render(
{content}
); + expect(rtlScreen.getByText(content)).toHaveClass('wrapper--light'); + }); + + it('renders a dark section', () => { + render(
{content}
); + expect(rtlScreen.getByText(content)).toHaveClass('wrapper--dark'); + }); +}); diff --git a/src/components/atoms/layout/section/section.tsx b/src/components/atoms/layout/section/section.tsx new file mode 100644 index 0000000..63c658a --- /dev/null +++ b/src/components/atoms/layout/section/section.tsx @@ -0,0 +1,50 @@ +import { + forwardRef, + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, +} from 'react'; +import styles from './section.module.scss'; + +export type SectionVariant = 'dark' | 'light'; + +export type SectionProps = Omit, 'children'> & { + /** + * The section content. + */ + children: ReactNode | ReactNode[]; + /** + * Add a border at the bottom of the section. + * + * @default false + */ + hasBorder?: boolean; + /** + * The section variant. + * + * @default 'light' + */ + variant?: SectionVariant; +}; + +const SectionWithRef: ForwardRefRenderFunction = ( + { children, className = '', hasBorder = false, variant = 'light', ...props }, + ref +) => { + const borderClass = hasBorder ? styles[`wrapper--borders`] : ''; + const variantClass = styles[`wrapper--${variant}`]; + const sectionClass = `${styles.wrapper} ${borderClass} ${variantClass} ${className}`; + + return ( +
+ {children} +
+ ); +}; + +/** + * Section component + * + * Render a section element with a heading and a body. + */ +export const Section = forwardRef(SectionWithRef); diff --git a/src/components/templates/sectioned/sectioned-layout.fixtures.tsx b/src/components/templates/sectioned/sectioned-layout.fixtures.tsx new file mode 100644 index 0000000..0da8e7d --- /dev/null +++ b/src/components/templates/sectioned/sectioned-layout.fixtures.tsx @@ -0,0 +1,59 @@ +/* eslint-disable react/jsx-no-literals */ +import { Heading } from '../../atoms'; +import type { PageSection } from './sectioned-layout'; + +export const sections: PageSection[] = [ + { + id: 'section-1', + children: ( + <> + Section 1 +
+ Qui suscipit ea et aut dicta. Quia ut dignissimos. Sapiente beatae + voluptatem quis et. Nemo vitae magni. Nihil iste officia est sed esse + molestiae doloribus. Quia temporibus nobis ea fuga quis incidunt + doloribus eaque. +
+ + ), + }, + { + id: 'section-2', + children: ( + <> + Section 2 +
+ Reprehenderit aut magnam ut quos. Voluptatibus beatae et. Earum non + atque voluptatum illum rem distinctio repellat. +
+ + ), + }, + { + id: 'section-3', + children: ( + <> + Section 3 +
+ Placeat rem dolores dolore illum earum officia dolore. Ut est ducimus. + Officia eveniet pariatur ut laboriosam voluptatibus aut doloremque + natus quis. +
+ + ), + }, + { + id: 'section-4', + children: ( + <> + Section 4 +
+ Vitae facere ipsa eum sunt debitis veritatis dolorem labore qui. + Dolores recusandae omnis aut. Repudiandae quia neque porro in + blanditiis. A atque minima fugit. Totam quidem voluptas natus velit + at. +
+ + ), + }, +]; diff --git a/src/components/templates/sectioned/sectioned-layout.stories.tsx b/src/components/templates/sectioned/sectioned-layout.stories.tsx index 7c97400..0336b7a 100644 --- a/src/components/templates/sectioned/sectioned-layout.stories.tsx +++ b/src/components/templates/sectioned/sectioned-layout.stories.tsx @@ -1,6 +1,7 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { LayoutBase } from '../layout/layout.stories'; import { SectionedLayout as SectionedLayoutComponent } from './sectioned-layout'; +import { sections } from './sectioned-layout.fixtures'; /** * SectionedLayout - Storybook Meta @@ -48,29 +49,6 @@ const Template: ComponentStory = (args) => ( ); -const sections = [ - { - title: 'Section 1', - content: - 'Qui suscipit ea et aut dicta. Quia ut dignissimos. Sapiente beatae voluptatem quis et. Nemo vitae magni. Nihil iste officia est sed esse molestiae doloribus. Quia temporibus nobis ea fuga quis incidunt doloribus eaque.', - }, - { - title: 'Section 2', - content: - 'Reprehenderit aut magnam ut quos. Voluptatibus beatae et. Earum non atque voluptatum illum rem distinctio repellat.', - }, - { - title: 'Section 3', - content: - 'Placeat rem dolores dolore illum earum officia dolore. Ut est ducimus. Officia eveniet pariatur ut laboriosam voluptatibus aut doloremque natus quis.', - }, - { - title: 'Section 4', - content: - 'Vitae facere ipsa eum sunt debitis veritatis dolorem labore qui. Dolores recusandae omnis aut. Repudiandae quia neque porro in blanditiis. A atque minima fugit. Totam quidem voluptas natus velit at.', - }, -]; - /** * Sectioned Layout Stories - Default */ diff --git a/src/components/templates/sectioned/sectioned-layout.test.tsx b/src/components/templates/sectioned/sectioned-layout.test.tsx index 2370337..372b0fb 100644 --- a/src/components/templates/sectioned/sectioned-layout.test.tsx +++ b/src/components/templates/sectioned/sectioned-layout.test.tsx @@ -1,31 +1,10 @@ import { describe, expect, it } from '@jest/globals'; -import { BreadcrumbList } from 'schema-dts'; -import { render, screen } from '../../../../tests/utils'; +import type { BreadcrumbList } from 'schema-dts'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { SectionedLayout } from './sectioned-layout'; +import { sections } from './sectioned-layout.fixtures'; const breadcrumbSchema: BreadcrumbList['itemListElement'][] = []; -const sections = [ - { - title: 'Section 1', - content: - 'Qui suscipit ea et aut dicta. Quia ut dignissimos. Sapiente beatae voluptatem quis et. Nemo vitae magni. Nihil iste officia est sed esse molestiae doloribus. Quia temporibus nobis ea fuga quis incidunt doloribus eaque.', - }, - { - title: 'Section 2', - content: - 'Reprehenderit aut magnam ut quos. Voluptatibus beatae et. Earum non atque voluptatum illum rem distinctio repellat.', - }, - { - title: 'Section 3', - content: - 'Placeat rem dolores dolore illum earum officia dolore. Ut est ducimus. Officia eveniet pariatur ut laboriosam voluptatibus aut doloremque natus quis.', - }, - { - title: 'Section 4', - content: - 'Vitae facere ipsa eum sunt debitis veritatis dolorem labore qui. Dolores recusandae omnis aut. Repudiandae quia neque porro in blanditiis. A atque minima fugit. Totam quidem voluptas natus velit at.', - }, -]; describe('SectionedLayout', () => { it('renders the correct number of section', () => { @@ -35,8 +14,8 @@ describe('SectionedLayout', () => { sections={sections} /> ); - expect(screen.getAllByRole('heading', { name: /^Section/ })).toHaveLength( - sections.length - ); + expect( + rtlScreen.getAllByRole('heading', { name: /^Section/ }) + ).toHaveLength(sections.length); }); }); diff --git a/src/components/templates/sectioned/sectioned-layout.tsx b/src/components/templates/sectioned/sectioned-layout.tsx index a307688..6d58e83 100644 --- a/src/components/templates/sectioned/sectioned-layout.tsx +++ b/src/components/templates/sectioned/sectioned-layout.tsx @@ -1,9 +1,9 @@ import Script from 'next/script'; -import { FC } from 'react'; -import { BreadcrumbList } from 'schema-dts'; +import type { FC } from 'react'; +import type { BreadcrumbList } from 'schema-dts'; import { Section, type SectionProps, type SectionVariant } from '../../atoms'; -export type PageSection = Pick; +export type PageSection = Required>; export type SectionedLayoutProps = { /** @@ -25,27 +25,23 @@ export const SectionedLayout: FC = ({ breadcrumbSchema, sections, }) => { - const getSections = (items: SectionProps[]) => { - return items.map((section, index) => { + const getSections = (items: PageSection[]) => + items.map((section, index) => { const variant: SectionVariant = index % 2 ? 'light' : 'dark'; const isLastSection = index === items.length - 1; return ( -
+
+ {section.children} +
); }); - }; return ( <>