diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-17 22:48:41 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-05-17 22:48:41 +0200 |
| commit | 271ef6debaca7ed9a01829dcef3a37e90a2dff05 (patch) | |
| tree | aa1513b40e7020a44cfcaaedc3a33d39ecfb8af7 /src/components | |
| parent | 4e53a8654441481029746ff4e35a4a19c8d83709 (diff) | |
chore: use persistent layout
It prevents to rerender the common components between pages (header,
footer...).
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/templates/layout/layout.module.scss | 14 | ||||
| -rw-r--r-- | src/components/templates/layout/layout.test.tsx | 11 | ||||
| -rw-r--r-- | src/components/templates/layout/layout.tsx | 54 | ||||
| -rw-r--r-- | src/components/templates/page/page-layout.module.scss | 11 | ||||
| -rw-r--r-- | src/components/templates/page/page-layout.stories.tsx | 24 | ||||
| -rw-r--r-- | src/components/templates/page/page-layout.tsx | 35 | ||||
| -rw-r--r-- | src/components/templates/sectioned/sectioned-layout.stories.tsx | 8 | ||||
| -rw-r--r-- | src/components/templates/sectioned/sectioned-layout.tsx | 25 |
8 files changed, 105 insertions, 77 deletions
diff --git a/src/components/templates/layout/layout.module.scss b/src/components/templates/layout/layout.module.scss index 09904dc..1080732 100644 --- a/src/components/templates/layout/layout.module.scss +++ b/src/components/templates/layout/layout.module.scss @@ -1,5 +1,6 @@ @use "@styles/abstracts/functions" as fun; @use "@styles/abstracts/mixins" as mix; +@use "@styles/abstracts/placeholders"; .header { border-bottom: fun.convert-px(3) solid var(--color-border-light); @@ -9,6 +10,19 @@ flex: 1; } +.article { + &--grid { + @extend %grid; + + grid-auto-flow: column dense; + align-items: baseline; + } + + &--padding { + padding-bottom: var(--spacing-lg); + } +} + .footer { border-top: fun.convert-px(3) solid var(--color-border-light); } diff --git a/src/components/templates/layout/layout.test.tsx b/src/components/templates/layout/layout.test.tsx index 94145ec..78547d4 100644 --- a/src/components/templates/layout/layout.test.tsx +++ b/src/components/templates/layout/layout.test.tsx @@ -4,33 +4,32 @@ import Layout from './layout'; const body = 'Sit dolorem eveniet. Sit sit odio nemo vitae corrupti modi sint est rerum. Pariatur quidem maiores distinctio. Quia et illum aspernatur est cum.'; -const breadcrumbSchema: BreadcrumbList['itemListElement'][] = []; describe('Layout', () => { it('renders the website header', () => { - render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>); + render(<Layout>{body}</Layout>); expect(screen.getByRole('banner')).toBeInTheDocument(); }); it('renders the website main content', () => { - render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>); + render(<Layout>{body}</Layout>); expect(screen.getByRole('main')).toBeInTheDocument(); }); it('renders the website footer', () => { - render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>); + render(<Layout>{body}</Layout>); expect(screen.getByRole('contentinfo')).toBeInTheDocument(); }); it('renders a skip to content link', () => { - render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>); + render(<Layout>{body}</Layout>); expect( screen.getByRole('link', { name: 'Skip to content' }) ).toBeInTheDocument(); }); it('renders an article', () => { - render(<Layout breadcrumbSchema={breadcrumbSchema}>{body}</Layout>); + render(<Layout>{body}</Layout>); expect(screen.getByRole('article')).toHaveTextContent(body); }); }); diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx index 3eb02ee..559eaed 100644 --- a/src/components/templates/layout/layout.tsx +++ b/src/components/templates/layout/layout.tsx @@ -9,18 +9,13 @@ import Main from '@components/atoms/layout/main'; import NoScript from '@components/atoms/layout/no-script'; import Footer, { type FooterProps } from '@components/organisms/layout/footer'; import Header, { type HeaderProps } from '@components/organisms/layout/header'; +import { type NextPageWithLayoutOptions } from '@ts/types/app'; import useScrollPosition from '@utils/hooks/use-scroll-position'; import useSettings from '@utils/hooks/use-settings'; import Script from 'next/script'; -import { FC, ReactNode, useState } from 'react'; +import { FC, ReactElement, ReactNode, useState } from 'react'; import { useIntl } from 'react-intl'; -import { - BreadcrumbList, - Person, - SearchAction, - WebSite, - WithContext, -} from 'schema-dts'; +import { Person, SearchAction, WebSite, WithContext } from 'schema-dts'; import styles from './layout.module.scss'; export type QueryAction = SearchAction & { @@ -29,17 +24,17 @@ export type QueryAction = SearchAction & { export type LayoutProps = Pick<HeaderProps, 'isHome'> & { /** - * The breadcrumb JSON schema. - */ - breadcrumbSchema: BreadcrumbList['itemListElement'][]; - /** * The layout main content. */ children: ReactNode; /** - * Set additional classnames to the article element. + * Determine if article has a comments section. + */ + withExtraPadding?: boolean; + /** + * Determine if article should use grid. Default: false. */ - className?: string; + useGrid?: boolean; }; /** @@ -48,14 +43,16 @@ export type LayoutProps = Pick<HeaderProps, 'isHome'> & { * Render the base layout used by all pages. */ const Layout: FC<LayoutProps> = ({ - breadcrumbSchema, children, + withExtraPadding = false, isHome, - ...props + useGrid = false, }) => { const intl = useIntl(); const { website } = useSettings(); const { baseline, copyright, locales, name, picture, url } = website; + const articleGridClass = useGrid ? 'article--grid' : ''; + const articleCommentsClass = withExtraPadding ? 'article--padding' : ''; const skipToContent = intl.formatMessage({ defaultMessage: 'Skip to content', @@ -188,11 +185,6 @@ const Layout: FC<LayoutProps> = ({ type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(brandingSchema) }} /> - <Script - id="schema-breadcrumb" - type="application/ld+json" - dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} - /> <noscript> <div className={styles['noscript-spacing']}></div> </noscript> @@ -211,7 +203,11 @@ const Layout: FC<LayoutProps> = ({ className={styles.header} /> <Main id="main" className={styles.main}> - <article {...props}>{children}</article> + <article + className={`${styles[articleGridClass]} ${styles[articleCommentsClass]}`} + > + {children} + </article> </Main> <Footer copyright={copyrightData} @@ -227,4 +223,18 @@ const Layout: FC<LayoutProps> = ({ ); }; +/** + * Get the global layout. + * + * @param {ReactElement} page - A page. + * @param {boolean} [isHome] - Determine if it is the homepage. + * @returns A page wrapped with the global layout. + */ +export const getLayout = ( + page: ReactElement, + props: NextPageWithLayoutOptions +) => { + return <Layout {...props}>{page}</Layout>; +}; + export default Layout; diff --git a/src/components/templates/page/page-layout.module.scss b/src/components/templates/page/page-layout.module.scss index c6b4e8d..c7674ae 100644 --- a/src/components/templates/page/page-layout.module.scss +++ b/src/components/templates/page/page-layout.module.scss @@ -2,17 +2,6 @@ @use "@styles/abstracts/mixins" as mix; @use "@styles/abstracts/placeholders"; -.article { - @extend %grid; - - grid-auto-flow: column dense; - align-items: baseline; - - &--no-comments { - padding-bottom: var(--spacing-lg); - } -} - .breadcrumb { @extend %grid; diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx index 8af5f98..bec1066 100644 --- a/src/components/templates/page/page-layout.stories.tsx +++ b/src/components/templates/page/page-layout.stories.tsx @@ -1,11 +1,11 @@ import ButtonLink from '@components/atoms/buttons/button-link'; import Heading from '@components/atoms/headings/heading'; import Link from '@components/atoms/links/link'; -import ProgressBar from '@components/atoms/loaders/progress-bar'; import PostsList from '@components/organisms/layout/posts-list'; import LinksListWidget from '@components/organisms/widgets/links-list-widget'; import Sharing from '@components/organisms/widgets/sharing'; import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { LayoutBase } from '../layout/layout.stories'; import PageLayoutComponent from './page-layout'; /** @@ -118,20 +118,6 @@ export default { required: false, }, }, - isHome: { - control: { - type: 'boolean', - }, - description: 'Determine if the current page is the homepage.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, title: { control: { type: 'text', @@ -168,6 +154,13 @@ export default { }, }, }, + decorators: [ + (Story) => ( + <LayoutBase {...LayoutBase.args}> + <Story /> + </LayoutBase> + ), + ], parameters: { layout: 'fullscreen', }, @@ -477,7 +470,6 @@ Blog.args = { children: ( <> <PostsList posts={posts} byYear={true} total={posts.length} /> - <ProgressBar min={1} max={1} current={1} info="1/1 page loaded." /> </> ), widgets: [ diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx index 2ff2084..d171944 100644 --- a/src/components/templates/page/page-layout.tsx +++ b/src/components/templates/page/page-layout.tsx @@ -21,19 +21,20 @@ import TableOfContents from '@components/organisms/widgets/table-of-contents'; import { type SendCommentVars } from '@services/graphql/api'; import { sendComment } from '@services/graphql/comments'; import useIsMounted from '@utils/hooks/use-is-mounted'; +import Script from 'next/script'; import { FC, HTMLAttributes, ReactNode, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; -import Layout, { type LayoutProps } from '../layout/layout'; +import { BreadcrumbList } from 'schema-dts'; import styles from './page-layout.module.scss'; -export type PageLayoutProps = Pick< - LayoutProps, - 'breadcrumbSchema' | 'isHome' -> & { +export type PageLayoutProps = { /** * True if the page accepts new comments. Default: false. */ allowComments?: boolean; + /** + * Set attributes to the page body. + */ bodyAttributes?: HTMLAttributes<HTMLDivElement>; /** * Set additional classnames to the body wrapper. @@ -44,6 +45,10 @@ export type PageLayoutProps = Pick< */ breadcrumb: BreadcrumbItem[]; /** + * The breadcrumb JSON schema. + */ + breadcrumbSchema: BreadcrumbList['itemListElement'][]; + /** * The main content of the page. */ children: ReactNode; @@ -98,7 +103,6 @@ const PageLayout: FC<PageLayoutProps> = ({ headerMeta, id, intro, - isHome = false, title, widgets, withToC = false, @@ -117,13 +121,7 @@ const PageLayout: FC<PageLayoutProps> = ({ const bodyRef = useRef<HTMLDivElement>(null); const isMounted = useIsMounted(bodyRef); - const hasComments = Array.isArray(comments) && comments.length > 0; - const articleModifier = - hasComments || allowComments - ? 'article--has-comments' - : 'article--no-comments'; - const [status, setStatus] = useState<NoticeKind>('info'); const [statusMessage, setStatusMessage] = useState<string>(''); const isReplyRef = useRef<boolean>(false); @@ -186,11 +184,12 @@ const PageLayout: FC<PageLayoutProps> = ({ }; return ( - <Layout - breadcrumbSchema={breadcrumbSchema} - isHome={isHome} - className={`${styles.article} ${styles[articleModifier]}`} - > + <> + <Script + id="schema-breadcrumb" + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} + /> <Breadcrumb items={breadcrumb} className={styles.breadcrumb} @@ -291,7 +290,7 @@ const PageLayout: FC<PageLayoutProps> = ({ </section> </div> )} - </Layout> + </> ); }; diff --git a/src/components/templates/sectioned/sectioned-layout.stories.tsx b/src/components/templates/sectioned/sectioned-layout.stories.tsx index ce31a83..689f9a7 100644 --- a/src/components/templates/sectioned/sectioned-layout.stories.tsx +++ b/src/components/templates/sectioned/sectioned-layout.stories.tsx @@ -1,4 +1,5 @@ import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { LayoutBase } from '../layout/layout.stories'; import SectionedLayoutComponent from './sectioned-layout'; /** @@ -31,6 +32,13 @@ export default { }, }, }, + decorators: [ + (Story) => ( + <LayoutBase {...LayoutBase.args}> + <Story /> + </LayoutBase> + ), + ], parameters: { layout: 'fullscreen', }, diff --git a/src/components/templates/sectioned/sectioned-layout.tsx b/src/components/templates/sectioned/sectioned-layout.tsx index 58d5ad0..f91c354 100644 --- a/src/components/templates/sectioned/sectioned-layout.tsx +++ b/src/components/templates/sectioned/sectioned-layout.tsx @@ -2,12 +2,17 @@ import Section, { type SectionProps, type SectionVariant, } from '@components/atoms/layout/section'; +import Script from 'next/script'; import { FC } from 'react'; -import Layout, { type LayoutProps } from '../layout/layout'; +import { BreadcrumbList } from 'schema-dts'; export type Section = Pick<SectionProps, 'content' | 'title'>; -export type SectionedLayoutProps = Pick<LayoutProps, 'breadcrumbSchema'> & { +export type SectionedLayoutProps = { + /** + * The breadcrumb JSON schema. + */ + breadcrumbSchema: BreadcrumbList['itemListElement'][]; /** * An array of objects describing each section. */ @@ -19,7 +24,10 @@ export type SectionedLayoutProps = Pick<LayoutProps, 'breadcrumbSchema'> & { * * Render a sectioned layout. */ -const SectionedLayout: FC<SectionedLayoutProps> = ({ sections, ...props }) => { +const SectionedLayout: FC<SectionedLayoutProps> = ({ + breadcrumbSchema, + sections, +}) => { const getSections = (items: SectionProps[]) => { return items.map((section, index) => { const variant: SectionVariant = index % 2 ? 'light' : 'dark'; @@ -37,7 +45,16 @@ const SectionedLayout: FC<SectionedLayoutProps> = ({ sections, ...props }) => { }); }; - return <Layout {...props}>{getSections(sections)}</Layout>; + return ( + <> + <Script + id="schema-breadcrumb" + type="application/ld+json" + dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }} + /> + {getSections(sections)} + </> + ); }; export default SectionedLayout; |
