Giter VIP home page Giter VIP logo

focus-trap-vue's Introduction

focus-trap-vue Build Status npm package thanks

Vue component to trap the focus within a DOM element

Installation

For Vue 2

npm install focus-trap focus-trap-vue@legacy

 For Vue 3

npm install focus-trap focus-trap-vue

Usage

This library exports one single named export FocusTrap and requires focus-trap as a peer dependency. So you can locally import the component or declare it globally:

 Register globally in a Vue 2 app

import { FocusTrap } from 'focus-trap-vue'

Vue.component('FocusTrap', FocusTrap)

 Register globally in a Vue 3 app

import { FocusTrap } from 'focus-trap-vue'

createApp(App)
  .component('FocusTrap', FocusTrap)
  .mount('#app')

Note this documentation is for Vue 3 and some props/events might not exist in the Vue 2 version

FocusTrap can be controlled in three different ways:

  • by using the active Boolean prop
  • by using v-model:active (uses the active prop, Vue 3 only)
  • by calling the activate/deactivate method on the component

The recommended approach is using v-model:active and it should contain one single child:

<focus-trap v-model:active="isActive">
  <modal-dialog tabindex="-1">
    <p>Do you accept the cookies?</p>
    <button @click="acceptCookies">Yes</button>
    <button @click="isActive = false">No</button>
  </modal-dialog>
</focus-trap>

When isActive becomes true, it activates the focus trap. By default it sets the focus to its child, so make sure the element is a focusable element. If it's not you wil need to give it the tabindex="-1" attribute. You can also customize the initial element focused. This element should be an element that the user can interact with. For example, an input. It's a good practice to always focus an interactable element instead of the modal container:

<focus-trap v-model:active="isActive" :initial-focus="() => $refs.nameInput">
  <modal-dialog>
    <p>What name do you want to use?</p>
    <form @submit.prevent="setName">
      <label>
        New Name
        <input ref="nameInput" />
      </label>
      <button>Change name</button>
    </form>
  </modal-dialog>
</focus-trap>

Props

FocusTrap also accepts other props:

  • escapeDeactivates: boolean
  • returnFocusOnDeactivate: boolean
  • allowOutsideClick: boolean | ((e: MouseEvent | TouchEvent) => boolean)
  • clickOutsideDeactivates: boolean | ((e: MouseEvent | TouchEvent) => boolean)
  • initialFocus: string | (() => Element) Selector or function returning an Element
  • fallbackFocus: string | (() => Element) Selector or function returning an Element
  • delayInitialFocus: boolean
  • tabbableOptions: FocusTrapTabbableOptions Options passed to tabbableOptions

Please, refer to focus-trap documentation to know what they do.

Events

FocusTrap emits 2 events. They are in-sync with the prop active

  • activate: Whenever the trap activates
  • deactivate: Whenever the trap deactivates (note it can also be deactivated by pressing Esc or clicking outside)

Methods

FocusTrap can be used without v-model:active. In that case, you will use the methods and probably need to initialize the trap as deactivated, otherwise, the focus will start as active:

<button @click="() => $refs.focusTrap.activate()">Show the modal</button>

<focus-trap :active="false" ref="focusTrap">
  <modal-dialog>
    <p>Hello there!</p>
    <button @click="() => $refs.focusTrap.deactivate()">Okay...</button>
  </modal-dialog>
</focus-trap>

Note the use of arrow functions, this is necessary because we are accessing $refs which are unset on first render.

Related

License

MIT

This project was created using the Vue Library boilerplate by posva

focus-trap-vue's People

Contributors

alexandrebonaventure avatar dependabot-preview[bot] avatar doreilly avatar dv8fromtheworld avatar fabien-ml avatar jakobberglund avatar jschroeter avatar laruiss avatar leopoldthecoder avatar outdoorsman avatar posva avatar renatodeleao avatar renovate-bot avatar renovate[bot] avatar sarayourfriend avatar unshame avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

focus-trap-vue's Issues

Update peer dependency

Current version of focus-trap is 6.0.1 so it gives me a warning to install ^5.0.2

Also it would be better to not specify the peer dependency versions down to the patch level.

Vue3 compatible version no longer exports `activate` and `deactivate` methods

In the Vue2 version (1.x) the following was valid:

<template>
  <FocusTrap ref="focusTrap">
    <!-- Content here -->
  </FocusTrap>
</template>

<script>
export default {
  methods: {
    manuallyDeactivate() {
      this.$refs.focusTrap.deactivate()
    }
  }
} 
</script>

