Skip to content

Instantly share code, notes, and snippets.

@movntains
Last active June 8, 2023 18:14
Show Gist options
  • Save movntains/7e8d61152d4254d7d2e8154e0fe32c24 to your computer and use it in GitHub Desktop.
Save movntains/7e8d61152d4254d7d2e8154e0fe32c24 to your computer and use it in GitHub Desktop.
Custom Vue focus trap directive with examples
import focusTrap from './directives/focusTrap'
const app = createApp({
// Any options here
})
app.directive('trap', focusTrap)
app.mount('#app')
<template>
<div
v-if="name === openedModal"
v-trap="modalIsOpen"
>
<div
role="dialog"
aria-labelledby="modalTitle"
>
<button
type="button"
aria-label="Close Modal"
@click="handleCloseModal"
@keydown.enter.prevent="handleCloseModal"
>
<!-- SVG for a close icon here -->
</button>
<div id="modalTitle">
<slot name="header" />
</div>
<div>
<slot name="content" />
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useModalStore } from '../store/modules/modal'
defineProps({
name: {
type: String,
required: true,
}
})
const { openedModal } = storeToRefs(useModalStore())
const modalIsOpen = computed(() => !!openedModal.value)
const handleCloseModal = () => {
// Logic for resetting "openedModal" to an empty string in the store
}
</script>
import { createFocusTrap } from 'focus-trap'
let trap
const createTrap = (element) => {
trap = createFocusTrap(element, {
escapeDeactivates: true,
allowOutsideClick: true,
})
}
const focusTrap = {
updated(element, binding) {
if (!trap && binding.value) {
const focusTrapElement = binding.arg
? [element, ...binding.arg]
: element
createTrap(focusTrapElement)
setTimeout(() => {
trap.activate()
})
} else if (trap && !binding.value) {
trap.deactivate()
}
},
}
export default focusTrap
<template>
<div
v-trap:[focusableElements]="mobileNavIsOpen"
role="dialog"
>
<nav
aria-label="Main Navigation"
@keydown.esc="resetMobileNav"
>
<ul>
<li
v-for="(navItem, index) in navItems"
:key="`nav-item-${index}`"
>
<a
:href="navItem.url"
v-text="navItem.title"
/>
</li>
</ul>
</nav>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUiStore } from '../store/modules/ui'
defineProps({
navItems: {
type: Array,
required: true,
},
})
// This is an element that's outside of this component
const focusableElements = ['#header-nav']
const { mobileNavIsOpen } = storeToRefs(useUiStore())
const resetMobileNav = () => {
// Logic for resetting "mobileNavIsOpen" to false in the store
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment