diff options
author | Derick Montague <derick.montague@ibm.com> | 2021-02-16 18:47:03 +0300 |
---|---|---|
committer | Derick Montague <derick.montague@ibm.com> | 2021-02-17 00:27:55 +0300 |
commit | 8026797012a0cbdce357a5c47304134fec9eca73 (patch) | |
tree | 8b10dc9cb962d56e8b55c36a955d00a137762be9 | |
parent | 2f469b9262e0c847746447948f055692b3436fcc (diff) | |
download | webui-vue-8026797012a0cbdce357a5c47304134fec9eca73.tar.xz |
Fix skip link 404 error on refresh bug
Problem:
When a user uses the skip link anchor to skip the navigation, the
route was being changed to /#main-content. This route does not
exist. If a user were to manually refresh the page, it would
return a 404. This link is critical to meet accessibility
guidelines and is needed by users that navigate with a keyboard.
The challenge is that we need to mirror a full page refresh on all
route changes, so we set focus on the app-header element on each route
change. When we click the skip to navigation link, there should not be
a route change. All we need is to set focus on the <main> element so
that the user can tab to the first tabbable element in the main content
section.
Solution:
- Use a native <a> element with an attached click event handler
- Prevent the default action of adding the hash to the URL
- Create a global mixin to reuse for route changes and skip link
activation
- Emit an event that can be listened for to call the global mixin
Signed-off-by: Derick Montague <derick.montague@ibm.com>
Change-Id: I4c2301b02f608eeb376ed2d1bd809f3d5c1bf545
-rw-r--r-- | src/components/AppHeader/AppHeader.vue | 10 | ||||
-rw-r--r-- | src/components/Global/PageContainer.vue | 14 | ||||
-rw-r--r-- | src/components/Mixins/SetFocusMixin.js | 12 | ||||
-rw-r--r-- | src/layouts/AppLayout.vue | 16 | ||||
-rw-r--r-- | tests/unit/__snapshots__/AppHeader.spec.js.snap | 1 |
5 files changed, 37 insertions, 16 deletions
diff --git a/src/components/AppHeader/AppHeader.vue b/src/components/AppHeader/AppHeader.vue index 7e1a1006..0e8d3db4 100644 --- a/src/components/AppHeader/AppHeader.vue +++ b/src/components/AppHeader/AppHeader.vue @@ -1,7 +1,11 @@ <template> <div> <header id="page-header"> - <a role="link" class="link-skip-nav btn btn-light" href="#main-content"> + <a + class="link-skip-nav btn btn-light" + href="#main-content" + @click="setFocus" + > {{ $t('appHeader.skipToContent') }} </a> @@ -207,6 +211,10 @@ export default { toggleNavigation() { this.$root.$emit('toggle-navigation'); }, + setFocus(event) { + event.preventDefault(); + this.$root.$emit('skip-navigation'); + }, }, }; </script> diff --git a/src/components/Global/PageContainer.vue b/src/components/Global/PageContainer.vue index e766d38d..c979759b 100644 --- a/src/components/Global/PageContainer.vue +++ b/src/components/Global/PageContainer.vue @@ -5,11 +5,17 @@ </template> <script> +import SetFocusMixin from '@/components/Mixins/SetFocusMixin'; export default { name: 'PageContainer', + mixins: [SetFocusMixin], + created() { + this.$root.$on('skip-navigation', () => { + this.setFocus(this.$el); + }); + }, }; </script> - <style lang="scss" scoped> main { width: 100%; @@ -18,6 +24,12 @@ main { padding-bottom: $spacer * 3; padding-left: $spacer; padding-right: $spacer; + + &:focus-visible { + box-shadow: inset 0 0 0 2px theme-color('primary'); + outline: none; + } + @include media-breakpoint-up($responsive-layout-bp) { padding-left: $spacer * 2; } diff --git a/src/components/Mixins/SetFocusMixin.js b/src/components/Mixins/SetFocusMixin.js new file mode 100644 index 00000000..ae3e8e0f --- /dev/null +++ b/src/components/Mixins/SetFocusMixin.js @@ -0,0 +1,12 @@ +const setFocusMixin = { + methods: { + setFocus(element) { + element.setAttribute('tabindex', '-1'); + element.focus(); + // Reason: https://axesslab.com/skip-links/#update-3-a-comment-from-gov-uk + element.removeAttribute('tabindex'); + }, + }, +}; + +export default setFocusMixin; diff --git a/src/layouts/AppLayout.vue b/src/layouts/AppLayout.vue index c2023dfb..d5b4c3df 100644 --- a/src/layouts/AppLayout.vue +++ b/src/layouts/AppLayout.vue @@ -15,6 +15,7 @@ import AppHeader from '@/components/AppHeader'; import AppNavigation from '@/components/AppNavigation'; import PageContainer from '@/components/Global/PageContainer'; import ButtonBackToTop from '@/components/Global/ButtonBackToTop'; +import SetFocusMixin from '@/components/Mixins/SetFocusMixin'; export default { name: 'App', @@ -24,6 +25,7 @@ export default { PageContainer, ButtonBackToTop, }, + mixins: [SetFocusMixin], data() { return { routerKey: 0, @@ -31,20 +33,8 @@ export default { }, watch: { $route: function () { - // $nextTick = DOM updated this.$nextTick(function () { - // Get the focusTarget DOM element - let focusTarget = this.$refs.focusTarget.$el; - - // Make focustarget programmatically focussable - focusTarget.setAttribute('tabindex', '-1'); - - // Focus element - focusTarget.focus(); - - // Remove tabindex from focustarget - // Reason: https://axesslab.com/skip-links/#update-3-a-comment-from-gov-uk - focusTarget.removeAttribute('tabindex'); + this.setFocus(this.$refs.focusTarget.$el); }); }, }, diff --git a/tests/unit/__snapshots__/AppHeader.spec.js.snap b/tests/unit/__snapshots__/AppHeader.spec.js.snap index 3e5c91eb..02d99b16 100644 --- a/tests/unit/__snapshots__/AppHeader.spec.js.snap +++ b/tests/unit/__snapshots__/AppHeader.spec.js.snap @@ -8,7 +8,6 @@ exports[`AppHeader.vue should render correctly 1`] = ` <a class="link-skip-nav btn btn-light" href="#main-content" - role="link" > appHeader.skipToContent |