The activate and deactivate methods were exposed on the component. The documentation speaks to their usage as well.

This is no longer possible in the Vue3 (2.x / 3.x) version as the methods are not being exposed on the component.

Unable to set focus to slot child component triggered by store commit

<Nav> // method triggers store commit that opens parent modal
<ParentModal> // parent modal has slot that populates with a child modal based on store commit data
<ChildModal> // child modal has input element that needs focus

When I set the focus trap on the ParentModal I an unable to set initial-focus in the ChildModal.

  • I tried using $nextTick for ChildModal ref.
  • This worked but gave me a console error 'focus trap expects a node ref'. I used a ref on the Parent to a Child method that set focus on the ref. I even tried returning the ref as a node.

Please advise.

Enhancement - preventScroll option

What problem is this solving

Prevents scroll jumps when focusing both <focus-trap> target HTMLElement and returnFocusOnDeactivate target HTMLElement.

Proposed solution

Implement what original focus-trap library uses (preventScroll create option and tryFocus function):

focus-trap's preventScroll create option

focus-trap's tryFocus function

We should have a tryFocus function called that passes preventScroll: true as FocusOptions in HTMLElement.focus(FocusOptions) method when focusing target HTMLElements.

Describe alternatives you've considered

There are ways to dodge scrolling issues, but it would require using position: fixed on target HTMLElements.

`preventScroll` property is not forwarded to `createFocusTrap`

Reproduction

Passing the any value to the FocusTrap component's :prevent-scroll property is ignored.

Steps to reproduce the behavior

  1. Create a component with <FocusTrap :prevent-scroll="true">
  2. The embedded trap does not initialize with preventScroll: true

Expected behavior

The FocusTrap component should take the defined preventScroll property and hand it down to the embedded focus-trap instance.

Actual behavior

The preventScroll property is defined, but ignored by the component.

Additional information

There are other properties that are defined but not passed through to the embedded trap. This report only focuses on preventScroll.

use of focus-trap v3.2.1 returns an error if do everything as in readme

Reproduction

Hi, thanks for vue3 version, but now it has some issues.

At first, it only works if use it as <FocusTrapVue>, the tag from version 2 <FocusTrap> returns an error - something with querySelector

And second, if use it as FocusTrapVue, you get a warning about correct exporting custom files.

Screenshot 2021-11-22 at 15 59 19

el.querySelectorAll is not a function when el is a Vue component Proxy

Hello and thank you for working on this package. I noticed a potential bug related to #380. Here's a description of the issue.

Context

Whenever the focus trap becomes active, and the sole element in the focus trap is another Vue component (at least that seems to be the case), an error will be thrown in the console.

Reproduction

I've created a CodeSandbox instance where you can see the behavior for yourself.

Steps to reproduce the behavior

  1. Click on the "Toggle trap focus" button
  2. See error

Expected behavior

Focus trap should kick in as the active prop is set to true.

Actual behavior

Received the following error when setting the active prop to true.

index.js:25 Uncaught (in promise) TypeError: el.querySelectorAll is not a function
    at getCandidates2 (index.js:25)
    at tabbable2 (index.js:243)
    at index.js:226
    at Array.map (<anonymous>)
    at updateTabbableNodes2 (index.js:225)
    at Object.activate (index.js:546)
    at watch.immediate (focus-trap-vue.esm-bundler.js:80)
    at callWithErrorHandling (runtime-core.esm-bundler.js:154)
    at callWithAsyncErrorHandling (runtime-core.esm-bundler.js:163)
    at job (runtime-core.esm-bundler.js:2096)

Additional information

I first discovered this issue on Electron, and did a little debugging there to look at what el is. It turns out when the element inside of <focus-trap> is a Vue component, instead of an HTMLElement, a Proxy of the Vue component is passed.

For clarity, I tried changing the element in the focus trap to a regular <div> and it worked as you'd expect.

Nov 12th, 2021
Dug a bit deeper to understand a bit more about the dependencies here.
The error is thrown from tabbable, which is a dependency of focus-trap, of which this library depends on. Looks like focus-trap is a expected to work in a framework-less scenario.

