diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-09 16:31:00 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | 891441a76173c708c6604fa203b175aefa222333 (patch) | |
| tree | 27295311bb01a4e44dcc4f68422975cd705a24b8 /src/components/molecules | |
| parent | f11a906420975e833f278a08470d8f9783c76f73 (diff) | |
refactor(components): rewrite Branding component
The component should only be responsible of the layout for the logo,
the name and the optional baseline. Also, the homepage url could
be different from `/` so the consumer should give the right url.
Diffstat (limited to 'src/components/molecules')
| -rw-r--r-- | src/components/molecules/branding/branding.module.scss | 80 | ||||
| -rw-r--r-- | src/components/molecules/branding/branding.stories.tsx | 92 | ||||
| -rw-r--r-- | src/components/molecules/branding/branding.test.tsx | 77 | ||||
| -rw-r--r-- | src/components/molecules/branding/branding.tsx | 57 | ||||
| -rw-r--r-- | src/components/molecules/branding/index.ts | 1 | ||||
| -rw-r--r-- | src/components/molecules/index.ts | 1 | ||||
| -rw-r--r-- | src/components/molecules/layout/branding.module.scss | 110 | ||||
| -rw-r--r-- | src/components/molecules/layout/branding.stories.tsx | 115 | ||||
| -rw-r--r-- | src/components/molecules/layout/branding.test.tsx | 109 | ||||
| -rw-r--r-- | src/components/molecules/layout/branding.tsx | 86 | ||||
| -rw-r--r-- | src/components/molecules/layout/index.ts | 1 |
11 files changed, 308 insertions, 421 deletions
diff --git a/src/components/molecules/branding/branding.module.scss b/src/components/molecules/branding/branding.module.scss new file mode 100644 index 0000000..2f35fd7 --- /dev/null +++ b/src/components/molecules/branding/branding.module.scss @@ -0,0 +1,80 @@ +@use "../../../styles/abstracts/functions" as fun; +@use "../../../styles/abstracts/mixins" as mix; + +.wrapper { + display: grid; + grid-template-columns: minmax(0, 1fr); + justify-items: center; + width: 100%; + text-align: center; + + @include mix.media("screen") { + @include mix.dimensions("2xs") { + grid-template-columns: + auto + minmax(0, 1fr); + align-items: center; + justify-items: left; + column-gap: var(--spacing-sm); + width: unset; + } + } + + > *:first-child { + max-width: fun.convert-px(200); + max-height: fun.convert-px(200); + margin-bottom: var(--spacing-2xs); + + @include mix.media("screen") { + @include mix.dimensions("2xs") { + margin-bottom: 0; + } + } + } + + > *:nth-child(2) { + margin-block: var(--spacing-2xs); + } + + > *:nth-child(3) { + margin-block: 0 var(--spacing-xs); + } + + > *:first-child, + > *:first-child:nth-last-child(2) + * { + grid-row: span 2; + } + + > *:first-child:nth-last-child(3) + * { + align-self: self-end; + } + + > *:first-child:nth-last-child(3) ~ *:last-child { + align-self: self-start; + } +} + +.link { + background: linear-gradient( + to top, + var(--color-primary-light) fun.convert-px(5), + transparent fun.convert-px(5) + ) + left / 0 100% no-repeat; + text-decoration: none; + transition: all 0.6s ease-out 0s; + + &:hover, + &:focus { + background-size: 100% 100%; + } + + &:focus { + color: var(--color-primary-light); + } + + &:active { + background-size: 0 100%; + color: var(--color-primary-dark); + } +} diff --git a/src/components/molecules/branding/branding.stories.tsx b/src/components/molecules/branding/branding.stories.tsx new file mode 100644 index 0000000..c2f216a --- /dev/null +++ b/src/components/molecules/branding/branding.stories.tsx @@ -0,0 +1,92 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import NextImage from 'next/image'; +import { Heading } from '../../atoms'; +import { Branding } from './branding'; + +/** + * Branding - Storybook Meta + */ +export default { + title: 'Molecules/Branding', + component: Branding, + args: {}, + argTypes: { + baseline: { + control: { + type: 'object', + }, + description: 'The brand baseline.', + type: { + name: 'function', + required: false, + }, + }, + logo: { + control: { + type: 'object', + }, + description: 'The brand logo.', + type: { + name: 'function', + required: true, + }, + }, + name: { + control: { + type: 'object', + }, + description: 'The brand name.', + type: { + name: 'function', + required: true, + }, + }, + url: { + control: { + type: 'string', + }, + description: 'The homepage url.', + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof Branding>; + +const Template: ComponentStory<typeof Branding> = (args) => ( + <Branding {...args} /> +); + +/** + * Branding Stories - Logo and title + */ +export const LogoAndTitle = Template.bind({}); +LogoAndTitle.args = { + logo: ( + <NextImage + alt="Your brand logo" + height={150} + src="https://picsum.photos/150" + width={150} + /> + ), + name: <Heading level={1}>Your brand name</Heading>, +}; + +/** + * Branding Stories - Logo, title and baseline + */ +export const LogoTitleAndBaseline = Template.bind({}); +LogoTitleAndBaseline.args = { + baseline: <div>Your brand baseline if any</div>, + logo: ( + <NextImage + alt="Your brand logo" + height={150} + src="https://picsum.photos/150" + width={150} + /> + ), + name: <Heading level={1}>Your brand name</Heading>, +}; diff --git a/src/components/molecules/branding/branding.test.tsx b/src/components/molecules/branding/branding.test.tsx new file mode 100644 index 0000000..7f41098 --- /dev/null +++ b/src/components/molecules/branding/branding.test.tsx @@ -0,0 +1,77 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import NextImage from 'next/image'; +import { Branding } from './branding'; + +describe('Branding', () => { + it('renders the brand logo and name', () => { + const altText = 'dolorem aut ullam'; + const name = 'ducimus quo enim'; + + render( + <Branding + logo={ + <NextImage + alt={altText} + height={100} + src="https://picsum.photos/100" + width={100} + /> + } + name={<div>{name}</div>} + /> + ); + + expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument(); + expect(rtlScreen.getByText(name)).toBeInTheDocument(); + }); + + it('can render the brand logo, name and baseline', () => { + const altText = 'dolorem aut ullam'; + const name = 'ducimus quo enim'; + const baseline = 'ab consequatur est'; + + render( + <Branding + baseline={<div>{baseline}</div>} + logo={ + <NextImage + alt={altText} + height={100} + src="https://picsum.photos/100" + width={100} + /> + } + name={<div>{name}</div>} + /> + ); + + expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument(); + expect(rtlScreen.getByText(name)).toBeInTheDocument(); + expect(rtlScreen.getByText(baseline)).toBeInTheDocument(); + }); + + it('can render the brand name wrapped in a link', () => { + const altText = 'dolorem aut ullam'; + const name = 'ducimus quo enim'; + const url = '/velit'; + + render( + <Branding + logo={ + <NextImage + alt={altText} + height={100} + src="https://picsum.photos/100" + width={100} + /> + } + name={<div>{name}</div>} + url={url} + /> + ); + + expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument(); + expect(rtlScreen.getByRole('link', { name })).toHaveAttribute('href', url); + }); +}); diff --git a/src/components/molecules/branding/branding.tsx b/src/components/molecules/branding/branding.tsx new file mode 100644 index 0000000..bb88a04 --- /dev/null +++ b/src/components/molecules/branding/branding.tsx @@ -0,0 +1,57 @@ +import { + type HTMLAttributes, + type ForwardRefRenderFunction, + forwardRef, + type ReactElement, +} from 'react'; +import styles from './branding.module.scss'; +import { Link } from 'src/components/atoms'; + +export type BrandingProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & { + /** + * The brand baseline. + */ + baseline?: ReactElement | null; + /** + * The brand logo. + * + * The logo size should not exceed ~200px. + */ + logo: ReactElement; + /** + * The brand name. + */ + name: ReactElement; + /** + * The homepage url if you want to wrap the name with a link. + */ + url?: string; +}; + +const BrandingWithRef: ForwardRefRenderFunction< + HTMLDivElement, + BrandingProps +> = ({ className = '', baseline, logo, name, url, ...props }, ref) => { + const wrapperClass = `${styles.wrapper} ${className}`; + + return ( + <div {...props} className={wrapperClass} ref={ref}> + {logo} + {url ? ( + <Link className={styles.link} href={url}> + {name} + </Link> + ) : ( + name + )} + {baseline} + </div> + ); +}; + +/** + * Branding component + * + * Render the branding logo, title and optional baseline. + */ +export const Branding = forwardRef(BrandingWithRef); diff --git a/src/components/molecules/branding/index.ts b/src/components/molecules/branding/index.ts new file mode 100644 index 0000000..5cf12ed --- /dev/null +++ b/src/components/molecules/branding/index.ts @@ -0,0 +1 @@ +export * from './branding'; diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index a62f3bf..70ac3c9 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -1,3 +1,4 @@ +export * from './branding'; export * from './buttons'; export * from './collapsible'; export * from './forms'; diff --git a/src/components/molecules/layout/branding.module.scss b/src/components/molecules/layout/branding.module.scss deleted file mode 100644 index 6f67c8b..0000000 --- a/src/components/molecules/layout/branding.module.scss +++ /dev/null @@ -1,110 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; -@use "../../../styles/abstracts/mixins" as mix; - -@mixin typing-animation { - --typing-animation: none; - - width: fit-content; - position: relative; - overflow: hidden; - - &::after { - content: "|"; - display: block; - width: 100%; - height: 100%; - position: absolute; - top: 0; - right: 0; - background: var(--color-bg); - color: var(--color-primary-darker); - font-weight: 400; - text-align: left; - visibility: hidden; - transform: translateX(100%); - transform-origin: right; - animation: var(--typing-animation); - - :global { - animation: var(--typing-animation); - } - } -} - -.wrapper { - --logo-size: #{clamp(fun.convert-px(90), 12vw, fun.convert-px(100))}; - - display: grid; - grid-template-columns: minmax(0, 1fr); - justify-items: center; - width: 100%; - - @include mix.media("screen") { - @include mix.dimensions("2xs") { - grid-template-columns: - var(--logo-size) - minmax(0, 1fr); - grid-template-rows: 1fr min-content; - align-items: center; - justify-items: left; - column-gap: var(--spacing-sm); - width: unset; - } - } - - .logo { - grid-row: span 2; - animation: flip-logo 9s ease-in 0s 1; - } - - .title { - font-size: clamp(var(--font-size-xl), 8vw, var(--font-size-2xl)); - text-align: center; - - @include typing-animation; - } - - .baseline { - color: var(--color-fg-light); - font-size: var(--font-size-lg); - text-align: center; - - @include typing-animation; - } - - .link { - background: linear-gradient( - to top, - var(--color-primary-light) fun.convert-px(5), - transparent fun.convert-px(5) - ) - left / 0 100% no-repeat; - text-decoration: none; - transition: all 0.6s ease-out 0s; - - &:hover, - &:focus { - background-size: 100% 100%; - } - - &:focus { - color: var(--color-primary-light); - } - - &:active { - background-size: 0 100%; - color: var(--color-primary-dark); - } - } -} - -@keyframes flip-logo { - 0%, - 90% { - transform: rotateY(180deg); - } - - 100% { - transform: rotateY(0deg); - } -} diff --git a/src/components/molecules/layout/branding.stories.tsx b/src/components/molecules/layout/branding.stories.tsx deleted file mode 100644 index 7ff88c9..0000000 --- a/src/components/molecules/layout/branding.stories.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import NextImage from 'next/image'; -import { Logo } from '../../atoms'; -import { Branding } from './branding'; - -/** - * Branding - Storybook Meta - */ -export default { - title: 'Molecules/Layout/Branding', - component: Branding, - args: { - isHome: false, - withLink: false, - }, - argTypes: { - baseline: { - control: { - type: 'text', - }, - description: 'The Branding baseline.', - type: { - name: 'string', - required: false, - }, - }, - isHome: { - control: { - type: 'boolean', - }, - description: 'Use H1 if the current page is homepage.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - photo: { - control: { - type: 'text', - }, - description: 'The Branding photo.', - type: { - name: 'string', - required: true, - }, - }, - title: { - control: { - type: 'text', - }, - description: 'The Branding title.', - type: { - name: 'string', - required: true, - }, - }, - withLink: { - control: { - type: 'boolean', - }, - description: 'Wraps the title with a link to homepage.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - }, -} as ComponentMeta<typeof Branding>; - -const Template: ComponentStory<typeof Branding> = (args) => ( - <Branding {...args} /> -); - -/** - * Branding Stories - Default - */ -export const Default = Template.bind({}); -Default.args = { - logo: <Logo heading="A logo example" />, - photo: ( - <NextImage - alt="A photo example" - height={200} - src="https://picsum.photos/200" - width={200} - /> - ), - title: 'Website title', -}; - -/** - * Branding Stories - With baseline - */ -export const WithBaseline = Template.bind({}); -WithBaseline.args = { - baseline: 'Maiores corporis qui', - logo: <Logo heading="A logo example" />, - photo: ( - <NextImage - alt="A photo example" - height={200} - src="https://picsum.photos/200" - width={200} - /> - ), - title: 'Website title', -}; diff --git a/src/components/molecules/layout/branding.test.tsx b/src/components/molecules/layout/branding.test.tsx deleted file mode 100644 index cfb55c5..0000000 --- a/src/components/molecules/layout/branding.test.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import NextImage from 'next/image'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { Logo } from '../../atoms'; -import { Branding } from './branding'; - -describe('Branding', () => { - it('renders a photo', () => { - const altText = 'A photo example'; - - render( - <Branding - logo={<Logo />} - photo={ - <NextImage - alt="A photo example" - height={200} - src="https://picsum.photos/200" - width={200} - /> - } - title="Website title" - /> - ); - expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument(); - }); - - it('renders a logo', () => { - const logoHeading = 'sed enim voluptatem'; - - render( - <Branding - logo={<Logo heading={logoHeading} />} - photo={ - <NextImage - alt="A photo example" - height={200} - src="https://picsum.photos/200" - width={200} - /> - } - title="Website name" - /> - ); - expect(rtlScreen.getByTitle(logoHeading)).toBeInTheDocument(); - }); - - it('renders a baseline', () => { - render( - <Branding - logo={<Logo />} - photo={ - <NextImage - alt="A photo example" - height={200} - src="https://picsum.photos/200" - width={200} - /> - } - title="Website title" - baseline="Website baseline" - /> - ); - expect(rtlScreen.getByText('Website baseline')).toBeInTheDocument(); - }); - - it('renders a title wrapped with h1 element', () => { - render( - <Branding - logo={<Logo />} - photo={ - <NextImage - alt="A photo example" - height={200} - src="https://picsum.photos/200" - width={200} - /> - } - title="Website title" - isHome={true} - /> - ); - expect( - rtlScreen.getByRole('heading', { level: 1, name: 'Website title' }) - ).toBeInTheDocument(); - }); - - it('renders a title with h1 styles', () => { - render( - <Branding - logo={<Logo />} - photo={ - <NextImage - alt="A photo example" - height={200} - src="https://picsum.photos/200" - width={200} - /> - } - title="Website title" - isHome={false} - /> - ); - expect( - rtlScreen.queryByRole('heading', { level: 1, name: 'Website title' }) - ).not.toBeInTheDocument(); - expect(rtlScreen.getByText('Website title')).toHaveClass('heading--1'); - }); -}); diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx deleted file mode 100644 index 9f8e6ce..0000000 --- a/src/components/molecules/layout/branding.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { type FC, useRef, type ReactNode } from 'react'; -import { useStyles } from '../../../utils/hooks'; -import { Heading, Link } from '../../atoms'; -import { FlippingLogo } from '../images'; -import styles from './branding.module.scss'; - -export type BrandingProps = { - /** - * The Branding baseline. - */ - baseline?: string; - /** - * Use H1 if the current page is homepage. Default: false. - */ - isHome?: boolean; - /** - * The website logo. - */ - logo: ReactNode; - /** - * Your photo. - */ - photo: ReactNode; - /** - * The Branding title; - */ - title: string; - /** - * Wraps the title with a link to homepage. Default: false. - */ - withLink?: boolean; -}; - -/** - * Branding component - * - * Render the branding logo, title and optional baseline. - */ -export const Branding: FC<BrandingProps> = ({ - baseline, - isHome = false, - logo, - photo, - title, - withLink = false, -}) => { - const baselineRef = useRef<HTMLParagraphElement>(null); - const titleRef = useRef<HTMLHeadingElement | HTMLParagraphElement>(null); - - useStyles({ - property: '--typing-animation', - styles: 'blink 0.7s ease-in-out 0s 2, typing 4.3s linear 0s 1', - target: titleRef, - }); - useStyles({ - property: '--typing-animation', - styles: - 'hide-text 4.25s linear 0s 1, blink 0.8s ease-in-out 4.25s 2, typing 3.8s linear 4.25s 1', - target: baselineRef, - }); - - return ( - <div className={styles.wrapper}> - <FlippingLogo back={logo} className={styles.logo} front={photo} /> - <Heading - className={styles.title} - isFake={!isHome} - level={1} - ref={titleRef} - > - {withLink ? ( - <Link className={styles.link} href="/"> - {title} - </Link> - ) : ( - title - )} - </Heading> - {baseline ? ( - <Heading className={styles.baseline} isFake level={4} ref={baselineRef}> - {baseline} - </Heading> - ) : null} - </div> - ); -}; diff --git a/src/components/molecules/layout/index.ts b/src/components/molecules/layout/index.ts index 1580baa..e43e664 100644 --- a/src/components/molecules/layout/index.ts +++ b/src/components/molecules/layout/index.ts @@ -1,4 +1,3 @@ -export * from './branding'; export * from './card'; export * from './code'; export * from './columns'; |
