Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/components/Headings/Heading.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { Heading, HeadingType } from './Heading';

describe('<Heading />', () => {
it('Renders standard heading levels (1-5)', () => {
const headers: HeadingType[] = ['1', '2', '3', '4', '5'];

for (const level of headers) {
const headingType: HeadingType = `${level}`;
const testid = `h${headingType}`;
const text = `Heading level ${headingType}`;

render(
<Heading type={headingType} data-testid={testid}>
{text}
</Heading>
);

const current = screen.getByTestId(testid);
expect(current).toBeInTheDocument();

expect(current.tagName.toLowerCase()).toBe(testid);
}
});

it('Renders Display heading', () => {
const headingType = `display`;
const testid = `h${headingType}`;
const text = `Heading level ${headingType}`;

render(
<Heading type={headingType} data-testid={testid}>
{text}
</Heading>
);

const current = screen.getByTestId(testid);

expect(current).toBeInTheDocument();
expect(current.tagName.toLowerCase()).toBe('h1');
expect(current.className === 'superheading').toBeTruthy();
});

it('Renders Eyebrow heading', () => {
const headingType = `eyebrow`;
const testid = `h${headingType}`;
const text = `Heading level ${headingType}`;

render(
<Heading type={headingType} data-testid={testid}>
{text}
</Heading>
);

const current = screen.getByTestId(testid);

expect(current).toBeInTheDocument();
expect(current.tagName.toLowerCase()).toBe('h5');
expect(current.className.includes('eyebrow')).toBeTruthy();
});

it('Renders Slug heading', () => {
const headingType = `slug`;
const testid = `h${headingType}`;
const text = `Heading level ${headingType}`;

render(
<Heading type={headingType} data-testid={testid}>
{text}
</Heading>
);

const wrapper = screen.getByTestId(testid);

expect(wrapper).toBeInTheDocument();
expect(wrapper.tagName.toLowerCase()).toBe('header');
expect(wrapper.classList.contains('m-slug-header'));

const content = screen.getByText(text);
expect(content.tagName.toLowerCase()).toBe('h2');
expect(content.classList.contains('a-heading'));
});
});
51 changes: 51 additions & 0 deletions src/components/Headings/Heading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import classnames from 'classnames';
import type { JSXElement } from '~/src/types/jsxElement';

export type HeadingType =
| '1'
| '2'
| '3'
| '4'
| '5'
| 'display'
| 'eyebrow'
| 'slug';

interface HeadingProperties extends React.HTMLProps<HTMLHeadingElement> {
/** Heading type (1-5, display, eyebrow, slug) */
type?: HeadingType;
}

export const Heading = ({
type = '1',
children,
className,
...properties
}: HeadingProperties): JSXElement => {
let DynamicHeading: keyof JSX.IntrinsicElements;
const classes = [className];

if (type === 'slug') {
classes.push('m-slug-header');

return (
<header className={classnames(classes)} {...properties}>
<h2 className='a-heading'>{children}</h2>
</header>
);
}

if (type === 'display') {
DynamicHeading = 'h1';
classes.push('superheading');
} else if (type === 'eyebrow') {
DynamicHeading = 'h5';
classes.push('eyebrow');
} else DynamicHeading = `h${type}`;

return (
<DynamicHeading {...properties} className={classnames(classes)}>
{children}
</DynamicHeading>
);
};
84 changes: 84 additions & 0 deletions src/components/Headings/Headings.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Heading } from '~/src/index';

/**
* A successful type hierarchy establishes the order of importance of elements on a page. Consistent scaling, weights, and capitalization are used to create distinction between headings and provide users with familiar focus points when scanning text.
*
* Source: <a href='https://cfpb.github.io/design-system/foundation/headings' target='_blank'> https://cfpb.github.io/design-system/foundation/headings</a>
*/
const meta: Meta<typeof Heading> = {
component: Heading,
argTypes: {
type: { control: { type: 'select' } }
}
};

export default meta;

type Story = StoryObj<typeof meta>;

export const H1: Story = {
name: 'H1',
args: {
children: 'Heading 1'
}
};

export const H2: Story = {
name: 'H2',
args: {
type: '2',
children: 'Heading 2'
}
};

export const H3: Story = {
name: 'H3',
args: {
type: '3',
children: 'Heading 3'
}
};

export const H4: Story = {
name: 'H4',
args: {
type: '4',
children: 'Heading 4'
}
};

export const H5: Story = {
name: 'H5',
args: {
type: '5',
children: 'Heading 5'
}
};

export const Display: Story = {
args: {
type: 'display',
children: 'Display'
}
};

export const Eyebrow: Story = {
args: {
type: 'eyebrow',
children: 'Eyebrow'
},
render: arguments_ => (
<>
<Heading {...arguments_} />
<Heading>Heading 1</Heading>
</>
)
};

export const Slug: Story = {
args: {
type: 'slug',
children: 'Slug'
}
};
64 changes: 62 additions & 2 deletions src/components/Icon/Icon.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta } from '@storybook/react';
import { Icon } from '~/src/index';
import type { Meta, StoryObj } from '@storybook/react';
import { Heading, Icon } from '~/src/index';
import type { HeadingType } from '../Headings/Heading';
import {
communicationIcons,
documentIcons,
Expand Down Expand Up @@ -29,6 +30,8 @@ Source: https://cfpb.github.io/design-system/foundation/iconography

export default meta;

type Story = StoryObj<typeof meta>;

const biggerIcon = { fontSize: '2em' };

const makeRows = (names: string[]): JSX.Element[] =>
Expand Down Expand Up @@ -105,3 +108,60 @@ export const ExpenseIcons = (): React.ReactElement => (
export const WebApplicationIcons = (): React.ReactElement => (
<IconTable>{makeRows(webIcons)}</IconTable>
);

export const IconWithText: Story = {
name: 'Icon with text',
render: () => {
interface LevelExample {
type: HeadingType;
text: string;
}

const acceptableLevels: LevelExample[] = [
{ type: '2', text: 'Auto loans' },
{ type: '3', text: 'Bank accounts' },
{ type: '4', text: 'Credit cards' },
{ type: '5', text: 'Submit a complaint' }
];

return (
<table>
<thead>
<th>Text element</th>
<th>Icon with background</th>
<th>Icon without background</th>
</thead>
<tbody>
{acceptableLevels.map(({ type, text }) => (
<tr key={type}>
<td>h{type}</td>
<td>
<Heading type={type}>
<Icon name='credit-card' withBg /> {text}
</Heading>
</td>
<td>
<Heading type={type}>
<Icon name='credit-card' /> {text}
</Heading>
</td>
</tr>
))}
<tr>
<td>p</td>
<td>
<p>
<Icon name='college' withBg /> Student loans
</p>
</td>
<td>
<p>
<Icon name='college' /> Student loans
</p>
</td>
</tr>
</tbody>
</table>
);
}
};
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { ExpandableGroup } from './components/Expandable/ExpandableGroup';
export { default as Footer } from './components/Footer/Footer';
export { FooterCfGov } from './components/Footer/FooterCfGov';
export { default as Grid } from './components/Grid';
export { Heading } from './components/Headings/Heading';
export { default as Hero } from './components/Hero/Hero';
export { Icon } from './components/Icon/Icon';
export { Label } from './components/Label/Label';
Expand Down
Loading