From 7255d25f6834a208c0ed44636356cc260f6ab6ba Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Wed, 27 Sep 2023 17:38:23 +0200 Subject: refactor(components): rewrite Heading component * remove `alignment` and `withMargin` props (consumer should handle that) * move styles to Sass placeholders to avoid repeats with headings coming from WordPress * refactor some other components that depend on Heading to avoid ESlint errors --- src/components/atoms/heading/heading.module.scss | 27 ++++ src/components/atoms/heading/heading.stories.tsx | 106 ++++++++++++++ src/components/atoms/heading/heading.test.tsx | 80 +++++++++++ src/components/atoms/heading/heading.tsx | 57 ++++++++ src/components/atoms/heading/index.ts | 1 + src/components/atoms/headings/heading.module.scss | 69 --------- src/components/atoms/headings/heading.stories.tsx | 160 --------------------- src/components/atoms/headings/heading.test.tsx | 57 -------- src/components/atoms/headings/heading.tsx | 93 ------------ src/components/atoms/headings/index.ts | 1 - src/components/atoms/index.ts | 2 +- .../atoms/layout/section/section.module.scss | 1 + .../atoms/layout/section/section.stories.tsx | 2 +- src/components/atoms/modal/modal.stories.tsx | 4 +- src/components/atoms/modal/modal.test.tsx | 8 +- src/components/atoms/modal/modal.tsx | 10 +- 16 files changed, 285 insertions(+), 393 deletions(-) create mode 100644 src/components/atoms/heading/heading.module.scss create mode 100644 src/components/atoms/heading/heading.stories.tsx create mode 100644 src/components/atoms/heading/heading.test.tsx create mode 100644 src/components/atoms/heading/heading.tsx create mode 100644 src/components/atoms/heading/index.ts delete mode 100644 src/components/atoms/headings/heading.module.scss delete mode 100644 src/components/atoms/headings/heading.stories.tsx delete mode 100644 src/components/atoms/headings/heading.test.tsx delete mode 100644 src/components/atoms/headings/heading.tsx delete mode 100644 src/components/atoms/headings/index.ts (limited to 'src/components/atoms') diff --git a/src/components/atoms/heading/heading.module.scss b/src/components/atoms/heading/heading.module.scss new file mode 100644 index 0000000..a2e339a --- /dev/null +++ b/src/components/atoms/heading/heading.module.scss @@ -0,0 +1,27 @@ +@use "../../../styles/abstracts/placeholders"; + +.heading { + &--1 { + @extend %h1; + } + + &--2 { + @extend %h2; + } + + &--3 { + @extend %h3; + } + + &--4 { + @extend %h4; + } + + &--5 { + @extend %h5; + } + + &--6 { + @extend %h6; + } +} diff --git a/src/components/atoms/heading/heading.stories.tsx b/src/components/atoms/heading/heading.stories.tsx new file mode 100644 index 0000000..c5ac4a0 --- /dev/null +++ b/src/components/atoms/heading/heading.stories.tsx @@ -0,0 +1,106 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading } from './heading'; + +/** + * Heading - Storybook Meta + */ +export default { + title: 'Atoms/Headings', + component: Heading, + args: { + isFake: false, + }, + argTypes: { + children: { + description: 'Heading body.', + type: { + name: 'string', + required: true, + }, + }, + isFake: { + control: { + type: 'boolean', + }, + description: 'Use an heading element or only its styles.', + table: { + category: 'Options', + defaultValue: { summary: false }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + level: { + control: { + type: 'number', + min: 1, + max: 6, + }, + description: 'Heading level.', + type: { + name: 'number', + required: true, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +/** + * Heading Story - h1 + */ +export const H1 = Template.bind({}); +H1.args = { + children: 'Your title', + level: 1, +}; + +/** + * Heading Story - h2 + */ +export const H2 = Template.bind({}); +H2.args = { + children: 'Your title', + level: 2, +}; + +/** + * Heading Story - h3 + */ +export const H3 = Template.bind({}); +H3.args = { + children: 'Your title', + level: 3, +}; + +/** + * Heading Story - h4 + */ +export const H4 = Template.bind({}); +H4.args = { + children: 'Your title', + level: 4, +}; + +/** + * Heading Story - h5 + */ +export const H5 = Template.bind({}); +H5.args = { + children: 'Your title', + level: 5, +}; + +/** + * Heading Story - h6 + */ +export const H6 = Template.bind({}); +H6.args = { + children: 'Your title', + level: 6, +}; diff --git a/src/components/atoms/heading/heading.test.tsx b/src/components/atoms/heading/heading.test.tsx new file mode 100644 index 0000000..39b23ad --- /dev/null +++ b/src/components/atoms/heading/heading.test.tsx @@ -0,0 +1,80 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Heading } from './heading'; + +describe('Heading', () => { + it('renders a h1', () => { + const body = 'provident'; + + render({body}); + + expect(rtlScreen.getByRole('heading', { level: 1 })).toHaveTextContent( + body + ); + }); + + it('renders a h2', () => { + const body = 'iure'; + + render({body}); + + expect(rtlScreen.getByRole('heading', { level: 2 })).toHaveTextContent( + body + ); + }); + + it('renders a h3', () => { + const body = 'ut'; + + render({body}); + + expect(rtlScreen.getByRole('heading', { level: 3 })).toHaveTextContent( + body + ); + }); + + it('renders a h4', () => { + const body = 'dolor'; + + render({body}); + + expect(rtlScreen.getByRole('heading', { level: 4 })).toHaveTextContent( + body + ); + }); + + it('renders a h5', () => { + const body = 'temporibus'; + + render({body}); + + expect(rtlScreen.getByRole('heading', { level: 5 })).toHaveTextContent( + body + ); + }); + + it('renders a h6', () => { + const body = 'at'; + + render({body}); + + expect(rtlScreen.getByRole('heading', { level: 6 })).toHaveTextContent( + body + ); + }); + + it('renders a fake heading', () => { + const body = 'dignissimos'; + + render( + + {body} + + ); + + expect( + rtlScreen.queryByRole('heading', { level: 2 }) + ).not.toBeInTheDocument(); + expect(rtlScreen.getByText(body)).toHaveClass('heading--2'); + }); +}); diff --git a/src/components/atoms/heading/heading.tsx b/src/components/atoms/heading/heading.tsx new file mode 100644 index 0000000..6cdb578 --- /dev/null +++ b/src/components/atoms/heading/heading.tsx @@ -0,0 +1,57 @@ +import { + type ForwardedRef, + forwardRef, + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactNode, +} from 'react'; +import styles from './heading.module.scss'; + +// eslint-disable-next-line @typescript-eslint/no-magic-numbers +export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; + +export type HeadingProps = HTMLAttributes & { + /** + * The heading body. + */ + children: ReactNode; + /** + * Use an heading element or only its styles. + * + * @default false + */ + isFake?: boolean; + /** + * HTML heading level. + */ + level: HeadingLevel; +}; + +const HeadingWithRef: ForwardRefRenderFunction< + HTMLHeadingElement | HTMLParagraphElement, + HeadingProps +> = ( + { children, className = '', isFake = false, level, ...props }, + ref: ForwardedRef +) => { + const HeadingTag = `h${level}` as const; + const levelClass = styles[`heading--${level}`]; + const headingClass = `${levelClass} ${className}`; + + return isFake ? ( +

+ {children} +

+ ) : ( + + {children} + + ); +}; + +/** + * Heading component. + * + * Render an HTML heading element or a paragraph with heading styles. + */ +export const Heading = forwardRef(HeadingWithRef); diff --git a/src/components/atoms/heading/index.ts b/src/components/atoms/heading/index.ts new file mode 100644 index 0000000..3de265c --- /dev/null +++ b/src/components/atoms/heading/index.ts @@ -0,0 +1 @@ +export * from './heading'; diff --git a/src/components/atoms/headings/heading.module.scss b/src/components/atoms/headings/heading.module.scss deleted file mode 100644 index 1c898e6..0000000 --- a/src/components/atoms/headings/heading.module.scss +++ /dev/null @@ -1,69 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; - -.heading { - color: var(--color-primary-dark); - font-family: var(--font-family-secondary); - letter-spacing: 0.01ex; - - &--regular { - margin-bottom: 0; - margin-top: 0; - } - - &--left { - text-align: left; - } - - &--center { - width: fit-content; - margin-left: auto; - margin-right: auto; - } - - &--margin { - margin-top: 0; - margin-bottom: var(--spacing-sm); - - & + & { - margin-top: var(--spacing-md); - } - } - - &--1 { - font-size: var(--font-size-3xl); - font-weight: 500; - } - - &--2 { - padding-bottom: fun.convert-px(3); - background: linear-gradient( - to top, - var(--color-primary-dark) 0.3rem, - transparent 0.3rem - ) - 0 0 / 3rem 100% no-repeat; - font-size: var(--font-size-2xl); - font-weight: 500; - text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light); - } - - &--3 { - font-size: var(--font-size-xl); - font-weight: 500; - } - - &--4 { - font-size: var(--font-size-lg); - font-weight: 500; - } - - &--5 { - font-size: var(--font-size-md); - font-weight: 600; - } - - &--6 { - font-size: var(--font-size-md); - font-weight: 500; - } -} diff --git a/src/components/atoms/headings/heading.stories.tsx b/src/components/atoms/headings/heading.stories.tsx deleted file mode 100644 index 4aa79c2..0000000 --- a/src/components/atoms/headings/heading.stories.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Heading } from './heading'; - -/** - * Heading - Storybook Meta - */ -export default { - title: 'Atoms/Typography/Headings', - component: Heading, - args: { - alignment: 'left', - isFake: false, - withMargin: true, - }, - argTypes: { - alignment: { - control: { - type: 'select', - }, - description: 'The title alignment.', - options: ['center', 'left'], - table: { - category: 'Options', - defaultValue: { summary: 'left' }, - }, - type: { - name: 'string', - required: false, - }, - }, - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - children: { - description: 'Heading body.', - type: { - name: 'string', - required: true, - }, - }, - id: { - control: { - type: 'text', - }, - description: 'An unique id.', - type: { - name: 'string', - required: false, - }, - }, - isFake: { - control: { - type: 'boolean', - }, - description: 'Use an heading element or only its styles.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - level: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'Heading level.', - type: { - name: 'number', - required: true, - }, - }, - withMargin: { - control: { - type: 'boolean', - }, - description: 'Adds margin.', - table: { - category: 'Options', - defaultValue: { summary: true }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - }, -} as ComponentMeta; - -const Template: ComponentStory = (args) => ( - -); - -/** - * Heading Story - h1 - */ -export const H1 = Template.bind({}); -H1.args = { - children: 'Your title', - level: 1, -}; - -/** - * Heading Story - h2 - */ -export const H2 = Template.bind({}); -H2.args = { - children: 'Your title', - level: 2, -}; - -/** - * Heading Story - h3 - */ -export const H3 = Template.bind({}); -H3.args = { - children: 'Your title', - level: 3, -}; - -/** - * Heading Story - h4 - */ -export const H4 = Template.bind({}); -H4.args = { - children: 'Your title', - level: 4, -}; - -/** - * Heading Story - h5 - */ -export const H5 = Template.bind({}); -H5.args = { - children: 'Your title', - level: 5, -}; - -/** - * Heading Story - h6 - */ -export const H6 = Template.bind({}); -H6.args = { - children: 'Your title', - level: 6, -}; diff --git a/src/components/atoms/headings/heading.test.tsx b/src/components/atoms/headings/heading.test.tsx deleted file mode 100644 index 61d7f8e..0000000 --- a/src/components/atoms/headings/heading.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { Heading } from './heading'; - -describe('Heading', () => { - it('renders a h1', () => { - render(Level 1); - expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent( - 'Level 1' - ); - }); - - it('renders a h2', () => { - render(Level 2); - expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent( - 'Level 2' - ); - }); - - it('renders a h3', () => { - render(Level 3); - expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent( - 'Level 3' - ); - }); - - it('renders a h4', () => { - render(Level 4); - expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent( - 'Level 4' - ); - }); - - it('renders a h5', () => { - render(Level 5); - expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent( - 'Level 5' - ); - }); - - it('renders a h6', () => { - render(Level 6); - expect(screen.getByRole('heading', { level: 6 })).toHaveTextContent( - 'Level 6' - ); - }); - - it('renders a text with heading styles', () => { - render( - - Fake heading - - ); - expect(screen.queryByRole('heading', { level: 2 })).not.toBeInTheDocument(); - expect(screen.getByText('Fake heading')).toHaveClass('heading'); - }); -}); diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx deleted file mode 100644 index b1b6164..0000000 --- a/src/components/atoms/headings/heading.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { - createElement, - ForwardedRef, - forwardRef, - ForwardRefRenderFunction, - HTMLAttributes, - ReactNode, -} from 'react'; -import styles from './heading.module.scss'; - -export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; - -export type HeadingProps = HTMLAttributes & { - /** - * The title alignment. Default: left; - */ - alignment?: 'center' | 'left'; - /** - * The heading body. - */ - children: ReactNode; - /** - * Use an heading element or only its styles. Default: false. - */ - isFake?: boolean; - /** - * HTML heading level. - */ - level: HeadingLevel; - /** - * Adds margin. Default: true. - */ - withMargin?: boolean; -}; - -type TitleTagProps = Pick & { - tagName: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p'; -}; - -const TitleTag = forwardRef< - HTMLHeadingElement | HTMLParagraphElement, - TitleTagProps ->( - ( - { children, tagName, ...props }, - ref: ForwardedRef - ) => { - return createElement(tagName, { ...props, ref }, children); - } -); -TitleTag.displayName = 'TitleTag'; - -const HeadingWithRef: ForwardRefRenderFunction< - HTMLHeadingElement | HTMLParagraphElement, - HeadingProps -> = ( - { - alignment = 'left', - children, - className = '', - id, - isFake = false, - level, - withMargin = true, - ...props - }, - ref: ForwardedRef -) => { - const tagName = isFake ? 'p' : (`h${level}` as TitleTagProps['tagName']); - const levelClass = `heading--${level}`; - const alignmentClass = `heading--${alignment}`; - const marginClass = withMargin ? 'heading--margin' : 'heading--regular'; - const headingClass = `${styles.heading} ${styles[levelClass]} ${styles[alignmentClass]} ${styles[marginClass]} ${className}`; - - return ( - - {children} - - ); -}; - -/** - * Heading component. - * - * Render an HTML heading element or a paragraph with heading styles. - */ -export const Heading = forwardRef(HeadingWithRef); diff --git a/src/components/atoms/headings/index.ts b/src/components/atoms/headings/index.ts deleted file mode 100644 index 3de265c..0000000 --- a/src/components/atoms/headings/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './heading'; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index d9cf865..672440c 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -1,6 +1,6 @@ export * from './buttons'; export * from './forms'; -export * from './headings'; +export * from './heading'; export * from './icons'; export * from './images'; export * from './layout'; diff --git a/src/components/atoms/layout/section/section.module.scss b/src/components/atoms/layout/section/section.module.scss index 771b8e3..3da74a2 100644 --- a/src/components/atoms/layout/section/section.module.scss +++ b/src/components/atoms/layout/section/section.module.scss @@ -21,5 +21,6 @@ > * { 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 0a3388b..e21209b 100644 --- a/src/components/atoms/layout/section/section.stories.tsx +++ b/src/components/atoms/layout/section/section.stories.tsx @@ -1,5 +1,5 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Heading } from '../../headings'; +import { Heading } from '../../heading'; import { Section } from './section'; /** diff --git a/src/components/atoms/modal/modal.stories.tsx b/src/components/atoms/modal/modal.stories.tsx index d0c2f0b..0490a8f 100644 --- a/src/components/atoms/modal/modal.stories.tsx +++ b/src/components/atoms/modal/modal.stories.tsx @@ -1,6 +1,6 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { Heading } from '../heading'; import { Modal } from './modal'; -import { Heading } from '../headings'; /** * Switch - Storybook Meta diff --git a/src/components/atoms/modal/modal.test.tsx b/src/components/atoms/modal/modal.test.tsx index 6e7d29e..dfa4a88 100644 --- a/src/components/atoms/modal/modal.test.tsx +++ b/src/components/atoms/modal/modal.test.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; -import { Heading } from '../headings'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; +import { Heading } from '../heading'; import { Modal } from './modal'; const title = 'A custom title'; @@ -16,11 +16,11 @@ describe('Modal', () => { {children} ); - expect(screen.getByRole('heading', { level })).toHaveTextContent(title); + expect(rtlScreen.getByRole('heading', { level })).toHaveTextContent(title); }); it('renders the modal body', () => { render({children}); - expect(screen.getByText(children)).toBeInTheDocument(); + expect(rtlScreen.getByText(children)).toBeInTheDocument(); }); }); diff --git a/src/components/atoms/modal/modal.tsx b/src/components/atoms/modal/modal.tsx index 78b4f6e..6f5506f 100644 --- a/src/components/atoms/modal/modal.tsx +++ b/src/components/atoms/modal/modal.tsx @@ -1,11 +1,11 @@ import { - ForwardRefRenderFunction, - HTMLAttributes, - ReactElement, - ReactNode, + type ForwardRefRenderFunction, + type HTMLAttributes, + type ReactElement, + type ReactNode, forwardRef, } from 'react'; -import { HeadingProps } from '../headings'; +import type { HeadingProps } from '../heading'; import styles from './modal.module.scss'; export type ModalProps = HTMLAttributes & { -- cgit v1.2.3