# 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 called CFocusTrap. 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

I am nested a Modal

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>

# API

Playground