diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-22 19:07:25 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-23 11:44:28 +0100 |
| commit | 4f1181581e177dd80a76165a0f930ef4577f9c6a (patch) | |
| tree | 6029f86d42af7700f5b59cd1477854190bab65c6 /src/components | |
| parent | 329e7c89bac50be9db2c6d2ec6751ab0ffad42ac (diff) | |
refactor(components): integrate sectioned page template into Page
* replace Section component by a generic one (other components should
be able to use it)
* add a PageSection component
* add `hasSections` prop to Page component
* remove sectioned-page template
Diffstat (limited to 'src/components')
18 files changed, 241 insertions, 340 deletions
diff --git a/src/components/atoms/layout/section/section.module.scss b/src/components/atoms/layout/section/section.module.scss deleted file mode 100644 index 3da74a2..0000000 --- a/src/components/atoms/layout/section/section.module.scss +++ /dev/null @@ -1,26 +0,0 @@ -@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; - margin-block: 0; - } -} diff --git a/src/components/atoms/layout/section/section.stories.tsx b/src/components/atoms/layout/section/section.stories.tsx index e21209b..fdc8217 100644 --- a/src/components/atoms/layout/section/section.stories.tsx +++ b/src/components/atoms/layout/section/section.stories.tsx @@ -1,85 +1,37 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { Heading } from '../../heading'; -import { Section } from './section'; +import { Section as SectionComponent } from './section'; /** * Section - Storybook Meta */ export default { title: 'Atoms/Layout/Section', - component: Section, - args: { - hasBorder: true, - variant: 'light', - }, + component: SectionComponent, argTypes: { children: { - description: 'The section content.', + description: 'The section contents.', 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<typeof Section>; +} as ComponentMeta<typeof SectionComponent>; -const Template: ComponentStory<typeof Section> = (args) => ( - <Section {...args} /> +const Template: ComponentStory<typeof SectionComponent> = (args) => ( + <SectionComponent {...args} /> ); /** - * Section Stories - Light - */ -export const Light = Template.bind({}); -Light.args = { - children: ( - <> - <Heading level={2}>A section title</Heading> - <div>The body</div> - </> - ), - variant: 'light', -}; - -/** - * Section Stories - Dark + * Section Story */ -export const Dark = Template.bind({}); -Dark.args = { +export const Section = Template.bind({}); +Section.args = { children: ( <> <Heading level={2}>A section title</Heading> <div>The body</div> </> ), - variant: 'dark', }; diff --git a/src/components/atoms/layout/section/section.test.tsx b/src/components/atoms/layout/section/section.test.tsx index 85305c0..be4c324 100644 --- a/src/components/atoms/layout/section/section.test.tsx +++ b/src/components/atoms/layout/section/section.test.tsx @@ -5,23 +5,8 @@ import { Section } from './section'; const content = 'Section content.'; describe('Section', () => { - it('renders its body', () => { + it('renders its children', () => { render(<Section>{content}</Section>); expect(rtlScreen.getByText(content)).toBeInTheDocument(); }); - - it('renders a section with border', () => { - render(<Section hasBorder>{content}</Section>); - expect(rtlScreen.getByText(content)).toHaveClass('wrapper--borders'); - }); - - it('renders a light section', () => { - render(<Section variant="light">{content}</Section>); - expect(rtlScreen.getByText(content)).toHaveClass('wrapper--light'); - }); - - it('renders a dark section', () => { - render(<Section variant="dark">{content}</Section>); - 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 index 63c658a..63a2499 100644 --- a/src/components/atoms/layout/section/section.tsx +++ b/src/components/atoms/layout/section/section.tsx @@ -2,45 +2,18 @@ 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<HTMLAttributes<HTMLElement>, '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; -}; +export type SectionProps = HTMLAttributes<HTMLElement>; const SectionWithRef: ForwardRefRenderFunction<HTMLElement, SectionProps> = ( - { children, className = '', hasBorder = false, variant = 'light', ...props }, + { children, ...props }, ref -) => { - const borderClass = hasBorder ? styles[`wrapper--borders`] : ''; - const variantClass = styles[`wrapper--${variant}`]; - const sectionClass = `${styles.wrapper} ${borderClass} ${variantClass} ${className}`; - - return ( - <section {...props} className={sectionClass} ref={ref}> - {children} - </section> - ); -}; +) => ( + <section {...props} ref={ref}> + {children} + </section> +); /** * Section component diff --git a/src/components/templates/index.ts b/src/components/templates/index.ts index ae34898..bd41ab2 100644 --- a/src/components/templates/index.ts +++ b/src/components/templates/index.ts @@ -1,3 +1,2 @@ export * from './layout'; export * from './page'; -export * from './sectioned'; diff --git a/src/components/templates/page/index.ts b/src/components/templates/page/index.ts index 3b26326..f6d2d48 100644 --- a/src/components/templates/page/index.ts +++ b/src/components/templates/page/index.ts @@ -3,4 +3,5 @@ export * from './page-body'; export * from './page-comments'; export * from './page-footer'; export * from './page-header'; +export * from './page-section'; export * from './page-sidebar'; diff --git a/src/components/templates/page/page-comments.tsx b/src/components/templates/page/page-comments.tsx index bc715e8..170d6b7 100644 --- a/src/components/templates/page/page-comments.tsx +++ b/src/components/templates/page/page-comments.tsx @@ -8,7 +8,7 @@ import { import { useIntl } from 'react-intl'; import { sendComment } from '../../../services/graphql'; import type { SendCommentInput } from '../../../types'; -import { Heading, Link } from '../../atoms'; +import { Heading, Link, Section } from '../../atoms'; import { Card, CardBody } from '../../molecules'; import { type CommentData, @@ -138,7 +138,7 @@ const PageCommentsWithRef: ForwardRefRenderFunction< return ( <div {...props} className={wrapperClass} ref={ref}> - <section className={styles.section}> + <Section className={styles.comments__body}> <Heading className={styles.heading} level={2}> {commentsListTitle} </Heading> @@ -154,10 +154,10 @@ const PageCommentsWithRef: ForwardRefRenderFunction< <CardBody>{noCommentsYet}</CardBody> </Card> )} - </section> + </Section> {areCommentsClosed ? null : ( - <section - className={styles.section} + <Section + className={styles.comments__body} // eslint-disable-next-line react/jsx-no-literals id="comment-form-section" > @@ -169,7 +169,7 @@ const PageCommentsWithRef: ForwardRefRenderFunction< className={styles.form} onSubmit={saveComment} /> - </section> + </Section> )} </div> ); diff --git a/src/components/templates/page/page-section.test.tsx b/src/components/templates/page/page-section.test.tsx new file mode 100644 index 0000000..b372ab7 --- /dev/null +++ b/src/components/templates/page/page-section.test.tsx @@ -0,0 +1,43 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { PageSection } from './page-section'; + +describe('PageSection', () => { + it('renders its children', () => { + const body = 'a voluptas iste'; + + render(<PageSection>{body}</PageSection>); + + expect(rtlScreen.getByText(body)).toBeInTheDocument(); + }); + + it('can use the light variant', () => { + const body = 'a voluptas iste'; + + render(<PageSection variant="light">{body}</PageSection>); + + expect(rtlScreen.getByText(body).parentElement).toHaveClass( + 'section--light' + ); + }); + + it('can use the dark variant', () => { + const body = 'a voluptas iste'; + + render(<PageSection variant="dark">{body}</PageSection>); + + expect(rtlScreen.getByText(body).parentElement).toHaveClass( + 'section--dark' + ); + }); + + it('can have a border at the bottom', () => { + const body = 'a voluptas iste'; + + render(<PageSection hasBorder>{body}</PageSection>); + + expect(rtlScreen.getByText(body).parentElement).toHaveClass( + 'section--bordered' + ); + }); +}); diff --git a/src/components/templates/page/page-section.tsx b/src/components/templates/page/page-section.tsx new file mode 100644 index 0000000..24bc1a1 --- /dev/null +++ b/src/components/templates/page/page-section.tsx @@ -0,0 +1,43 @@ +import { type ForwardRefRenderFunction, forwardRef } from 'react'; +import { Section, type SectionProps } from '../../atoms'; +import styles from './page.module.scss'; + +export type PageSectionVariant = 'dark' | 'light'; + +export type PageSectionProps = SectionProps & { + /** + * Add a border at the bottom of the section. + * + * @default false + */ + hasBorder?: boolean; + /** + * The section variant. + * + * @default 'light' + */ + variant?: PageSectionVariant; +}; + +const PageSectionWithRef: ForwardRefRenderFunction< + HTMLElement, + PageSectionProps +> = ( + { children, className = '', hasBorder = false, variant = 'light', ...props }, + ref +) => { + const sectionClass = [ + styles.section, + styles[hasBorder ? 'section--bordered' : ''], + styles[`section--${variant}`], + className, + ].join(' '); + + return ( + <Section {...props} className={sectionClass} ref={ref}> + <div className={styles.section__body}>{children}</div> + </Section> + ); +}; + +export const PageSection = forwardRef(PageSectionWithRef); diff --git a/src/components/templates/page/page.module.scss b/src/components/templates/page/page.module.scss index b521438..d2752a1 100644 --- a/src/components/templates/page/page.module.scss +++ b/src/components/templates/page/page.module.scss @@ -14,7 +14,8 @@ } .breadcrumbs, -.page { +.page--regular, +.section { --border-size: #{fun.convert-px(3)}; --col-gap: clamp(var(--spacing-md), 4vw, var(--spacing-2xl)); --left-col: 0; @@ -25,6 +26,10 @@ grid-auto-flow: column dense; align-items: baseline; +} + +.breadcrumbs, +.page--regular { margin-top: var(--spacing-sm); } @@ -107,6 +112,36 @@ } } +.section { + --right-col: minmax(0, 1fr); + --left-col: minmax(0, 1fr); + + @extend %grid; + + row-gap: var(--spacing-sm); + padding: var(--spacing-md) 0; + + &--bordered { + border-bottom: fun.convert-px(1) solid var(--color-border); + } + + &--dark { + background: var(--color-bg-secondary); + } + + &--light { + background: var(--color-bg); + } + + &__body { + grid-column: 2; + + > * + * { + margin-block: var(--spacing-sm); + } + } +} + :where(.footer) { .btn { margin-inline-end: var(--spacing-2xs); @@ -161,13 +196,13 @@ padding: 0 0 var(--spacing-lg); background: var(--color-bg-secondary); border-top: var(--border-size) solid var(--color-border-light); -} -:where(.comments) { - .section { + &__body { grid-column: 2; } +} +:where(.comments) { .heading { width: fit-content; margin: var(--spacing-md) auto; @@ -181,7 +216,7 @@ @container page (width > #{var.get-breakpoint("md")}) { .breadcrumbs, - .page { + .page--regular { --right-col: minmax(25ch, 1fr); } @@ -200,7 +235,7 @@ @container page (width > #{var.get-breakpoint("lg")}) { .breadcrumbs, - .page { + .page--regular { --left-col: minmax(25ch, 1fr); } diff --git a/src/components/templates/page/page.stories.tsx b/src/components/templates/page/page.stories.tsx index 6b1058e..8b1616b 100644 --- a/src/components/templates/page/page.stories.tsx +++ b/src/components/templates/page/page.stories.tsx @@ -7,6 +7,7 @@ import { PageBody } from './page-body'; import { PageComments } from './page-comments'; import { PageFooter } from './page-footer'; import { PageHeader } from './page-header'; +import { PageSection } from './page-section'; import { PageSidebar } from './page-sidebar'; /** @@ -454,3 +455,42 @@ HeaderBodyComments.args = { </> ), }; + +/** + * Page Stories - SectionedPage + */ +export const SectionedPage = Template.bind({}); +SectionedPage.args = { + children: ( + <> + <PageSection> + <Heading level={2}>A section title</Heading> + <p> + Illo temporibus nihil maiores nesciunt. Veritatis distinctio aperiam + culpa eveniet incidunt eos harum porro labore. Soluta culpa unde + adipisci fugiat voluptas eos. + </p> + </PageSection> + <PageSection variant="dark"> + <Heading level={2}>Another section title</Heading> + <p> + Sint consequatur animi eum beatae. Non corporis quos quia et magnam. + Cumque molestiae blanditiis aut. Et suscipit iusto laudantium iusto + dignissimos. + </p> + </PageSection> + <PageSection> + <Heading level={2}>A third section title</Heading> + <p> + Omnis corporis perferendis animi iste quidem placeat est minus. Enim + autem consequatur voluptatem provident qui culpa. Aliquid aliquam + consequatur non explicabo ut distinctio quis a non. Delectus unde odio + eveniet temporibus omnis. Reprehenderit consequatur minima in + consequatur saepe est sed. Accusantium quia quae magnam expedita nihil + rerum omnis temporibus perspiciatis. + </p> + </PageSection> + </> + ), + hasSections: true, +}; diff --git a/src/components/templates/page/page.test.tsx b/src/components/templates/page/page.test.tsx index 21c5a86..fb06cb1 100644 --- a/src/components/templates/page/page.test.tsx +++ b/src/components/templates/page/page.test.tsx @@ -3,6 +3,8 @@ import { render, screen as rtlScreen } from '../../../../tests/utils'; import type { BreadcrumbsItem } from '../../organisms'; import { Page } from './page'; import { PageBody } from './page-body'; +import { PageSection } from './page-section'; +import { Heading } from 'src/components/atoms'; describe('Page', () => { it('renders its children', () => { @@ -46,4 +48,33 @@ describe('Page', () => { expect(rtlScreen.getByText(body)).toHaveClass('page--body-last'); }); + + it('can render a sectioned page', () => { + const sections = [ + { + heading: 'excepturi ex dolorum', + contents: + 'Id eius voluptas rerum nemo ullam omnis provident deserunt. Expedita sit ut consequatur deleniti. Maiores nam. Necessitatibus pariatur et qui dolor quia labore.', + }, + { + heading: 'rerum corporis et', + contents: + 'Vel maxime doloremque quo laborum debitis. Ab perferendis animi dolores et ut voluptatem. Tempore aut doloremque sunt enim aut sint. Quae iure saepe consectetur. Ex animi ut. Nobis aliquid iste accusantium nesciunt ab voluptas illum.', + }, + ]; + + render( + <Page hasSections> + {sections.map((section) => ( + <PageSection aria-label={section.heading} key={section.heading}> + <Heading level={2}>{section.heading}</Heading> + <p>{section.contents}</p> + </PageSection> + ))} + </Page> + ); + + expect(rtlScreen.getAllByRole('region')).toHaveLength(sections.length); + expect(rtlScreen.getByRole('article')).toHaveClass('page--full'); + }); }); diff --git a/src/components/templates/page/page.tsx b/src/components/templates/page/page.tsx index f5f3ea5..b40c2f9 100644 --- a/src/components/templates/page/page.tsx +++ b/src/components/templates/page/page.tsx @@ -14,6 +14,12 @@ export type PageProps = HTMLAttributes<HTMLDivElement> & { */ breadcrumbs?: BreadcrumbsItem[]; /** + * Is it a regular page or a sectioned one? + * + * @default false + */ + hasSections?: boolean; + /** * Add an extra padding to the body when there are no footer/comments. * * Note: this should be refactored when `:has()` pseudo-class will have a @@ -25,13 +31,22 @@ export type PageProps = HTMLAttributes<HTMLDivElement> & { }; const PageWithRef: ForwardRefRenderFunction<HTMLDivElement, PageProps> = ( - { breadcrumbs, children, className = '', isBodyLastChild = false, ...props }, + { + breadcrumbs, + children, + className = '', + hasSections = false, + isBodyLastChild = false, + ...props + }, ref ) => { const wrapperClass = `${styles.wrapper} ${className}`; - const pageClass = `${styles.page} ${ - styles[isBodyLastChild ? 'page--body-last' : ''] - }`; + const pageClass = [ + styles.page, + styles[hasSections ? 'page--full' : 'page--regular'], + styles[isBodyLastChild ? 'page--body-last' : ''], + ].join(' '); const intl = useIntl(); const breadcrumbsLabel = intl.formatMessage({ defaultMessage: 'Breadcrumbs', diff --git a/src/components/templates/sectioned/index.ts b/src/components/templates/sectioned/index.ts deleted file mode 100644 index a8c6045..0000000 --- a/src/components/templates/sectioned/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './sectioned-layout'; diff --git a/src/components/templates/sectioned/sectioned-layout.fixtures.tsx b/src/components/templates/sectioned/sectioned-layout.fixtures.tsx deleted file mode 100644 index 0da8e7d..0000000 --- a/src/components/templates/sectioned/sectioned-layout.fixtures.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* eslint-disable react/jsx-no-literals */ -import { Heading } from '../../atoms'; -import type { PageSection } from './sectioned-layout'; - -export const sections: PageSection[] = [ - { - id: 'section-1', - children: ( - <> - <Heading level={2}>Section 1</Heading> - <div> - 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. - </div> - </> - ), - }, - { - id: 'section-2', - children: ( - <> - <Heading level={2}>Section 2</Heading> - <div> - Reprehenderit aut magnam ut quos. Voluptatibus beatae et. Earum non - atque voluptatum illum rem distinctio repellat. - </div> - </> - ), - }, - { - id: 'section-3', - children: ( - <> - <Heading level={2}>Section 3</Heading> - <div> - Placeat rem dolores dolore illum earum officia dolore. Ut est ducimus. - Officia eveniet pariatur ut laboriosam voluptatibus aut doloremque - natus quis. - </div> - </> - ), - }, - { - id: 'section-4', - children: ( - <> - <Heading level={2}>Section 4</Heading> - <div> - 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. - </div> - </> - ), - }, -]; diff --git a/src/components/templates/sectioned/sectioned-layout.stories.tsx b/src/components/templates/sectioned/sectioned-layout.stories.tsx deleted file mode 100644 index 0336b7a..0000000 --- a/src/components/templates/sectioned/sectioned-layout.stories.tsx +++ /dev/null @@ -1,58 +0,0 @@ -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 - */ -export default { - title: 'Templates/Sectioned', - component: SectionedLayoutComponent, - args: { - breadcrumbSchema: [], - }, - argTypes: { - breadcrumbSchema: { - control: { - type: null, - }, - description: 'The JSON schema for breadcrumb items.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - sections: { - description: 'The different sections.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - }, - decorators: [ - (Story) => ( - <LayoutBase {...LayoutBase.args}> - <Story /> - </LayoutBase> - ), - ], - parameters: { - layout: 'fullscreen', - }, -} as ComponentMeta<typeof SectionedLayoutComponent>; - -const Template: ComponentStory<typeof SectionedLayoutComponent> = (args) => ( - <SectionedLayoutComponent {...args} /> -); - -/** - * Sectioned Layout Stories - Default - */ -export const Sectioned = Template.bind({}); -Sectioned.args = { - sections, -}; diff --git a/src/components/templates/sectioned/sectioned-layout.test.tsx b/src/components/templates/sectioned/sectioned-layout.test.tsx deleted file mode 100644 index 372b0fb..0000000 --- a/src/components/templates/sectioned/sectioned-layout.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -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'][] = []; - -describe('SectionedLayout', () => { - it('renders the correct number of section', () => { - render( - <SectionedLayout - breadcrumbSchema={breadcrumbSchema} - sections={sections} - /> - ); - 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 deleted file mode 100644 index 6d58e83..0000000 --- a/src/components/templates/sectioned/sectioned-layout.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import Script from 'next/script'; -import type { FC } from 'react'; -import type { BreadcrumbList } from 'schema-dts'; -import { Section, type SectionProps, type SectionVariant } from '../../atoms'; - -export type PageSection = Required<Pick<SectionProps, 'children' | 'id'>>; - -export type SectionedLayoutProps = { - /** - * The breadcrumb JSON schema. - */ - breadcrumbSchema: BreadcrumbList['itemListElement'][]; - /** - * An array of objects describing each section. - */ - sections: PageSection[]; -}; - -/** - * SectionedLayout component - * - * Render a sectioned layout. - */ -export const SectionedLayout: FC<SectionedLayoutProps> = ({ - breadcrumbSchema, - sections, -}) => { - const getSections = (items: PageSection[]) => - items.map((section, index) => { - const variant: SectionVariant = index % 2 ? 'light' : 'dark'; - const isLastSection = index === items.length - 1; - - return ( - <Section hasBorder={!isLastSection} key={section.id} variant={variant}> - {section.children} - </Section> - ); - }); - - return ( - <> - <Script - dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} - // eslint-disable-next-line react/jsx-no-literals -- Id allowed. - id="schema-breadcrumb" - type="application/ld+json" - /> - {getSections(sections)} - </> - ); -}; |
