summaryrefslogtreecommitdiffstats
path: root/src/components/atoms/headings
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/atoms/headings')
-rw-r--r--src/components/atoms/headings/heading.module.scss69
-rw-r--r--src/components/atoms/headings/heading.stories.tsx160
-rw-r--r--src/components/atoms/headings/heading.test.tsx56
-rw-r--r--src/components/atoms/headings/heading.tsx94
4 files changed, 379 insertions, 0 deletions
diff --git a/src/components/atoms/headings/heading.module.scss b/src/components/atoms/headings/heading.module.scss
new file mode 100644
index 0000000..a420bc1
--- /dev/null
+++ b/src/components/atoms/headings/heading.module.scss
@@ -0,0 +1,69 @@
+@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
new file mode 100644
index 0000000..0e3885d
--- /dev/null
+++ b/src/components/atoms/headings/heading.stories.tsx
@@ -0,0 +1,160 @@
+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<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/headings/heading.test.tsx b/src/components/atoms/headings/heading.test.tsx
new file mode 100644
index 0000000..6b6789a
--- /dev/null
+++ b/src/components/atoms/headings/heading.test.tsx
@@ -0,0 +1,56 @@
+import { render, screen } from '@test-utils';
+import Heading from './heading';
+
+describe('Heading', () => {
+ it('renders a h1', () => {
+ render(<Heading level={1}>Level 1</Heading>);
+ expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(
+ 'Level 1'
+ );
+ });
+
+ it('renders a h2', () => {
+ render(<Heading level={2}>Level 2</Heading>);
+ expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
+ 'Level 2'
+ );
+ });
+
+ it('renders a h3', () => {
+ render(<Heading level={3}>Level 3</Heading>);
+ expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(
+ 'Level 3'
+ );
+ });
+
+ it('renders a h4', () => {
+ render(<Heading level={4}>Level 4</Heading>);
+ expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent(
+ 'Level 4'
+ );
+ });
+
+ it('renders a h5', () => {
+ render(<Heading level={5}>Level 5</Heading>);
+ expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent(
+ 'Level 5'
+ );
+ });
+
+ it('renders a h6', () => {
+ render(<Heading level={6}>Level 6</Heading>);
+ expect(screen.getByRole('heading', { level: 6 })).toHaveTextContent(
+ 'Level 6'
+ );
+ });
+
+ it('renders a text with heading styles', () => {
+ render(
+ <Heading isFake={true} level={2}>
+ Fake heading
+ </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
new file mode 100644
index 0000000..e385249
--- /dev/null
+++ b/src/components/atoms/headings/heading.tsx
@@ -0,0 +1,94 @@
+import {
+ createElement,
+ ForwardedRef,
+ forwardRef,
+ ForwardRefRenderFunction,
+ ReactNode,
+} from 'react';
+import styles from './heading.module.scss';
+
+export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
+
+export type HeadingProps = {
+ /**
+ * The title alignment. Default: left;
+ */
+ alignment?: 'center' | 'left';
+ /**
+ * The heading body.
+ */
+ children: ReactNode;
+ /**
+ * Set additional classnames.
+ */
+ className?: string;
+ /**
+ * The heading id.
+ */
+ id?: string;
+ /**
+ * 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<HeadingProps, 'children' | 'className' | 'id'> & {
+ tagName: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
+};
+
+const TitleTag = forwardRef<
+ HTMLHeadingElement | HTMLParagraphElement,
+ TitleTagProps
+>(
+ (
+ { children, tagName, ...props },
+ ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
+ ) => {
+ return createElement(tagName, { ...props, ref }, children);
+ }
+);
+TitleTag.displayName = 'TitleTag';
+
+/**
+ * Heading component.
+ *
+ * Render an HTML heading element or a paragraph with heading styles.
+ */
+const Heading: ForwardRefRenderFunction<HTMLDivElement, HeadingProps> = (
+ {
+ alignment = 'left',
+ children,
+ className,
+ id,
+ isFake = false,
+ level,
+ withMargin = true,
+ },
+ ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
+) => {
+ const tagName = isFake ? 'p' : (`h${level}` as TitleTagProps['tagName']);
+ const levelClass = `heading--${level}`;
+ const alignmentClass = `heading--${alignment}`;
+ const marginClass = withMargin ? 'heading--margin' : 'heading--regular';
+
+ return (
+ <TitleTag
+ tagName={tagName}
+ className={`${styles.heading} ${styles[levelClass]} ${styles[alignmentClass]} ${styles[marginClass]} ${className}`}
+ id={id}
+ ref={ref}
+ >
+ {children}
+ </TitleTag>
+ );
+};
+
+export default forwardRef(Heading);