Skip to content

Commit 99abfdf

Browse files
committed
feat: Add FocusLoop
1 parent cb7b989 commit 99abfdf

File tree

2 files changed

+116
-23
lines changed

2 files changed

+116
-23
lines changed

Diff for: src/components/FocusLoop.vue

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<template>
2+
<div class="vue-focus-loop">
3+
<div
4+
:tabindex="getTabindex"
5+
@focus="handleFocusStart"
6+
/>
7+
<div ref="focusLoopRef">
8+
<slot />
9+
</div>
10+
<div
11+
:tabindex="getTabindex"
12+
@focus="handleFocusEnd"
13+
/>
14+
</div>
15+
</template>
16+
17+
<script>
18+
import { ref, computed, watch, onMounted } from 'vue'
19+
20+
const focusableElementsSelector = [
21+
...['input', 'select', 'button', 'textarea'].map(field => `${field}:not([disabled])`),
22+
'a[href]',
23+
'video[controls]',
24+
'audio[controls]',
25+
'[tabindex]:not([tabindex^="-"])',
26+
'[contenteditable]:not([contenteditable="false"])'
27+
].join(',')
28+
29+
export default {
30+
name: 'FocusLoop',
31+
32+
props: {
33+
disabled: {
34+
type: Boolean,
35+
default: false
36+
},
37+
isVisible: {
38+
type: Boolean,
39+
default: false
40+
}
41+
},
42+
43+
disableAxeAudit: true,
44+
45+
setup (props) {
46+
const focusLoopRef = ref(null)
47+
const alreadyFocused = ref(false)
48+
const getTabindex = computed(() => props.disabled ? -1 : 0)
49+
50+
watch(() => props.isVisible, focusFirst)
51+
52+
onMounted(() => focusFirst(props.isVisible || true))
53+
54+
function focusFirst (visible) {
55+
if (visible) {
56+
const elements = getFocusableElements()
57+
if (elements.length) setTimeout(() => elements[0].focus(), 200)
58+
}
59+
}
60+
61+
function getFocusableElements () {
62+
const focusableElements = focusLoopRef.value.querySelectorAll(focusableElementsSelector)
63+
if (focusableElements && focusableElements.length) return focusableElements
64+
return []
65+
}
66+
67+
function handleFocusStart () {
68+
const elements = getFocusableElements()
69+
if (elements.length) {
70+
const index = alreadyFocused.value ? elements.length - 1 : 0
71+
alreadyFocused.value = true
72+
elements[index].focus()
73+
}
74+
}
75+
76+
function handleFocusEnd () {
77+
const elements = getFocusableElements()
78+
elements.length && elements[0].focus()
79+
}
80+
81+
return {
82+
focusLoopRef,
83+
getTabindex,
84+
handleFocusEnd,
85+
handleFocusStart
86+
}
87+
}
88+
}
89+
</script>

Diff for: src/components/Popup.vue

+27-23
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
<template>
2-
<div
3-
class="va-popup va-fixed va-flex va-flex-wrap va-justify-end va-antialiased va-text-color"
4-
style="z-index: 10000"
5-
role="region"
6-
:dir="dir"
7-
>
8-
<transition name="scale">
9-
<div
10-
v-show="isOpen"
11-
id="va-popup-box"
12-
class="va-popup__box va-w-full va-rounded-lg va-mb-4 va-shadow-lg va-bg-main va-border va-border-solid va-border-gray-200 va-overflow-hidden"
13-
>
14-
<PopupHeader />
15-
<PopupBody />
16-
<PopupFooter />
17-
</div>
18-
</transition>
19-
<PopupButton
20-
:popup-show="isOpen"
21-
:notifications="issuesFound"
22-
@toggle-popup="togglePopup"
23-
/>
24-
</div>
2+
<FocusLoop>
3+
<div
4+
class="va-popup va-fixed va-flex va-flex-wrap va-justify-end va-antialiased va-text-color"
5+
style="z-index: 10000"
6+
role="region"
7+
:dir="dir"
8+
>
9+
<transition name="scale">
10+
<div
11+
v-show="isOpen"
12+
id="va-popup-box"
13+
class="va-popup__box va-w-full va-rounded-lg va-mb-4 va-shadow-lg va-bg-main va-border va-border-solid va-border-gray-200 va-overflow-hidden"
14+
>
15+
<PopupHeader />
16+
<PopupBody />
17+
<PopupFooter />
18+
</div>
19+
</transition>
20+
<PopupButton
21+
:popup-show="isOpen"
22+
:notifications="issuesFound"
23+
@toggle-popup="togglePopup"
24+
/>
25+
</div>
26+
</FocusLoop>
2527
<Highlight />
2628
</template>
2729

@@ -35,13 +37,15 @@ import Highlight from '@/components/Highlight'
3537
import PopupButton from '@/components/PopupButton'
3638
import PopupHeader from '@/components/PopupHeader'
3739
import PopupFooter from '@/components/PopupFooter'
40+
import FocusLoop from '@/components/FocusLoop'
3841
3942
export default {
4043
name: 'Popup',
4144
4245
disableAxeAudit: true,
4346
4447
components: {
48+
FocusLoop,
4549
Highlight,
4650
PopupBody,
4751
PopupHeader,

0 commit comments

Comments
 (0)