A bit more digging shows that this library is using the as keyword in TypeScript on line 65 in the FocusTrap.ts file. Pasting that block here for context:

   const el = ref<HTMLElement | null>(null)

    const ensureTrap = () => {
      if (trap) {
        return
      }

      const { initialFocus } = props
      trap = createFocusTrap(el.value as HTMLElement, {

I'm not familiar with Vue's API for plugins but if I understand it correctly, el is set during render, particularly on line 143 in the same file?

Nov 13th, 2021
Cloned the repo to try see if I can figure something out.

I'm away from my computer now, but when I tried earlier, I was able to "fix" the issue. I'm not sure if it's the right thing to do, however.

First, I added the ComponentPublicInstance type (from vue) to the union for el.

Then I added an extra check before createFocusTrap is called, like so

  if (
    !(el instanceof HTMLElement) &&
    Reflect.has(el.value, "$el")
  ) {
    el.value = (el.value as ComponentPublicInstance).$el;
  }

  trap = createFocusTrap(el.value as HTMLElement, {

However, one thing that's notable is that $el has type any within ComponentPublicInstance, not HTMLElement. Hence why I'm not sure if this is a good solution.

I'll likely check things out a bit further, get a bit more guarantee in before I make a PR.


Related issues found: #380

Activating focus trap gives the following error in the console

Following the basic instructions to install and use.

Code snippet:

<template>
  <FocusTrap v-model:active="focusTrapActive">
    <MDBModal ...>
    ...
    </MDBModal>
  </FocusTrap>
<template>

When focusTrapActive is set I get the following stack trace:

index.esm.js?62f7:10 Uncaught (in promise) TypeError: el.querySelectorAll is not a function
    at getCandidates (index.esm.js?62f7:10)
    at tabbable (index.esm.js?62f7:206)
    at eval (focus-trap.esm.js?e16c:237)
    at Array.map (<anonymous>)
    at updateTabbableNodes (focus-trap.esm.js?e16c:236)
    at Object.activate (focus-trap.esm.js?e16c:519)
    at Object.immediate (focus-trap-vue.esm-browser.js?164c:80)
    at callWithErrorHandling (runtime-core.esm-bundler.js?5c40:6656)
    at callWithAsyncErrorHandling (runtime-core.esm-bundler.js?5c40:6665)
    at Array.job (runtime-core.esm-bundler.js?5c40:7056)

Implement delayInitialFocus

What problem is this solving

I am getting the error "Your focus-trap must have at least one container with at least one tabbable node in it at all times".

Proposed solution

The solution is to expose the parameter delayInitialFocus from the focus-trap library and set it to false as per focus-trap/focus-trap-react#91

Vue 3 version

Hello 👋
For my own project I forked and upgraded this library with Vue 3 API. Would you be interested in joining our effort to merge these changes for a vue 3 compat ?

looks roughly like that

import { defineComponent, onMounted, watch, ref, cloneVNode, onUnmounted } from 'vue';
import createFocusTrap, { FocusTrap as FocusTrapI } from 'focus-trap';

const FocusTrap = defineComponent({
  props: {
    active: {
      // TODO: could be options for activate
      type: Boolean as () => boolean,
      default: true,
    },
    escapeDeactivates: {
      type: Boolean as () => boolean,
      default: true,
    },
    returnFocusOnDeactivate: {
      type: Boolean as () => boolean,
      default: true,
    },
    allowOutsideClick: {
      type: Boolean as () => boolean,
      default: true,
    },
    initialFocus: {
      type: [String as () => string, Function as () => () => HTMLElement],
      default: undefined,
    },
    fallbackFocus: {
      type: [String as () => string, Function as () => () => HTMLElement],
      default: undefined,
    },
  },
  setup (props, { slots, emit }) {
    let trap: FocusTrapI | null;
    const el = ref<HTMLElement | null>(null);
    onMounted(function () {
      watch(() => props.active, (active) => {
        if (active && el.value) {
          // has no effect if already activated
          trap = createFocusTrap(
            el.value,
            {
              escapeDeactivates: props.escapeDeactivates,
              allowOutsideClick: () => props.allowOutsideClick,
              returnFocusOnDeactivate: props.returnFocusOnDeactivate,
              onActivate: () => {
                emit('update:active', true);
                emit('activate');
              },
              onDeactivate: () => {
                emit('update:active', false);
                emit('deactivate');
              },
              initialFocus: typeof props.initialFocus === 'string' ? props.initialFocus : props.initialFocus?.() ?? el.value,
              fallbackFocus: props.fallbackFocus,
            },
          );
          trap.activate();
        } else {
          trap?.deactivate();
        }
      },
      { immediate: true },
      );
    });
    onUnmounted(() => {
      trap?.deactivate();
      trap = null;
    });
    return () => {
      const content = slots.default?.();
      if (!content || !content.length || content.length > 1) { throw new Error('FocusTrap requires exactly one child'); };
      const vnode = cloneVNode(content[0], { ref: el });
      return vnode;
    };
  },
});

export { FocusTrap };

```

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.