70

I wish to listen to an 'esc' key event in order to call a method in a Vue component. The docs shows this example:

<input v-on:keyup.enter="submit">

but i'm using a <div></div>, and need to catch the event from outside. However, I wish NOT to overload global handlers or anything like that.

Any suggestions?

Chen
  • 2,958
  • 5
  • 26
  • 45

6 Answers6

99

For anyone who wanders here from google, in Vue 2...

<div @keydown.esc="something_in_your_methods"></div>
laaksom
  • 2,050
  • 2
  • 18
  • 17
51

The secret for making keydown events work on divs and other non-focusable elements is to add a tabindex attribute:

<div tabindex="0"
    @keydown.left="previousImage"
    @keydown.right="nextImage" />

Now the div has become a focusable element and the key events will be triggered.

Here is more info on focusable elements and tabindex

BassMHL
  • 8,523
  • 9
  • 50
  • 67
  • 2
    Isn't that potentially bad for accessibility? – Nickolai Jun 16 '21 at 20:14
  • Use this on places where it makes sense. I personally have a table with rows. Any row can be clicked on to open a window. I want to make it accessible using keyboard so I'd argue it's actually good for accessibility – Kerwin Sneijders Jul 31 '21 at 13:09
  • @Nickolai [This article](https://www.maxability.co.in/2016/06/13/tabindex-for-accessibility-good-bad-and-ugly/) might answer your question – Marvin Aug 03 '22 at 21:37
24

What I did was go for a mixin.

The mixin in a file called close.js

export default {
    created() {
        let that = this;

        document.addEventListener('keyup', function (evt) {
            if (evt.keyCode === 27) {
                that.close();
            }
        });
    },
};

Import and use it in the desired component

import closeMixin from './../../mixins/close.js';

export default {
    mixins: [closeMixin],
    props: [],
    computed: {}
    methods: {
        close(){
            // closing logic
        }
    }
}
  • 14
    But, you forgot to remove the event listener on destroy, wouldn't this have unintended effects? – Denis Nutiu Jul 24 '20 at 09:44
  • 2
    store the in a variable e.g. `this.escListener = document.addEventListener('keyup'` then in the destroy() lifecycle method `document.removeEventListener(this.escListener)` – Jaybeecave Feb 22 '21 at 21:56
  • 3
    As of 2022 `if (evt.key === "Escape") {` is the preferred (non-deprecated) expression. – Joe Sep 22 '22 at 09:57
18

3 things to make sure of on the main element:

  • It has a tabindex
  • It or a descendant is focused
  • It is listening for the event

Here is how I usually manage my modals:

<div ref="modal" @keyup.esc="close" tabindex="-1">
   <!-- Modal content -->
</div>
mounted() {
    this.$refs.modal.focus();
}
Pierre de LESPINAY
  • 44,700
  • 57
  • 210
  • 307
7

In my case, I created a directive, v-esc.ts. (※ This is Vue3 directive writing way)

import { Directive } from 'vue'
const directive: Directive = {
  beforeMount(el, binding) {
    el._keydownCallback = (event) => {
        if (event.key === 'Escape') {
            binding.value()
      }
    }
    document.addEventListener('keydown', el._keydownCallback)
  },
  unmounted(el, binding) {
    document.removeEventListener('keydown', el._keydownCallback)
    delete el._keydownCallback
  }
}
export const esc = { esc: directive }

Then I can use it in any component like this. (NOTE: you must pass a function param to v-esc, because the param executed as binding.value() in the directive)

<template>
  <img
    @click.prevent="close"
    v-esc="close"
    src="@/assets/icons/close.svg"
  />
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { esc } from '@/common/directives/v-esc'
export default defineComponent({
  name: 'nitCloseButton',
  ...
  methods: {
    close() {
      this.$emit('close')
    }
  },
  directives: {
    ...esc
  }
})
</script>

P.S One month after, I also need arrow left and arrow right keys. So, I've made this directive more general like this.

import { Directive } from 'vue'
const directive: Directive = {
  beforeMount(el, binding) {
    el._keydownCallback = event => {
      console.log('keydown', event.key)
      if (event.key === binding.arg) {
        binding.value()
      }
    }
    document.addEventListener('keydown', el._keydownCallback)
  },
  unmounted(el, binding) {
    document.removeEventListener('keydown', el._keydownCallback)
    delete el._keydownCallback
  }
}
export const keydown = { keydown: directive }

You can detect any key's keydown by passing keyname as binding.args (v-keydown:{keyName} like below)

<button
  v-keydown:ArrowLeft="moveToPreviousPage"
  class="controller-button lo-center"
  @click="moveToPreviousPage"
>
  <arrow-icon :rotation="180" />
</button>
<button
  v-keydown:ArrowRight="moveToNextPage"
  class="controller-button lo-center"
  @click="moveToNextPage"

export default defineComponent({
  name: 'componentName',
  directives: {
    ...keydown
  }
...
})
daolanfler
  • 68
  • 4
kazuwombat
  • 1,515
  • 1
  • 16
  • 19
  • 1
    Interesting, I haven't tried writing my own directives yet, but this looks quite promising. Also this did work out of the box for me. The only thing my intellisense mentioned is, that the directives property should be close to top (before setup in my case). Thank you @Matsumoto Kazuya – mecograph Jul 13 '21 at 19:56
5

You can't. Key events are dispatched from the body tag and Vue can't be mounted to the <body> tag.

Sourced from "When VueJS Can't Help You"]

You'll have to set up your own event listener.

(image source & more info at When VueJS Can't Help You)

Mathieu Dhondt
  • 8,405
  • 5
  • 37
  • 58