Skip to content

Instantly share code, notes, and snippets.

@SigurdMW
Created May 16, 2018 11:20
Show Gist options
  • Save SigurdMW/c4d9b12698df815aaf7594d0d9cf5c6b to your computer and use it in GitHub Desktop.
Save SigurdMW/c4d9b12698df815aaf7594d0d9cf5c6b to your computer and use it in GitHub Desktop.
Accessible dropdown for vue
<template>
<div
class="dropdown"
:class="{ 'dropdown--open': isOpen }"
v-click-outside="closeDropdown"
>
<button
class="dropdown__button"
type="button"
@click="toggle"
:id="id"
aria-haspopup="true"
:aria-expanded="isOpen"
:class="{ [buttonClass]: hasButtonClass }"
>
<slot name="before-button-text-slot"></slot>
{{ buttonText }}
</button>
<ul
class="dropdown__menu"
:aria-labelledby="id"
:aria-hidden="!isOpen"
:class="{ 'dropdown__menu--open': isOpen }"
@keydown.down.stop.prevent="moveFocusDown"
@keydown.up.stop.prevent="moveFocusUp"
>
<slot></slot>
</ul>
</div>
</template>
<script>
import { isFocusWithin } from "services/helpers";
export default {
name: "DropdownComponent",
props: {
buttonText: {
type: String,
required: true
},
buttonClass: {
type: String,
default: ""
}
},
data () {
return {
id: null,
isOpen: false,
children: [],
hasButtonClass: this.buttonClass !== ""
}
},
mounted () {
this.id = "dropdown-" + this._uid;
document.addEventListener("keydown", (e) => {
if (e.keyCode === 27) { //Esc key
if (this.isOpen) {
const isFocused = isFocusWithin(this.$el, document.activeElement);
if (isFocused) this.$el.querySelector("#" + this.id).focus();
this.isOpen = false;
}
}
if (e.keyCode === 9) { //tab
if (this.isOpen && !isFocusWithin(this.$el, document.activeElement)) {
this.isOpen = !this.isOpen;
}
}
});
this.children = Array.from(this.$el.querySelectorAll(".dropdown__menu a, .dropdown__menu button"));
this.closeDropdownOnClick(this.children);
},
updated () {
this.children = Array.from(this.$el.querySelectorAll(".dropdown__menu a, .dropdown__menu button"));
this.closeDropdownOnClick(this.children);
},
methods: {
toggle () {
this.isOpen = !this.isOpen;
},
moveFocusUp (e) {
this.moveFocus(e, 'up');
},
moveFocusDown (e) {
this.moveFocus(e, 'down');
},
moveFocus (e, type) {
for (let i = 0; i < this.children.length; i++) {
if (document.activeElement === this.children[i]) {
if (type === 'up') {
if (i - 1 >= 0) {
this.children[i - 1].focus();
} else {
this.children[this.children.length - 1].focus();
}
}
if (type === 'down') {
if (i + 1 < this.children.length) {
this.children[i+1].focus();
} else {
this.children[0].focus();
}
}
break;
}
}
},
closeDropdown () {
if (this.isOpen) {
const isFocused = isFocusWithin(this.$el, document.activeElement);
if (isFocused) this.$el.querySelector("#" + this.id).focus();
this.isOpen = !this.isOpen;
}
},
closeDropdownOnClick (els) {
els.map(el => {
// need to remove event listener due to els (children)
// is updated after mount
el.removeEventListener("click", this.handleClickOnDropdown);
el.addEventListener("click", this.handleClickOnDropdown);
});
},
handleClickOnDropdown (e) {
// do not handle focus on close because of PageComponent
// ensures focus on heading
this.isOpen = !this.isOpen;
}
},
directives: {
'click-outside': {
bind: function(el, binding, vNode) {
// thanks to https://jsfiddle.net/Linusborg/Lx49LaL8/
// Provided expression must evaluate to a function.
if (typeof binding.value !== 'function') {
const compName = vNode.context.name
let warn = `[Vue-click-outside:] provided expression '${binding.expression}' is not a function, but has to be`
if (compName) { warn += `Found in component '${compName}'` }
this.captureError(new Error(err));
}
// Define Handler and cache it on the element
const bubble = binding.modifiers.bubble
const handler = (e) => {
if (bubble || (!el.contains(e.target) && el !== e.target)) {
binding.value(e)
}
}
el.__vueClickOutside__ = handler
// add Event Listeners
document.addEventListener('click', handler)
},
unbind: function(el, binding) {
// Remove Event Listeners
document.removeEventListener('click', el.__vueClickOutside__)
el.__vueClickOutside__ = null
}
}
}
}
</script>
// Usage
<dropdown-component buttonText="Dropdown" buttonClass="some-class">
<span slot="before-button-text-slot">Some text</span>
<li class="dropdown__item">
<a href="javascript:void(0);">Some link</a>
</li>
<li class="dropdown__item">
<a href="javascript: void(0);">Some other link</a>
</li>
</dropdown-component>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment