Skip to content

Commit 91c0094

Browse files
Update footer for NHS.UK frontend v10.0.0
1 parent 36a44eb commit 91c0094

File tree

4 files changed

+129
-170
lines changed

4 files changed

+129
-170
lines changed

src/components/navigation/footer/Footer.tsx

Lines changed: 59 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,98 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import React, { Children, FC, HTMLProps, cloneElement } from 'react';
1+
import React, { Children, FC, HTMLProps } from 'react';
32
import classNames from 'classnames';
43
import { Container } from '@components/layout';
54
import { childIsOfComponentType } from '@util/types/TypeGuards';
65

7-
type FooterListProps = HTMLProps<HTMLOListElement> & { singleColumn?: boolean };
6+
interface FooterMetaProps extends HTMLProps<HTMLUListElement> {
7+
containerClassName?: string;
8+
visuallyHiddenText?: string;
9+
}
810

9-
const FooterList: FC<FooterListProps> = ({
10-
className,
11+
const FooterMeta: FC<FooterMetaProps> = ({
1112
children,
12-
singleColumn = false,
13+
visuallyHiddenText = 'Support links',
1314
...rest
1415
}) => {
15-
let newChildren = children;
16+
const items = Children.toArray(children);
1617

17-
if (singleColumn) {
18-
newChildren = Children.map(newChildren, (child) =>
19-
childIsOfComponentType(child, FooterListItem) ? cloneElement(child, { singleColumn }) : child,
20-
);
21-
}
18+
const metaItems = items.filter((child) => childIsOfComponentType(child, FooterListItem));
19+
const metaCopyright = items.filter((child) => childIsOfComponentType(child, FooterCopyright));
2220

2321
return (
24-
<ul className={classNames('nhsuk-footer__list', className)} {...rest}>
25-
{newChildren}
26-
</ul>
22+
<div className="nhsuk-footer__meta">
23+
<h2 className="nhsuk-u-visually-hidden">{visuallyHiddenText}</h2>
24+
<FooterList {...rest}>{metaItems}</FooterList>
25+
{metaCopyright.length ? metaCopyright : <FooterCopyright />}
26+
</div>
2727
);
2828
};
2929

30-
const FooterListItem: FC<HTMLProps<HTMLAnchorElement> & { singleColumn?: boolean }> = ({
31-
className,
32-
singleColumn = false,
33-
...rest
34-
}) => (
35-
<li
36-
className={classNames(
37-
'nhsuk-footer__list-item',
38-
singleColumn ? 'nhsuk-footer-default__list-item' : '',
39-
)}
40-
>
30+
type FooterListProps = HTMLProps<HTMLUListElement>;
31+
32+
const FooterList: FC<FooterListProps> = ({ className, children, ...rest }) => (
33+
<ul className={classNames('nhsuk-footer__list', className)} {...rest}>
34+
{children}
35+
</ul>
36+
);
37+
38+
const FooterListItem: FC<HTMLProps<HTMLAnchorElement>> = ({ className, ...rest }) => (
39+
<li className="nhsuk-footer__list-item">
4140
<a className={classNames('nhsuk-footer__list-item-link', className)} {...rest} />
4241
</li>
4342
);
4443

45-
const FooterCopyright: FC<HTMLProps<HTMLParagraphElement>> = ({ className, ...rest }) => (
46-
<p className={classNames('nhsuk-footer__copyright', className)} {...rest} />
44+
const FooterCopyright: FC<HTMLProps<HTMLParagraphElement>> = ({
45+
children = '© NHS England',
46+
className,
47+
...rest
48+
}) => (
49+
<p className={classNames('nhsuk-body-s', className)} {...rest}>
50+
{children}
51+
</p>
4752
);
4853

4954
interface FooterProps extends HTMLProps<HTMLDivElement> {
50-
visuallyHiddenText?: false | string;
55+
containerClassName?: string;
5156
}
5257

5358
interface FooterComponent extends FC<FooterProps> {
59+
Meta: FC<FooterMetaProps>;
5460
List: FC<FooterListProps>;
5561
ListItem: FC<HTMLProps<HTMLAnchorElement>>;
5662
Copyright: FC<HTMLProps<HTMLParagraphElement>>;
5763
}
5864

59-
const FooterComponent: FooterComponent = ({
60-
className,
61-
children,
62-
visuallyHiddenText = 'Support links',
63-
...rest
64-
}) => {
65-
const footerCols = Children.toArray(children).filter((child) =>
66-
childIsOfComponentType(child, FooterList),
67-
);
68-
const footerCopyright = Children.toArray(children).filter((child) =>
69-
childIsOfComponentType(child, FooterCopyright),
70-
);
65+
const FooterComponent: FooterComponent = ({ className, containerClassName, children, ...rest }) => {
66+
const items = Children.toArray(children);
67+
const meta = items.filter((child) => childIsOfComponentType(child, FooterMeta));
68+
const columns = items.filter((child) => childIsOfComponentType(child, FooterList));
7169

72-
let newChildren;
73-
const footerHasMultipleColumns = footerCols.length > 1;
74-
75-
if (footerHasMultipleColumns) {
76-
// Remove the copyright from being rendered inside the 'nhsuk-footer' div
77-
newChildren = Children.toArray(children).filter(
78-
(child) => !childIsOfComponentType(child, FooterCopyright),
79-
);
80-
} else {
81-
newChildren = Children.map(children, (child) =>
82-
childIsOfComponentType(child, FooterList)
83-
? cloneElement(child, { singleColumn: true })
84-
: child,
85-
);
86-
}
70+
const columnsPerRow = 4;
71+
const columnsTotal = Math.ceil(columns.length / columnsPerRow);
72+
73+
const rows = Array.from({ length: columnsTotal }, (column, index) =>
74+
columns.slice(index * columnsPerRow, index * columnsPerRow + columnsPerRow),
75+
);
8776

8877
return (
89-
<footer role="contentinfo" {...rest}>
90-
<div className={classNames('nhsuk-footer-container', className)}>
91-
<Container>
92-
{visuallyHiddenText ? (
93-
<h2 className="nhsuk-u-visually-hidden">{visuallyHiddenText}</h2>
94-
) : null}
95-
<div className="nhsuk-footer">{newChildren}</div>
96-
{footerHasMultipleColumns ? <div>{footerCopyright}</div> : undefined}
97-
</Container>
98-
</div>
78+
<footer className={classNames('nhsuk-footer', className)} role="contentinfo" {...rest}>
79+
<Container className={containerClassName}>
80+
{rows.map((row, rowIndex) => (
81+
<div className="nhsuk-footer__navigation nhsuk-grid-row" key={`row-${rowIndex}`}>
82+
{row.map((column, columnIndex) => (
83+
<div className="nhsuk-grid-column-one-quarter" key={`column-${columnIndex}`}>
84+
{column}
85+
</div>
86+
))}
87+
</div>
88+
))}
89+
{meta}
90+
</Container>
9991
</footer>
10092
);
10193
};
10294

95+
FooterComponent.Meta = FooterMeta;
10396
FooterComponent.List = FooterList;
10497
FooterComponent.ListItem = FooterListItem;
10598
FooterComponent.Copyright = FooterCopyright;

src/components/navigation/footer/__tests__/Footer.test.tsx

Lines changed: 36 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,103 +10,56 @@ describe('Footer', () => {
1010

1111
expect(container).toMatchSnapshot('Footer');
1212
});
13-
it('has default visually hidden text', () => {
14-
const { container } = render(<Footer />);
1513

16-
expect(container.querySelector('.nhsuk-u-visually-hidden')?.textContent).toBe('Support links');
17-
});
14+
describe('Footer.List', () => {
15+
afterEach(() => {
16+
jest.clearAllMocks();
17+
});
1818

19-
it('has disabled visually hidden text', () => {
20-
const { container } = render(<Footer visuallyHiddenText={false} />);
19+
it('matches snapshot', () => {
20+
const { container } = render(<Footer.List />);
2121

22-
expect(container.querySelector('.nhsuk-u-visually-hidden')).toBeFalsy();
22+
expect(container).toMatchSnapshot('Footer.List');
23+
});
2324
});
2425

25-
it('has custom visually hidden text', () => {
26-
const { container } = render(<Footer visuallyHiddenText="Custom" />);
26+
describe('Footer.Meta', () => {
27+
it('matches snapshot', () => {
28+
const { container } = render(<Footer.Meta />);
2729

28-
expect(container.querySelector('.nhsuk-u-visually-hidden')?.textContent).toBe('Custom');
29-
});
30+
expect(container).toMatchSnapshot('Footer.Meta');
31+
});
3032

31-
it('Includes the single column class on ListItem when there is only one column', () => {
32-
const { container } = render(
33-
<Footer>
34-
<Footer.List>
35-
<Footer.ListItem id="test-listItem"></Footer.ListItem>
36-
</Footer.List>
37-
</Footer>,
38-
);
39-
40-
expect(container.querySelector('#test-listItem')?.parentElement).toHaveClass(
41-
'nhsuk-footer-default__list-item',
42-
);
43-
});
33+
it('has default visually hidden text', () => {
34+
const { container } = render(<Footer.Meta />);
4435

45-
it('Renders the copyright within the nhsuk-footer when there is only one column', () => {
46-
const { container } = render(
47-
<Footer>
48-
<Footer.List>
49-
<Footer.ListItem id="test-listItem"></Footer.ListItem>
50-
</Footer.List>
51-
<Footer.Copyright>This is the copyright</Footer.Copyright>
52-
</Footer>,
53-
);
54-
expect(container.querySelectorAll('.nhsuk-footer__copyright').length).toBe(1);
55-
expect(
56-
container.querySelector('.nhsuk-footer')?.querySelector('.nhsuk-footer__copyright'),
57-
).not.toBeNull();
58-
});
36+
expect(container.querySelector('.nhsuk-u-visually-hidden')?.textContent).toBe(
37+
'Support links',
38+
);
39+
});
5940

60-
it('Renders the copyright outside of the nhsuk-footer when there is more than one column', () => {
61-
const { container } = render(
62-
<Footer>
63-
<Footer.List>
64-
<Footer.ListItem id="test-listItem"></Footer.ListItem>
65-
</Footer.List>
66-
<Footer.List>
67-
<Footer.ListItem id="test-listItem2"></Footer.ListItem>
68-
</Footer.List>
69-
<Footer.Copyright>This is the copyright</Footer.Copyright>
70-
</Footer>,
71-
);
72-
expect(container.querySelectorAll('.nhsuk-footer__copyright').length).toBe(1);
73-
expect(
74-
container.querySelector('.nhsuk-width-container')?.querySelector('.nhsuk-footer__copyright'),
75-
).not.toBeNull();
76-
expect(
77-
container.querySelector('.nhsuk-footer')?.querySelector('.nhsuk-footer__copyright'),
78-
).toBeNull();
79-
});
41+
it('has custom visually hidden text', () => {
42+
const { container } = render(<Footer.Meta visuallyHiddenText="Custom" />);
8043

81-
it('Does not include the single column class on ListItem when there is more than one column', () => {
82-
const { container } = render(
83-
<Footer>
84-
<Footer.List>
85-
<Footer.ListItem id="test-listItem"></Footer.ListItem>
86-
</Footer.List>
87-
<Footer.List>
88-
<Footer.ListItem id="test-listItem2"></Footer.ListItem>
89-
</Footer.List>
90-
</Footer>,
91-
);
92-
93-
expect(container.querySelector('#test-listItem')?.parentElement).not.toHaveClass(
94-
'nhsuk-footer-default__list-item',
95-
);
96-
expect(container.querySelector('#test-listItem2')?.parentElement).not.toHaveClass(
97-
'nhsuk-footer-default__list-item',
98-
);
99-
});
44+
expect(container.querySelector('.nhsuk-u-visually-hidden')?.textContent).toBe('Custom');
45+
});
10046

101-
describe('Footer.List', () => {
102-
afterEach(() => {
103-
jest.clearAllMocks();
47+
it('has default copyright text', () => {
48+
const { container } = render(<Footer.Meta />);
49+
50+
expect(container).toContainHTML('<p class="nhsuk-body-s">© NHS England</p>');
10451
});
10552

106-
it('matches snapshot', () => {
107-
const { container } = render(<Footer.List />);
53+
it('has custom copyright text', () => {
54+
const { container } = render(
55+
<Footer.Meta>
56+
<Footer.Copyright>© East London NHS Foundation Trust</Footer.Copyright>
57+
</Footer.Meta>,
58+
);
10859

109-
expect(container).toMatchSnapshot('Footer.List');
60+
expect(container).toContainHTML(
61+
'<p class="nhsuk-body-s">© East London NHS Foundation Trust</p>',
62+
);
11063
});
11164
});
11265

src/components/navigation/footer/__tests__/__snapshots__/Footer.test.tsx.snap

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
exports[`Footer Footer.Copyright matches snapshot: Footer.Copyright 1`] = `
44
<div>
55
<p
6-
class="nhsuk-footer__copyright"
7-
/>
6+
class="nhsuk-body-s"
7+
>
8+
© NHS England
9+
</p>
810
</div>
911
`;
1012

@@ -28,27 +30,37 @@ exports[`Footer Footer.ListItem matches snapshot: Footer.ListItem 1`] = `
2830
</div>
2931
`;
3032

33+
exports[`Footer Footer.Meta matches snapshot: Footer.Meta 1`] = `
34+
<div>
35+
<div
36+
class="nhsuk-footer__meta"
37+
>
38+
<h2
39+
class="nhsuk-u-visually-hidden"
40+
>
41+
Support links
42+
</h2>
43+
<ul
44+
class="nhsuk-footer__list"
45+
/>
46+
<p
47+
class="nhsuk-body-s"
48+
>
49+
© NHS England
50+
</p>
51+
</div>
52+
</div>
53+
`;
54+
3155
exports[`Footer matches snapshot: Footer 1`] = `
3256
<div>
3357
<footer
58+
class="nhsuk-footer"
3459
role="contentinfo"
3560
>
3661
<div
37-
class="nhsuk-footer-container"
38-
>
39-
<div
40-
class="nhsuk-width-container"
41-
>
42-
<h2
43-
class="nhsuk-u-visually-hidden"
44-
>
45-
Support links
46-
</h2>
47-
<div
48-
class="nhsuk-footer"
49-
/>
50-
</div>
51-
</div>
62+
class="nhsuk-width-container"
63+
/>
5264
</footer>
5365
</div>
5466
`;

stories/Navigation/Footer.stories.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const Standard: Story = {
2525
<Footer.ListItem href="https://www.nhs.uk/about-us/sitemap/">Sitemap</Footer.ListItem>
2626
<Footer.ListItem href="https://www.nhs.uk/our-policies/">Our policies</Footer.ListItem>
2727
</Footer.List>
28-
<Footer.Copyright>&copy; Crown copyright</Footer.Copyright>
28+
<Footer.Copyright />
2929
</Footer>
3030
</>
3131
),
@@ -61,13 +61,14 @@ export const WithLinksArrangedInColumns: Story = {
6161
<Footer.ListItem href="#">Profile editor login</Footer.ListItem>
6262
</Footer.List>
6363

64-
<Footer.List className="nhsuk-footer__meta">
64+
<Footer.Meta>
6565
<Footer.ListItem href="#">About us</Footer.ListItem>
6666
<Footer.ListItem href="#">Accessibility statement</Footer.ListItem>
6767
<Footer.ListItem href="#">Our policies</Footer.ListItem>
6868
<Footer.ListItem href="#">Cookies</Footer.ListItem>
69-
</Footer.List>
70-
<Footer.Copyright>&copy; Crown copyright</Footer.Copyright>
69+
70+
<Footer.Copyright />
71+
</Footer.Meta>
7172
</Footer>
7273
</>
7374
),

0 commit comments

Comments
 (0)