Skip to content

Commit 477988a

Browse files
Add panel component
1 parent 9891ee9 commit 477988a

File tree

7 files changed

+198
-0
lines changed

7 files changed

+198
-0
lines changed

src/__tests__/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe('Index', () => {
5252
'Legend',
5353
'NavAZ',
5454
'Pagination',
55+
'Panel',
5556
'Radios',
5657
'RadiosContext',
5758
'ReadingWidth',

src/components/content-presentation/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './hero/index.js';
44
export * from './icons/index.js';
55
export * from './images/index.js';
66
export * from './inset-text/index.js';
7+
export * from './panel/index.js';
78
export * from './summary-list/index.js';
89
export * from './table/index.js';
910
export * from './tabs/index.js';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import classNames from 'classnames';
2+
import { Children, forwardRef, type ComponentPropsWithoutRef, type FC } from 'react';
3+
import { HeadingLevel, type HeadingLevelProps } from '#components/utils/HeadingLevel.js';
4+
import { childIsOfComponentType } from '#util/types/TypeGuards.js';
5+
6+
export type PanelTitleProps = HeadingLevelProps;
7+
8+
const PanelTitle: FC<PanelTitleProps> = ({ children, headingLevel = 'h1', ...rest }) => (
9+
<HeadingLevel className="nhsuk-panel__title" headingLevel={headingLevel} {...rest}>
10+
{children}
11+
</HeadingLevel>
12+
);
13+
14+
export type PanelProps = ComponentPropsWithoutRef<'div'>;
15+
16+
const PanelComponent = forwardRef<HTMLDivElement, PanelProps>(
17+
({ children, className, ...rest }, forwardedRef) => {
18+
const items = Children.toArray(children);
19+
const title = items.find((child) => childIsOfComponentType(child, PanelTitle));
20+
const bodyItems = items.filter((child) => !childIsOfComponentType(child, PanelTitle));
21+
22+
return (
23+
<div className={classNames('nhsuk-panel', className)} ref={forwardedRef} {...rest}>
24+
{title}
25+
{bodyItems ? <div className="nhsuk-panel__body">{bodyItems}</div> : null}
26+
</div>
27+
);
28+
},
29+
);
30+
31+
PanelComponent.displayName = 'Panel';
32+
PanelComponent.displayName = 'Panel.Title';
33+
34+
export const Panel = Object.assign(PanelComponent, {
35+
Title: PanelTitle,
36+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { render } from '@testing-library/react';
2+
import { createRef } from 'react';
3+
import { Panel, type PanelTitleProps } from '..';
4+
import { renderClient, renderServer } from '#util/components';
5+
6+
describe('Panel', () => {
7+
it('matches snapshot', async () => {
8+
const { container } = await renderClient(
9+
<Panel>
10+
<Panel.Title>Booking complete</Panel.Title>
11+
We have sent you a confirmation email
12+
</Panel>,
13+
{ className: 'nhsuk-panel' },
14+
);
15+
16+
expect(container).toMatchSnapshot();
17+
});
18+
19+
it('matches snapshot (via server)', async () => {
20+
const { container, element } = await renderServer(
21+
<Panel>
22+
<Panel.Title>Booking complete</Panel.Title>
23+
We have sent you a confirmation email
24+
</Panel>,
25+
{ className: 'nhsuk-panel' },
26+
);
27+
28+
expect(container).toMatchSnapshot('server');
29+
30+
await renderClient(element, {
31+
className: 'nhsuk-panel',
32+
hydrate: true,
33+
container,
34+
});
35+
36+
expect(container).toMatchSnapshot('client');
37+
});
38+
39+
it('forwards refs', async () => {
40+
const ref = createRef<HTMLDivElement>();
41+
42+
const { modules } = await renderClient(
43+
<Panel ref={ref}>
44+
<Panel.Title>Booking complete</Panel.Title>
45+
We have sent you a confirmation email
46+
</Panel>,
47+
{ className: 'nhsuk-panel' },
48+
);
49+
50+
const [panelEl] = modules;
51+
52+
expect(ref.current).toBe(panelEl);
53+
expect(ref.current).toHaveClass('nhsuk-panel');
54+
});
55+
56+
it.each<PanelTitleProps | undefined>([
57+
undefined,
58+
{ headingLevel: 'h1' },
59+
{ headingLevel: 'h2' },
60+
{ headingLevel: 'h3' },
61+
{ headingLevel: 'h4' },
62+
])('renders heading level $headingLevel if specified', (props) => {
63+
const { container } = render(
64+
<Panel>
65+
<Panel.Title {...props}>Booking complete</Panel.Title>
66+
We have sent you a confirmation email
67+
</Panel>,
68+
);
69+
70+
const title = container.querySelector('.nhsuk-panel__title');
71+
72+
expect(title).toHaveProperty('tagName', props?.headingLevel?.toUpperCase() ?? 'H1');
73+
});
74+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2+
3+
exports[`Panel matches snapshot (via server): client 1`] = `
4+
<div>
5+
<div
6+
class="nhsuk-panel"
7+
>
8+
<h1
9+
class="nhsuk-panel__title"
10+
>
11+
Booking complete
12+
</h1>
13+
<div
14+
class="nhsuk-panel__body"
15+
>
16+
We have sent you a confirmation email
17+
</div>
18+
</div>
19+
</div>
20+
`;
21+
22+
exports[`Panel matches snapshot (via server): server 1`] = `
23+
<div>
24+
<div
25+
class="nhsuk-panel"
26+
>
27+
<h1
28+
class="nhsuk-panel__title"
29+
>
30+
Booking complete
31+
</h1>
32+
<div
33+
class="nhsuk-panel__body"
34+
>
35+
We have sent you a confirmation email
36+
</div>
37+
</div>
38+
</div>
39+
`;
40+
41+
exports[`Panel matches snapshot 1`] = `
42+
<div>
43+
<div
44+
class="nhsuk-panel"
45+
>
46+
<h1
47+
class="nhsuk-panel__title"
48+
>
49+
Booking complete
50+
</h1>
51+
<div
52+
class="nhsuk-panel__body"
53+
>
54+
We have sent you a confirmation email
55+
</div>
56+
</div>
57+
</div>
58+
`;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Panel.js';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { type Meta, type StoryObj } from '@storybook/react';
2+
import { Panel } from '#components';
3+
4+
const meta: Meta<typeof Panel> = {
5+
title: 'Content Presentation/Panel',
6+
component: Panel,
7+
};
8+
export default meta;
9+
type Story = StoryObj<typeof Panel>;
10+
11+
export const StandardPanel: Story = {
12+
render: () => (
13+
<Panel>
14+
<Panel.Title>Booking complete</Panel.Title>
15+
We have sent you a confirmation email
16+
</Panel>
17+
),
18+
};
19+
20+
export const PanelWithCustomHeadingLevel: Story = {
21+
render: () => (
22+
<Panel>
23+
<Panel.Title headingLevel="h2">Booking complete</Panel.Title>
24+
We have sent you a confirmation email
25+
</Panel>
26+
),
27+
};

0 commit comments

Comments
 (0)