Syed
Syed

Reputation: 16513

Vue.js Element Ui - trap / lock focus when el-dialog is open

As you can see here, when you open any dialog example and start pressing the tab key, the focus start highlighting the elements that are outside/behind the dialog. I was wondering if element-ui have something to offer which I missed to notice?

Anyhow, for now I am using vue-focus-lock but I wish something native to be offered by element-ui rather pulling a library to fix this issue.

Upvotes: 2

Views: 3000

Answers (2)

Syed
Syed

Reputation: 16513

Here is answer to my own question:

First of all I tried to use vue-focus-lock plugin it works but also have issues when selecting the components like el-checkbox or el-radio. You can see the issue when the outside/behind content of el-dialog is more (below fold) then the checkbox or radio will not be checked.

Anyhow here is my solution, I have created a component FocusLock.vue:

<template lang="pug">
  div.dialog-focus-lock
    slot
</template>

<script>
export default {
  name: 'focus-lock',

  data() {
    return {
      focusableEls: [],
      firstFocusableEl: [],
      lastFocusableEl: [],
      focusedElBeforeOpen: document.activeElement,
    };
  },

  methods: {
    handleDialogFocusLock() {
      const selectors = '' +
        'a[href], ' +
        'input:not([disabled]), ' +
        'select:not([disabled]), ' +
        'textarea:not([disabled]), ' +
        'button:not([disabled])';

      const getEls = this.$el.querySelectorAll(selectors);

      this.focusableEls = Array.prototype.slice.call(getEls);
      [this.firstFocusableEl] = this.focusableEls; // get first array item
      [this.lastFocusableEl] = this.focusableEls.slice(-1); // get last array item

      this.$el.addEventListener('keydown', e => this.handleKeyDown(e));
      this.firstFocusableEl.focus();
    },

    handleBackwardTab(e) {
      if (document.activeElement === this.firstFocusableEl) {
        e.preventDefault();
        this.lastFocusableEl.focus();
      }
    },

    handleForwardTab(e) {
      if (document.activeElement === this.lastFocusableEl) {
        e.preventDefault();
        this.firstFocusableEl.focus();
      }
    },

    handleKeyDown(e) {
      const KEY_TAB = 9;

      switch (e.keyCode) {
        case KEY_TAB:
          if (this.focusableEls.length === 1) {
            e.preventDefault();
            break;
          }
          if (e.shiftKey) {
            this.handleBackwardTab(e);
          } else {
            this.handleForwardTab(e);
          }
          break;
        default:
          break;
      }
    },
  },

  mounted() {
    this.handleDialogFocusLock();
  },
};
</script>

I wanted the component to be available globally, so imported it in main.js or in your case just import it wherever you need.

import FocusLock from '@/components/partials/FocusLock.vue';
Vue.component('FocusLock', FocusLock);

And using them wherever I need, just like this:

<template lang="pug">
  focus-lock
    el-dialog(:visible.sync="dialogVisible")
      el-form(:model="editedItem")
        // form elements goes here
</template>

Took some help from HERE.

Also, I think you should not worry about handling this.$el.removeEventListener, this is taken care by vue.js, you can test by pasting below code in your browser console and run it when the el-dialog is open and run again when it's closed.

Array.from(document.querySelectorAll('*'))
  .reduce(function(pre, dom){
    var clks = getEventListeners(dom).click;
    pre += clks ? clks.length || 0 : 0;
    return pre
  }, 0)

Upvotes: 1

Fleeck
Fleeck

Reputation: 1066

That seems like a bug, which element-ui hasn't fixed yet. If you want a sheer native solution, here's an idea: you can listen for a keydown event on a Tab key. In that case, you are able to access the last element (document.activeElement) in the dialog and prevent further tab presses. If you do that, don't forget to remove event listener when the dialog is closed. Hope this helps.

Upvotes: 2

Related Questions