Skip to content

renatodeleao/a11y-vue-dialog

Repository files navigation

Why another modal/dialog plugin

  • ✅ Accessibility first — Focus trap[1] + keyboard navigation + aria-attributes
  • ✅ Fully controlled component
  • ✅ Pure vue, no wrapping.
  • ✅ Simplicity + size
  • 🕸 Nested dialogs (questionable pattern, not recommended, but possible because it happens) and it's actually in WAI-ARIA examples so...
  • 🚧 renderless version

Detailed documentation and additional info is available at documentation site

Install

npm i a11y-vue-dialog

# or

yarn add a11y-vue-dialog
// portal vue is binstalled as dependency, and it's required
import PortalVue from "portal-vue";
Vue.use(PortalVue);

// add the component
import A11yVueDialog from "a11y-vue-dialog";
// if you want to register globally
Vue.use(A11yVueDialog);

Usage

<a11y-vue-dialog :open="true">
  <p>This slot content will be rendered wherever the <portal-target name="a11y-vue-dialogs">
    is located. (we adivse at the bottom of your root component)</p>
</a11y-vue-dialog>
<portal-target name="a11y-vue-dialogs" multiple />

Renderless version

A renderless version provides all the functionality required to build a proper Dialog, but gives zero f*cks about your markup and styles. The default scopedSlot props help you bind the accessibility attributes and event listeners to your markup elements, but semantics and styling layer it's now the consumer full responsibility.

Each ref suffixed slotProp is an object that contains a "props" and "listeners" keys to be attached to elements via v-bind and v-on respectively

slotProp type desc
open Boolean prop forwarding for portal v-if
close Function method forwarding for closing the dialog
backdropRef Object for the backdrop element
dialogRef Object for the main dialog element
closeRef Object for attaching close buttons/actions
titleRef Object For attaching dialog title, accessibility
focusRef Object For cherry-picking the first focusable element on open

Example

<!-- compose into you own markup, MyDialog.vue -->
<template>
  <portal to="a11y-vue-dialogs" v-if="open">
    <a11y-vue-dialog-renderless 
      v-bind="$props"
      v-bind="$listeners"
      #default="{ open, closeFn, backdropRef, dialogRef, titleRef, closeRef, focusRef }"
    >
      <div class="youclasses" 
        v-bind="backdropRef.props" 
        v-on="backdropRef.listeners"
      >
        <div 
          class="youclasses__element" 
          v-bind="dialogRef.props" 
          v-on="dialogRef.listeners"
        >
          <h1 v-bind="titleRef.props">Title</h1> 
          <button 
            v-bind="closeRef.props" 
            v-on="closeRef.listeners"
          >
            x
          </button>
          <section>
            <!-- autofocus would also work on this case, but not every focusable element supports it -->
            <input 
              type="text" 
              placeholder="I will get focused first because i'm the focus ref" 
              v-bind="focusRef.props"
            />
            <slot />
          </section>
          <footer>
            <button @click="closeFn">Cancel</button>
            <button @click="emit('confirm')">Confirm</button>
          </footer>
        </div>
      </div>
    </a11y-vue-dialog-renderless>
  </portal>
</template>

<script>
import { A11yVueDialogRenderless } from "a11y-vue-dialog";
import { Portal } from "portal-vue";

export default {
  name: 'MyDialog',
  components: {
    A11yVueDialogRenderless,
    Portal
  },
  extends: { A11yVueDialogRenderless },
  props: ['open', 'role'],
}
</script>

Then re-use and conquer

<!-- page.vue -->
<template>
  <div id="page">

    <button @click="openMyModal = true">
    <my-dialog
      open="openMyModal" 
      @close="openMyModal = false" 
      @confirm="handSubmit"
    />
      My markup, my rules.
    </my-dialog>
  </div>
</template>

Usage with

When you use a as the root element of the portal and then remove the portal (i.e. with v-if) or set its disabled prop to true, no leave transition will happen. While this is to expected, as the same thing would happen if you removed a div that contains a , it often trips people up, which is why it's mentioned here. vue-simple-portal

if you really need to apply the v-if to portal, check the example in the link above

But based on the info above, this also works fine:

  <portal to="a11y-vue-dialogs">
    <!-- 
      [1] note the v-if is applied to transition not portal.
          could also be applied to the component itself
    -->
    <transition name="fade" appear v-if="open">
      <a11y-vue-dialog-renderless 
        v-bind="$props"
        v-bind="$listeners"
        #default="{ open, closeFn, backdropRef, dialogRef, titleRef, closeRef, focusRef }"
      >
        <!-- your implementation -->
      </a11y-vue-dialog-renderless>
    </transition>
  </portal>

Play

A playground is used to test the component locally. It uses vue/cli instant prototyping feature, so the downside is that you have to install it globally.

  • Then, just run yarn play

Colophon

Thanks to all this packages for inspiration and guidance.

Dependencies

  1. Since v0.5.0 focus trap is powered by the awesome focus-trap — go and give them some ✨
  2. portal-vue from @LinusBorg wich makes escape overflow traps like breeze

Acknowledgements

  • a11y-dialog (vanilla) from @HugoGiraudel to lead the path that ended here
  • vue-a11y-dialog (wrapper around ^) from @morkro for the motivation to build a pure vue alternative to it.
  • vue-js-dialog a fully fledge massive dialog

License

MIT © Renato de Leão