# Focus Trap
The focus trap package makes it easy to trap the focus within a component/element. @vue-cdk/focus-trap
is a wrapper around the focus-trap package from @davidtheclark (opens new window). The main motivation of this wrapper was to make it more convenient for Vue developers.
# Overview
The main objective of @vue-cdk/focus-trap
is to make focus trapping more convenient. In order to serve different needs @vue-cdk/focus-trap
comes with both, high– and low–level ways to accomplish focus trapping.
- High Level Focus Trap Component:
@vue-cdk/focus-trap
contains a component calledCFocusTrap
. You can use this component to trap the focus within a wrapped container element or component. - Low Level Utility Function:
@vue-cdk/focus-trap
also exports a function that allows you to create traps manually.
# Installation
$ npm install @vue-cdk/focus-trap --save
# Usage
# Using the Plugin
After installing @vue-cdk/focus-trap
you may simply use the Vue plugin:
import Vue from 'vue'
import FocusTrap from '@vue-cdk/focus-trap'
Vue.use(FocusTrap /*, optional options */)
The plugin globally registers a component that can be used to create focus traps. This component is called CFocusTrap
. Please refer to the examples below to see how it is used.
# Using the Low–Level Utilities
createFocusTrap
is the main entry point to the low-level API. The basic usage goes like this:
import { createFocusTrap } from '@vue-cdk/focus-trap'
// `vm` is an instance of a Vue component
const trap = createFocusTrap(vm)
trap.activate()
// later… trap.deactivate()
createFocusTrap
expects an instance of a Vue component you would like to trap the focus in. This function returns a FocusTrap
-object that you should strongly reference – for example by assinging it to a property:
methods: {
enableTrap() {
this.trap = createFocusTrap(this)
// now you can use the trap
}
}
FocusTrap
exposes two methods that you can use to activate and deactivate the trap on the Vue instance you intially passed to createFocusTrap
.
# activate(options)
Activates the focus trap. The options are optional and have to following shape:
export interface _ActivateOptions {
readonly deactivation: Deactivation
// A function thas is called when the trap is deactivated.
readonly onDeactivate: () => void
// The element that should be focused initially. The focus-trap package accepts an element selector or an HTMLElement. Vue's $el is only typed Element. This is the reason why 'our' initialFocus (the one below) is typed Element – to make it more convenient for Vue developers. Internally we simply force cast to HTMLElement. Hope hope hope.
readonly initialFocus: Element
}
export type Deactivation =
| 'on-esc' // default
| 'manual'
# deactivate()
Deactivates the focus trap.
WARNING
You should always deactivate a previously activated trap. The last moment you can safely deactivate a trap is in beforeDestroy
(opens new window).
# Using the (High-Level) Vue Component
The low-level API should only be used in case you really need it. Using the high-level API is more convenient. You can use it like this:
I am a Modal
Show Code
<template>
<div>
<div>
<input tabindex="0" style="width: 100%" value="focusable input outside" />
</div>
<button @click="active = true">Activate Trap</button>
<CFocusTrap :active="active">
<div
:style="{
width: '200px',
height: '200px',
padding: '20px',
margin: '20px 0',
border: '1px solid #ccc',
backgroundColor: '#fefefe',
}"
>
<p>I am a Modal</p>
<input ref="intialInput" tabindex="0" />
<input />
<input />
<button @click="active = false">Deactivate Trap</button>
</div>
</CFocusTrap>
</div>
</template>
<script>
export default {
data() {
return {
active: false,
}
},
}
</script>
# More Examples
WARNING
On iOS the user can escape an active trap by tapping on the down arrow/up arrow buttons in Safari. This is a known issue (opens new window). Safari does not emit any events when tappingon the down arrow and/or up arrow. A future modal component should fix that issue though.
# Using CFocusTrap
# Hello World
I am a Modal
Show Code
<template>
<div>
<div>
<input ref="inputOutside" tabindex="-1" />
</div>
<button @click.prevent.cancel.stop="activateTrap">trap</button>
<Modal ref="modal">
<div>
<p>I am a Modal</p>
<input ref="intialInput" tabindex="0" />
<input />
<input />
</div>
</Modal>
</div>
</template>
<script>
import { createFocusTrap } from '@vue-cdk/focus-trap'
const Modal = {
render(h) {
return h('div', { style: 'width: 200px; height: 200px;' }, this.$slots.default)
},
}
export default {
components: { Modal },
beforeDestroy() {
if (this.trap != null) {
this.trap.deactivate()
}
},
methods: {
activateTrap() {
this.trap = createFocusTrap(this.$refs.modal)
this.trap.activate({
onDeactivate: () => {
this.$refs.inputOutside.focus()
},
initialFocus: this.$refs.intialInput,
})
},
},
}
</script>
# Nested Focus Trapping
Show Code
<template>
<div>
<button @click="showModal">Show Modal</button>
<Modal v-if="modalVisible" ref="modal">
<div>
<p>I am a Modal</p>
<button @click="showNestedTrap">Show nested Modal</button>
<button @click="closeModal">Close Modal</button>
<input ref="intialInput" tabindex="0" />
<input />
<input />
</div>
</Modal>
<Modal v-show="nestedModalVisible" ref="nestedModal">
<div>
<p>I am <strong>nested</strong> a Modal</p>
<button @click="closeNestedModal">Close nested Modal</button>
<input ref="nestedInitialInput" tabindex="0" />
<input />
<input />
</div>
</Modal>
</div>
</template>
<script>
import { createFocusTrap } from '@vue-cdk/focus-trap'
const Modal = {
render(h) {
const style = {
width: '200px',
height: '200px',
border: '1px solid #ccc',
backgroundColor: '#fefefe',
}
return h('div', { style }, this.$slots.default)
},
}
export default {
components: { Modal },
data() {
return {
modalVisible: false,
nestedModalVisible: false,
}
},
beforeDestroy() {
if (this.nestedTrap != null) {
this.nestedTrap.deactivate()
}
if (this.trap != null) {
this.trap.deactivate()
}
},
methods: {
closeModal() {
this.trap.deactivate()
},
async showModal() {
this.modalVisible = true
await this.$nextTick()
this.trap = createFocusTrap(this.$refs.modal)
this.trap.activate({
onDeactivate: () => {
this.modalVisible = false
},
initialFocus: this.$refs.intialInput,
})
},
async showNestedTrap() {
this.nestedModalVisible = true
await this.$nextTick()
this.nestedTrap = createFocusTrap(this.$refs.nestedModal)
this.nestedTrap.activate({
onDeactivate: () => {
this.nestedModalVisible = false
},
initialFocus: this.$refs.nestedInitialInput,
})
},
async closeNestedModal() {
this.nestedTrap.deactivate()
},
},
}
</script>