Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set focus to skip link target to improve screen reader announcements #2450

Merged
merged 4 commits into from
Dec 7, 2021

Conversation

hannalaakso
Copy link
Member

When user activates the skip link, Mac VoiceOver doesn't announce the main content or continue reading
from it as reported in #2187.

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, and follows the approach recommended by @selfthinker.

Results from screen reader testing

This solves the issue in Mac VoiceOver: when the user activates the skip link, VoiceOver now announces the main element. If the user then tries to read the next element, VoiceOver now correctly continues reading from the main content. There is also a smaller 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, " After double-tapping to follow link: "main, Press release, "

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

See #2187 (comment) for more details.

Closes #2187

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2450 November 29, 2021 18:28 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2450 November 30, 2021 09:46 Inactive
@36degrees 36degrees self-requested a review December 1, 2021 13:43
src/govuk/components/skip-link/skip-link.test.js Outdated Show resolved Hide resolved
src/govuk/components/skip-link/skip-link.test.js Outdated Show resolved Hide resolved
Comment on lines 56 to 63
$content.addEventListener('blur', function () {
$content.removeAttribute('tabindex')
$content.classList.remove('govuk-skip-link-focused-element')
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will set up a new event listener every time the user uses the skip link, so you'll end up with multiple event listeners bound that all do the same thing.

I think if it was a named function (for example, if it was a separate function in the module) it would only ever be added once.

An alternative approach would be for it to unbind itself?

Suggested change
$content.addEventListener('blur', function () {
$content.removeAttribute('tabindex')
$content.classList.remove('govuk-skip-link-focused-element')
})
var handler = $content.addEventListener('blur', function () {
$content.removeAttribute('tabindex')
$content.classList.remove('govuk-skip-link-focused-element')
$content.removeEventListener('blur', handler)
})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is true, it's valid to use an anonymous function with an event listener: https://developer.mozilla.org/en-US/docs/Web/API/EventListener#javascript I don't think a named function would change the actual behaviour in any way. But I'm going to leave this open, we can compare notes on Monday 👀

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, I've had another look and this was actually a good catch 💯 I'm now binding the function on the event listener which stops the event listener being created more than once.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still isn't working as expected - I want to refactor the code so that we set the event listener at the point of initialisation but going to pick this up later so that we can get the actual change to the component behaviour tested as part of the pre-release.

Comment on lines 43 to 53
var contentId = this.getFragmentFromUrl($target.href)
var $content = document.getElementById(contentId)
if (!$content) {
return false
}

if (!$content.getAttribute('tabindex')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this easier to follow, what about making the variable names consistent with the method name and refer to the thing we want to focus as the target, rather than content?

Suggested change
var contentId = this.getFragmentFromUrl($target.href)
var $content = document.getElementById(contentId)
if (!$content) {
return false
}
if (!$content.getAttribute('tabindex')) {
var targetId = this.getFragmentFromUrl($target.href)
var $target = document.getElementById(contentId)
if (!$target) {
return false
}
if (!$target.getAttribute('tabindex')) {

etc…

Copy link
Member Author

@hannalaakso hannalaakso Dec 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had tried to find a better name for the variable but we're constrained by the existing naming conventions here. $target is already defined, it contains the skip link element. The function duplicates logic in the error summary (ideally we should be extracting this to be a reusable function but as just discussed in a different context doing that will be part a bigger piece of work) so I was loath to rename the function or its parameter in order to keep that linkage.

In the error summary we call the elements to focus by the HMTL element name ($input and $legendOrLabel) but $main in this context felt too specific, so that's why I settled on $content.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point – but I think points to a bigger naming issue (which is absolutely already present in the error summary) and which is potentially even more confusing… target means two different things in two different parts of the code. Within the context of handleClick target refers to the 'event target' (the thing that was clicked), but focusTarget at least suggests that the target is the element that we want to focus.

Base on the function's signature (focusTarget($target)) you would definitely expect it to focus whatever $target is. And that's also what the doc block says it does! But it doesn't do that – it focuses whatever $target links to. The target of the $target, I guess…?

I think this is worth trying to fix rather than introducing the issue to another component.

Copy link
Member Author

@hannalaakso hannalaakso Dec 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've amended the wording, just for this component for now, to distinguish the page content that the skiplink targets from the event target of the skiplink. Have a look and let me know if you think this makes the distinction clearer.

src/govuk/components/skip-link/skip-link.js Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2450 December 2, 2021 19:42 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2450 December 2, 2021 19:57 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2450 December 2, 2021 19:59 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2450 December 2, 2021 20:15 Inactive
CHANGELOG.md Outdated Show resolved Hide resolved
Copy link
Contributor

@EoinShaughnessy EoinShaughnessy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hannalaakso Changelog update is great! Just suggesting one word-change, but otherwise this looks good to go. 👍🏻

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2450 December 6, 2021 11:19 Inactive
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-2450 December 6, 2021 18:41 Inactive
Copy link
Contributor

@vanitabarrett vanitabarrett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR looks really good, thanks @hannalaakso !

As discussed in dev catchup, this PR is good apart from one issue Ollie noticed where the event listener is added each time a user interacts with the skip link. It feels unlikely that this would happen or cause many problems, but it's something we should fix.

However, we want to include this change in the pre-release so I'm going to approve it for now so it can be merged. We'll revisit this to fix the multiple event listeners issue before the actual v4.0.0 release.

hannalaakso and others added 4 commits December 7, 2021 11:20
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).

See #2187 (comment) for more details and the full testing results.
Add basic page content and initialise JavaScript for components so that the pages can be used for tests.
Co-authored-by: Eoin Shaughnessy <eoin.shaughnessy@digital.cabinet-office.gov.uk>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

After clicking "Skip to main content", focus isn't sent to the content so screenreader can't read from there
5 participants