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
diff --git a/src/components/AppHeader/AppHeader.vue b/src/components/AppHeader/AppHeader.vue
index 7e1a100..0e8d3db 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 @@
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 e766d38..c979759 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 @@
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 0000000..ae3e8e0
--- /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 c2023df..d5b4c3d 100644
--- a/src/layouts/AppLayout.vue
+++ b/src/layouts/AppLayout.vue
@@ -15,6 +15,7 @@
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 @@
PageContainer,
ButtonBackToTop,
},
+ mixins: [SetFocusMixin],
data() {
return {
routerKey: 0,
@@ -31,20 +33,8 @@
},
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 3e5c91e..02d99b1 100644
--- a/tests/unit/__snapshots__/AppHeader.spec.js.snap
+++ b/tests/unit/__snapshots__/AppHeader.spec.js.snap
@@ -8,7 +8,6 @@
<a
class="link-skip-nav btn btn-light"
href="#main-content"
- role="link"
>
appHeader.skipToContent