Skip to content

Commit 1617491

Browse files
Update skip link for NHS.UK frontend v10.0.0
1 parent 9620e5f commit 1617491

File tree

4 files changed

+38
-162
lines changed

4 files changed

+38
-162
lines changed

src/components/navigation/skip-link/SkipLink.tsx

Lines changed: 12 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,30 @@
1-
import React, { HTMLProps, MouseEvent, useEffect } from 'react';
1+
import React, { ComponentProps, useEffect, useRef, useState } from 'react';
2+
import { SkipLink } from 'nhsuk-frontend';
23
import classNames from 'classnames';
34

4-
interface SkipLinkProps extends HTMLProps<HTMLAnchorElement> {
5-
/** The reference to the element to set focus to when the link is clicked */
6-
focusTargetRef?: React.RefObject<HTMLElement>;
7-
/** Disables the default anchor click behaviour, avoiding navigation entries being added to the browser history */
8-
disableDefaultBehaviour?: boolean;
9-
/** Disables focusing the first h1 level heading when {@link focusTargetRef} is not set */
10-
disableHeadingFocus?: boolean;
11-
}
12-
135
const SkipLinkComponent = ({
146
children = 'Skip to main content',
157
className,
16-
disableDefaultBehaviour,
17-
disableHeadingFocus,
18-
focusTargetRef,
198
href = '#maincontent',
20-
tabIndex = 0,
21-
onClick,
229
...rest
23-
}: SkipLinkProps) => {
24-
let firstHeadingElement: HTMLElement | null = null;
25-
26-
const getFirstHeadingElement = (): HTMLElement | null => {
27-
const allHeadings = document.getElementsByTagName('h1');
28-
if (allHeadings.length > 0) {
29-
return allHeadings[0];
30-
}
31-
return null;
32-
};
33-
34-
const handleHeadingBlur = (event: Event) => {
35-
event.preventDefault();
36-
if (firstHeadingElement) {
37-
unfocusElement(firstHeadingElement);
38-
}
39-
};
10+
}: ComponentProps<'a'>) => {
11+
const moduleRef = useRef<HTMLAnchorElement>(null);
12+
const [instance, setInstance] = useState<SkipLink>();
4013

4114
useEffect(() => {
42-
firstHeadingElement = getFirstHeadingElement();
43-
if (firstHeadingElement) {
44-
firstHeadingElement.addEventListener('blur', handleHeadingBlur);
15+
if (!moduleRef.current || instance) {
16+
return;
4517
}
46-
return () => {
47-
if (firstHeadingElement) {
48-
firstHeadingElement.removeEventListener('blur', handleHeadingBlur);
49-
}
50-
};
51-
}, []);
5218

53-
const focusElement = (element: HTMLElement): void => {
54-
// Sometimes custom focus code can cause a loop of focus events
55-
// (especially in IE11), so check for attributes before performing
56-
// focus actions and DOM manipulation.
57-
if (!element.hasAttribute('tabIndex')) {
58-
element.setAttribute('tabIndex', '-1');
59-
}
60-
if (document.activeElement !== element) {
61-
element.focus();
62-
}
63-
};
64-
65-
const unfocusElement = (element: HTMLElement): void => {
66-
if (element.hasAttribute('tabIndex')) element.removeAttribute('tabIndex');
67-
};
68-
69-
const focusTarget = (event: MouseEvent<HTMLAnchorElement>): void => {
70-
if (disableDefaultBehaviour) event.preventDefault();
71-
if (focusTargetRef && focusTargetRef.current) {
72-
focusElement(focusTargetRef.current);
73-
} else if (!disableHeadingFocus) {
74-
// Follow the default NHSUK Frontend behaviour, but go about it in a safer way.
75-
// https://github.com/nhsuk/nhsuk-frontend/blob/master/packages/components/skip-link/skip-link.js
76-
if (firstHeadingElement) focusElement(firstHeadingElement);
77-
}
78-
if (onClick) {
79-
event.persist();
80-
onClick(event);
81-
}
82-
};
19+
setInstance(new SkipLink(moduleRef.current));
20+
}, [moduleRef, instance]);
8321

