Skip to content

Commit a5169ab

Browse files
Focus skip link target to improve screen reader announcements
Based on alphagov/govuk-frontend#2450
1 parent bcd7eed commit a5169ab

4 files changed

Lines changed: 89 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ We've made fixes to NHS.UK frontend in the following pull requests:
8383
- [#1148: Fix Tabs component in Safari < 14 and Internet Explorer 11](https://github.com/nhsuk/nhsuk-frontend/pull/1148)
8484
- [#908: Updating secondary, reverse and warning buttons to use their hover variable rather than darkening the base colour](https://github.com/nhsuk/nhsuk-frontend/pull/908)
8585
- [#1169: Update visually hidden behaviour to match GOV.UK Frontend](https://github.com/nhsuk/nhsuk-frontend/pull/1169)
86+
- [#1173: Focus skip link target to improve screen reader announcements](https://github.com/nhsuk/nhsuk-frontend/pull/1173)
8687

8788
## 9.3.0 - 13 February 2025
8889

packages/components/skip-link/_skip-link.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,18 @@
1313
padding: nhsuk-spacing(2);
1414
@include visually-hidden-focusable; // [1]
1515
}
16+
17+
.nhsuk-skip-link-focused-element {
18+
&:focus {
19+
// Remove the native visible focus indicator when the element is
20+
// programmatically focused.
21+
//
22+
// We set the focus on the linked element (this is usually the <main>
23+
// element) when the skip link is activated to improve screen reader
24+
// announcements. However, we remove the visible focus indicator from the
25+
// linked element because the user cannot interact with it.
26+
//
27+
// A related discussion: https://github.com/w3c/wcag/issues/1001
28+
outline: none;
29+
}
30+
}
Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { setFocus } from '../../common.js'
2+
13
/*
24
* NHS.UK skip link.
35
*
@@ -6,23 +8,37 @@
68
*/
79

810
export default () => {
9-
// Assign required DOM elements
10-
const heading = document.querySelector('h1')
11-
const skipLink = document.querySelector('.nhsuk-skip-link')
11+
const $skipLink = document.querySelector('.nhsuk-skip-link')
1212

13-
const addEvents = () => {
14-
// Add tabindex = -1 and apply focus to heading on skip link click
15-
skipLink.addEventListener('click', (event) => {
16-
event.preventDefault()
17-
heading.setAttribute('tabIndex', '-1')
18-
heading.focus()
19-
})
20-
// Remove tabindex from heading on blur
21-
heading.addEventListener('blur', (event) => {
22-
event.preventDefault()
23-
heading.removeAttribute('tabIndex')
24-
})
13+
// Check for skip link
14+
if (!$skipLink || !($skipLink instanceof HTMLAnchorElement)) {
15+
return
2516
}
2617

27-
if (heading && skipLink) addEvents()
18+
const linkedElementId = $skipLink.hash.split('#').pop()
19+
const $linkedElement = linkedElementId
20+
? document.getElementById(linkedElementId)
21+
: null
22+
23+
// Check for linked element
24+
if (!$linkedElement) {
25+
return
26+
}
27+
28+
/**
29+
* Focus the linked element on click
30+
*
31+
* Adds a helper CSS class to hide native focus styles,
32+
* but removes it on blur to restore native focus styles
33+
*/
34+
$skipLink.addEventListener('click', () =>
35+
setFocus($linkedElement, {
36+
onBeforeFocus() {
37+
$linkedElement.classList.add('nhsuk-skip-link-focused-element')
38+
},
39+
onBlur() {
40+
$linkedElement.classList.remove('nhsuk-skip-link-focused-element')
41+
}
42+
})
43+
)
2844
}

tests/integration/jsdom/skip-link.test.js

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,82 @@ import SkipLink from '../../../packages/components/skip-link/skip-link.js'
33
// Mock HTML
44
const skipLinkHtml =
55
'<a class="nhsuk-skip-link" href="#maincontent">Skip to main content</a>'
6-
const headingHtml = '<h1>Test Heading</h1>'
6+
const mainHtml = '<main class="nhsuk-main-wrapper" id="maincontent"></main>'
77

8-
// DOM Elements to be set
9-
let skipLink
10-
let heading
8+
/** @type {HTMLElement | null} */
9+
let main = null
10+
11+
/** @type {HTMLAnchorElement | null} */
12+
let skipLink = null
1113

1214
// Helper to set DOM Elements
1315
const initTest = (html = '') => {
1416
document.body.innerHTML = html
15-
heading = document.querySelector('h1')
1617
skipLink = document.querySelector('.nhsuk-skip-link')
18+
main = document.querySelector('main')
1719
SkipLink()
1820
}
1921

2022
describe('NHS.UK skiplink', () => {
2123
describe('Does not throw an error', () => {
2224
it('if no skiplink exists', () => {
23-
initTest(headingHtml)
24-
expect(heading).toBeDefined()
25+
initTest(mainHtml)
2526
expect(skipLink).toBeNull()
27+
expect(main).toBeDefined()
2628
})
2729

28-
it('if no heading exists', () => {
30+
it('if no main content exists', () => {
2931
initTest(skipLinkHtml)
3032
expect(skipLink).toBeDefined()
31-
expect(heading).toBeNull()
33+
expect(main).toBeNull()
3234
})
3335

34-
it('if no skiplink or heading exists', () => {
36+
it('if no skiplink or main content exists', () => {
3537
initTest()
36-
expect(heading).toBeNull()
3738
expect(skipLink).toBeNull()
39+
expect(main).toBeNull()
3840
})
3941
})
4042

41-
describe('Focuses the heading on skiplink click', () => {
42-
it('if skiplink and heading elements exist', () => {
43-
initTest(skipLinkHtml + headingHtml)
44-
expect(skipLink).toBeDefined()
45-
expect(heading).toBeDefined()
43+
describe('Focuses main content on skiplink click', () => {
44+
it('if skiplink and main element exist', () => {
45+
initTest(skipLinkHtml + mainHtml)
46+
47+
// Main content not focused
48+
expect(main).not.toEqual(document.activeElement)
49+
expect(main.getAttribute('tabIndex')).toBeNull()
50+
expect(main.classList.value).not.toContain(
51+
'nhsuk-skip-link-focused-element'
52+
)
4653

4754
skipLink.click()
4855

49-
expect(heading.getAttribute('tabIndex')).toBe('-1')
50-
expect(document.activeElement).toEqual(heading)
56+
// Main content focused
57+
expect(main).toEqual(document.activeElement)
58+
expect(main.getAttribute('tabIndex')).toBe('-1')
59+
expect(main.classList.value).toContain('nhsuk-skip-link-focused-element')
5160
})
5261
})
5362

54-
describe('Unfocuses the heading on blur', () => {
55-
it('if skiplink and heading elements exist', () => {
56-
initTest(skipLinkHtml + headingHtml)
57-
expect(skipLink).toBeDefined()
58-
expect(heading).toBeDefined()
63+
describe('Unfocuses main content on blur', () => {
64+
it('if skiplink and main element exist', () => {
65+
initTest(skipLinkHtml + mainHtml)
5966

6067
skipLink.click()
6168

62-
expect(heading.getAttribute('tabIndex')).toBe('-1')
63-
expect(document.activeElement).toEqual(heading)
69+
// Main content focused
70+
expect(main).toEqual(document.activeElement)
71+
expect(main.getAttribute('tabIndex')).toBe('-1')
72+
expect(main.classList.value).toContain('nhsuk-skip-link-focused-element')
6473

65-
heading.blur()
74+
main.blur()
6675

67-
expect(heading.getAttribute('tabIndex')).toBeNull()
68-
expect(document.activeElement).not.toEqual(heading)
76+
// Main content not focused
77+
expect(main).not.toEqual(document.activeElement)
78+
expect(main.getAttribute('tabIndex')).toBeNull()
79+
expect(main.classList.value).not.toContain(
80+
'nhsuk-skip-link-focused-element'
81+
)
6982
})
7083
})
71-
72-
describe('Is initialised by nhsuk.js', () => {})
7384
})

0 commit comments

Comments
 (0)