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/templates | |
| 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/templates')
| -rw-r--r-- | src/components/templates/index.ts | 1 | ||||
| -rw-r--r-- | src/components/templates/page/index.ts | 1 | ||||
| -rw-r--r-- | src/components/templates/page/page-comments.tsx | 12 | ||||
| -rw-r--r-- | src/components/templates/page/page-section.test.tsx | 43 | ||||
| -rw-r--r-- | src/components/templates/page/page-section.tsx | 43 | ||||
| -rw-r--r-- | src/components/templates/page/page.module.scss | 47 | ||||
| -rw-r--r-- | src/components/templates/page/page.stories.tsx | 40 | ||||
| -rw-r--r-- | src/components/templates/page/page.test.tsx | 31 | ||||
| -rw-r--r-- | src/components/templates/page/page.tsx | 23 | ||||
| -rw-r--r-- | src/components/templates/sectioned/index.ts | 1 | ||||
| -rw-r--r-- | src/components/templates/sectioned/sectioned-layout.fixtures.tsx | 59 | ||||
| -rw-r--r-- | src/components/templates/sectioned/sectioned-layout.stories.tsx | 58 | ||||
| -rw-r--r-- | src/components/templates/sectioned/sectioned-layout.test.tsx | 21 | ||||
| -rw-r--r-- | src/components/templates/sectioned/sectioned-layout.tsx | 51 |
14 files changed, 224 insertions, 207 deletions
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)} - </> - ); -}; |
