Skip to content

Commit ca85261

Browse files
committed
feat(a11y): Content outside inert for assistive technology users
1 parent 8943820 commit ca85261

File tree

1 file changed

+56
-19
lines changed

1 file changed

+56
-19
lines changed

src/FocusLoop.vue

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
<template>
2-
<div class="vue-focus-loop">
2+
<div
3+
v-if="isVisible"
4+
ref="VuefocusLoopContainer"
5+
class="vue-focus-loop"
6+
>
37
<div
48
:tabindex="getTabindex"
9+
aria-hidden="true"
510
@focus="handleFocusStart"
611
/>
712
<div ref="focusLoop">
813
<slot />
914
</div>
1015
<div
1116
:tabindex="getTabindex"
17+
aria-hidden="true"
1218
@focus="handleFocusEnd"
1319
/>
1420
</div>
@@ -24,6 +30,8 @@ const focusableElementsSelector = [
2430
'[contenteditable]:not([contenteditable="false"])'
2531
].join(',')
2632
33+
let ariaHiddenElements = []
34+
2735
export default {
2836
name: 'FocusLoop',
2937
@@ -55,41 +63,70 @@ export default {
5563
},
5664
5765
watch: {
58-
isVisible (val) {
59-
this.managePrevFocusElement(val)
60-
this.focusFirst(val)
61-
}
66+
isVisible: 'init',
67+
disabled: 'init'
6268
},
6369
6470
mounted () {
65-
this.managePrevFocusElement(this.isVisible)
66-
this.focusFirst(this.isVisible)
67-
},
68-
69-
beforeDestroy () {
70-
this.managePrevFocusElement(false)
71+
this.init()
7172
},
7273
7374
methods: {
74-
managePrevFocusElement (visible) {
75-
if (!visible && window.vflPrevFocusedElement) {
75+
init () {
76+
this.$nextTick(() => {
77+
const active = this.isVisible && !this.disabled
78+
!this.disabled && this.focusFirst(active && this.autoFocus)
79+
this.managePrevFocusElement(active)
80+
this.lockForSwipeScreenReader(active)
81+
if (!active) {
82+
ariaHiddenElements = []
83+
}
84+
})
85+
},
86+
87+
managePrevFocusElement (active) {
88+
if (!active && window.vflPrevFocusedElement) {
7689
return window.vflPrevFocusedElement.focus()
7790
}
7891
window.vflPrevFocusedElement = document.activeElement
7992
},
8093
94+
getElementsToAriaHidden (focusLoopContainer) {
95+
function getElements (element) {
96+
const children = Array.from(element.children)
97+
children.forEach(el => {
98+
if (el === focusLoopContainer) return
99+
if (!el.contains(focusLoopContainer)) {
100+
ariaHiddenElements.push(el)
101+
return
102+
}
103+
getElements(el)
104+
})
105+
}
106+
getElements(document.body)
107+
},
108+
109+
lockForSwipeScreenReader (active = true) {
110+
if (active) this.getElementsToAriaHidden(this.$refs.VuefocusLoopContainer)
111+
ariaHiddenElements.forEach(el => {
112+
if (['SCRIPT', 'STYLE'].includes(el.nodeName) || el.hasAttribute('aria-live')) return
113+
el.setAttribute('aria-hidden', active.toString())
114+
})
115+
},
116+
117+
focusFirst (isAutoFocus) {
118+
if (isAutoFocus) {
119+
const elements = this.getFocusableElements()
120+
if (elements.length) setTimeout(() => elements[0].focus(), 200)
121+
}
122+
},
123+
81124
getFocusableElements () {
82125
const focusableElements = this.$refs.focusLoop.querySelectorAll(focusableElementsSelector)
83126
if (focusableElements && focusableElements.length) return focusableElements
84127
return []
85128
},
86129
87-
focusFirst (visible) {
88-
if (!visible && !this.autoFocus) return
89-
const elements = this.getFocusableElements()
90-
if (elements.length) setTimeout(() => elements[0].focus(), 200)
91-
},
92-
93130
handleFocusStart () {
94131
const elements = this.getFocusableElements()
95132
if (elements.length) {

0 commit comments

Comments
 (0)