8422
return (
8523
<a
86-
className={classNames('nhsuk-skip-link', className)}
87-
onClick={focusTarget}
8824
href={href}
89-
tabIndex={tabIndex}
25+
className={classNames('nhsuk-skip-link', className)}
26+
data-module="nhsuk-skip-link"
27+
ref={moduleRef}
9028
{...rest}
9129
>
9230
{children}
Lines changed: 13 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,41 @@
1-
import React, { createRef } from 'react';
2-
import { fireEvent, render } from '@testing-library/react';
1+
import React from 'react';
2+
import { fireEvent, render, waitFor } from '@testing-library/react';
33
import SkipLink from '../';
44

55
function SkipLinkTestApp(): JSX.Element {
6-
const mainContentRef = createRef<HTMLElement>();
7-
86
return (
97
<>
10-
<SkipLink focusTargetRef={mainContentRef} />
11-
<main className="nhsuk-main-wrapper" id="maincontent" role="main" ref={mainContentRef}>
12-
<div className="nhsuk-grid-row"></div>
13-
</main>
8+
<SkipLink />
9+
<div className="nhsuk-width-container">
10+
<main className="nhsuk-main-wrapper" id="maincontent"></main>
11+
</div>
1412
</>
1513
);
1614
}
1715

1816
describe('SkipLink', () => {
1917
it('matches snapshot', () => {
20-
const { container } = render(<SkipLink />);
18+
const { container } = render(<SkipLinkTestApp />);
2119

2220
expect(container).toMatchSnapshot('SkipLink');
2321
});
2422

25-
it('sets the href to #maincontent by default and focuses the first heading', () => {
26-
const { container } = render(
27-
<>
28-
<SkipLink />
29-
<h1 id="heading">Heading</h1>
30-
</>,
31-
);
32-
33-
const headingEl = container.querySelector('#heading') as HTMLElement;
34-
const focusSpy = jest.spyOn(headingEl, 'focus');
23+
it('sets the href to #maincontent by default', () => {
24+
const { container } = render(<SkipLinkTestApp />);
3525

3626
const skipLinkEl = container.querySelector('.nhsuk-skip-link')!;
3727

3828
expect(skipLinkEl.getAttribute('href')).toBe('#maincontent');
39-
40-
fireEvent.click(skipLinkEl);
41-
42-
expect(focusSpy).toHaveBeenCalled();
4329
});
4430

45-
it('Does not focus the first heading if disableHeadingFocus is set', () => {
46-
const { container } = render(
47-
<>
48-
<SkipLink disableHeadingFocus />
49-
<h1 id="heading">Heading</h1>
50-
</>,
51-
);
52-
53-
const headingEl = container.querySelector('#heading') as HTMLElement;
54-
const focusSpy = jest.spyOn(headingEl, 'focus');
31+
it('focuses the main content when clicked', async () => {
32+
const { container } = render(<SkipLinkTestApp />);
5533

34+
const mainEl = container.querySelector('main');
5635
const skipLinkEl = container.querySelector('.nhsuk-skip-link')!;
5736

58-
expect(skipLinkEl.getAttribute('href')).toBe('#maincontent');
59-
60-
fireEvent.click(skipLinkEl);
61-
62-
expect(focusSpy).not.toHaveBeenCalled();
63-
});
64-
65-
it('calls onClick callback when clicked', () => {
66-
const onClick = jest.fn();
67-
const { container } = render(<SkipLink onClick={onClick} />);
68-
69-
const skipLinkEl = container.querySelector('.nhsuk-skip-link')!;
7037
fireEvent.click(skipLinkEl);
7138

72-
expect(onClick).toHaveBeenCalled();
73-
});
74-
75-
it('Focuses the main content when clicked', () => {
76-
const { container } = render(<SkipLinkTestApp />);
77-
78-
const mainContent = container.querySelector('main#maincontent') as HTMLElement;
79-
80-
const focusSpy = jest.spyOn(mainContent, 'focus');
81-
82-
expect(focusSpy).not.toHaveBeenCalled();
83-
84-
const skipButton = container.querySelector('.nhsuk-skip-link')!;
85-
86-
fireEvent.click(skipButton);
87-
88-
expect(focusSpy).toHaveBeenCalled();
39+
await waitFor(() => expect(document.activeElement).toBe(mainEl));
8940
});
9041
});

src/components/navigation/skip-link/__tests__/__snapshots__/SkipLink.test.tsx.snap

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ exports[`SkipLink matches snapshot: SkipLink 1`] = `
44
<div>
55
<a
66
class="nhsuk-skip-link"
7+
data-module="nhsuk-skip-link"
8+
data-nhsuk-skip-link-init=""
79
href="#maincontent"
8-
tabindex="0"
910
>
1011
Skip to main content
1112
</a>
13+
<div
14+
class="nhsuk-width-container"
15+
>
16+
<main
17+
class="nhsuk-main-wrapper"
18+
id="maincontent"
19+
/>
20+
</div>
1221
</div>
1322
`;

stories/Navigation/SkipLink.stories.tsx

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,36 +33,14 @@ const meta: Meta<typeof SkipLink> = {
3333
<CodeText>tab</CodeText>
3434
to show the SkipLink
3535
</HintText>
36-
<SkipLink
37-
disableDefaultBehaviour={args.disableDefaultBehaviour}
38-
disableHeadingFocus={args.disableHeadingFocus}
39-
/>
36+
<SkipLink />
4037
<h1>Page heading</h1>
41-
<div id="#maincontent">This is the main content</div>
38+
<div id="maincontent">This is the main content</div>
4239
</>
4340
),
4441
};
4542

4643
export default meta;
4744
type Story = StoryObj<typeof SkipLink>;
4845

49-
export const Standard: Story = {
50-
args: {
51-
disableDefaultBehaviour: false,
52-
disableHeadingFocus: false,
53-
},
54-
};
55-
56-
export const SkipLinkWithDefaultBehaviourDisabled: Story = {
57-
args: {
58-
disableDefaultBehaviour: true,
59-
disableHeadingFocus: false,
60-
},
61-
};
62-
63-
export const SkipLinkWithHeadingFocusDisabled: Story = {
64-
args: {
65-
disableDefaultBehaviour: false,
66-
disableHeadingFocus: true,
67-
},
68-
};
46+
export const Standard: Story = {};

0 commit comments

Comments
 (0)