Skip to content

Commit

Permalink
Focus skip link target to fix Mac VoiceOver announcements
Browse files Browse the repository at this point in the history
When user activates the skip link, Mac VoiceOver currently does not announce the main content or continue reading
from it.

To improve the experience for Mac VoiceOver users:
- make the main content element focusable by adding a `tabindex` attribute
- move focus to it programmatically
- override the native focus outline to none whilst `tabindex` is present
- remove the `tabindex` attribute and the style override on blur

This follows the pattern we already use in the error summary and notification banner components.

There also seems to be an improvement to the announcements on JAWS (both with Chrome and IE11): JAWS now announces it's on the main region, before it starts reading the main content.

Behaviour | Announces skip link when it's navigated to | Announces main has been navigated to | Reads out content of main
-- | -- | -- | --
JAWS 2020 / Chrome | same page link skip to main content | enter **main** region **main** region page has 4 regions and 8 headings `<main content>` | enter **main** region **main** region page has 4 regions and 8 headings `<main content>`
JAWS 2020 / IE 11 | skip to main content same page link | enter **main** region page has 4 regions and 8 headings `<main content>` | enter main region page has 4 regions and 8 headings `<main content>`
NVDA 2021.1/ Firefox 90 | skip to main content link | main landmark  `<main content>` | main landmark  `<main content>`
Voiceover / Safari (macOS Mojave) | skip to main content, link | **main you are currently on a text area** | **`<main content>`**
Voiceover / Safari (iOS 15) | Skip to main content, in-page link | After double-tapping to follow link: "Press release, main landmark" | After double-tapping to follow link: "Press release, main landmark"
Talkback / Chrome 96 (Android 11) | skip to main content, link | After double-tapping to follow link: "main, Press release, <main content>" | After double-tapping to follow link: "main, Press release, <main content>"

(Announcements that have changed from the live version are in **bold** ^)

See #2187 (comment) for more details
  • Loading branch information
hannalaakso committed Nov 29, 2021
1 parent 3f111f5 commit 0a3d8b3
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 0 deletions.
14 changes: 14 additions & 0 deletions src/govuk/components/skip-link/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,18 @@
}
}
}

.govuk-skip-link-focused-element {
&:focus {
// Remove the native visible focus indicator when the element is programmatically focused.
//
// We set the focus on the main content element when the skip link is activated to improve Mac
// VoiceOver announcements. However, we don't display the visible focus indicator on the main
// content element because the user cannot interact with it, and the main content element is
// also predictably the first element after the 'Skip to main content' component is activated.
//
// A related discussion: https://github.com/w3c/wcag/issues/1001
outline: none;
}
}
}
64 changes: 64 additions & 0 deletions src/govuk/components/skip-link/skip-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,70 @@ SkipLink.prototype.init = function () {
if (!$module) {
return
}

$module.addEventListener('click', this.handleClick.bind(this))
}

/**
* Click event handler
*
* @param {MouseEvent} event - Click event
*/
SkipLink.prototype.handleClick = function (event) {
var target = event.target

if (this.focusTarget(target)) {
event.preventDefault()
}
}

/**
* Focus the target element
*
* @param {HTMLElement} $target - Event target
* @returns {boolean} True if the target was able to be focussed
*/
SkipLink.prototype.focusTarget = function ($target) {
// If the element that was clicked does not have a href, return early
if ($target.href === false) {
return false
}

var contentId = this.getFragmentFromUrl($target.href)
var $content = document.getElementById(contentId)
if (!$content) {
return false
}

// Set the content tabindex to -1 so it can be focused with JavaScript.
$content.setAttribute('tabindex', '-1')
$content.classList.add('govuk-skip-link-focused-element')

$content.focus()

// Remove the tabindex attribute on blur because the content only needs to be focusable until it
// has received programmatic focus and a screen reader has announced it.
$content.addEventListener('blur', function () {
$content.removeAttribute('tabindex')
$content.classList.remove('govuk-skip-link-focused-element')
})
}

/**
* Get fragment from URL
*
* Extract the fragment (everything after the hash) from a URL, but not including
* the hash.
*
* @param {string} url - URL
* @returns {string} Fragment from URL, without the hash
*/
SkipLink.prototype.getFragmentFromUrl = function (url) {
if (url.indexOf('#') === -1) {
return false
}

return url.split('#').pop()
}

export default SkipLink
50 changes: 50 additions & 0 deletions src/govuk/components/skip-link/skip-link.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-env jest */

const configPaths = require('../../../../config/paths.json')
const PORT = configPaths.ports.test

const baseUrl = 'http://localhost:' + PORT

describe('/examples/template-default', () => {
describe('skip link', () => {
beforeAll(async () => {
await page.goto(`${baseUrl}/examples/template-default`, { waitUntil: 'load' })
await page.keyboard.press('Tab')
await page.click('.govuk-skip-link')
})

it('focuses the target element', async () => {
const activeElement = await page.evaluate(() => document.activeElement.id)

expect(activeElement).toBe('main-content')
})

it('adds the tabindex attribute to the target', async () => {
const tabindex = await page.$eval('.govuk-main-wrapper', el => el.getAttribute('tabindex'))

expect(tabindex).toBe('-1')
})

it('adds the class for removing the native focus style to the target', async () => {
const cssClass = await page.$eval('.govuk-main-wrapper', el => el.getAttribute('class'))

expect(cssClass).toContain('govuk-skip-link-focused-element')
})

it('removes the tabindex attribute from the target on blur', async () => {
await page.$eval('.govuk-main-wrapper', el => el.blur())

const tabindex = await page.$eval('.govuk-main-wrapper', el => el.getAttribute('tabindex'))

expect(tabindex).toBeNull()
})

it('removes the class for removing the native focus style from the target on blur', async () => {
await page.$eval('.govuk-main-wrapper', el => el.blur())

const cssClass = await page.$eval('.govuk-main-wrapper', el => el.getAttribute('class'))

expect(cssClass).not.toContain('govuk-skip-link-focused-element')
})
})
})
9 changes: 9 additions & 0 deletions src/govuk/components/skip-link/template.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,14 @@ describe('Skip link', () => {
expect($component.attr('data-test')).toEqual('attribute')
expect($component.attr('aria-label')).toEqual('Skip to content')
})

it('renders a data-module attribute to initialise JavaScript', () => {
const $ = render('skip-link', examples.default)

// const $ = render('notification-banner', examples.default)
const $component = $('.govuk-skip-link')

expect($component.attr('data-module')).toEqual('govuk-skip-link')
})
})
})

0 comments on commit 0a3d8b3

Please sign in to comment.