aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/heading
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-27 17:38:23 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:25:00 +0200
commit7255d25f6834a208c0ed44636356cc260f6ab6ba (patch)
tree88016a958190f766a3ac0ab4b77f4732e17502e8 /src/components/atoms/heading
parentba793e043e4d8515b1a9ea490ee2c5f92b1fd6c2 (diff)
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
Diffstat (limited to 'src/components/atoms/heading')
-rw-r--r--src/components/atoms/heading/heading.module.scss27
-rw-r--r--src/components/atoms/heading/heading.stories.tsx106
-rw-r--r--src/components/atoms/heading/heading.test.tsx80
-rw-r--r--src/components/atoms/heading/heading.tsx57
-rw-r--r--src/components/atoms/heading/index.ts1
5 files changed, 271 insertions, 0 deletions
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<typeof Heading>;
+
+const Template: ComponentStory<typeof Heading> = (args) => (
+ <Heading {...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(<Heading level={1}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 1 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h2', () => {
+ const body = 'iure';
+
+ render(<Heading level={2}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 2 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h3', () => {
+ const body = 'ut';
+
+ render(<Heading level={3}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 3 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h4', () => {
+ const body = 'dolor';
+
+ render(<Heading level={4}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 4 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h5', () => {
+ const body = 'temporibus';
+
+ render(<Heading level={5}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 5 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h6', () => {
+ const body = 'at';
+
+ render(<Heading level={6}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 6 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a fake heading', () => {
+ const body = 'dignissimos';
+
+ render(
+ <Heading isFake level={2}>
+ {body}
+ </Heading>
+ );
+
+ 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<HTMLHeadingElement> & {
+ /**
+ * 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<HTMLHeadingElement | HTMLParagraphElement>
+) => {
+ const HeadingTag = `h${level}` as const;
+ const levelClass = styles[`heading--${level}`];
+ const headingClass = `${levelClass} ${className}`;
+
+ return isFake ? (
+ <p {...props} className={headingClass} ref={ref}>
+ {children}
+ </p>
+ ) : (
+ <HeadingTag {...props} className={headingClass} ref={ref}>
+ {children}
+ </HeadingTag>
+ );
+};
+
+/**
+ * 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';