<dropdown-menu v-slot="{ open, close }">
<dropdown-toggle @click="open">
Dropdown
</dropdown-toggle>
<dropdown-menu>
<a href="#item-1" @click="close">Item 1</a>
<a href="#item-2" @click="close">Item 2</a>
<a href="#item-3" @click="close">Item 3</a>
</dropdown-menu>
</dropdown-menu>
<template>
<span v-on-clickaway="close">
<slot v-bind="{ open, close }"></slot>
</span>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway'
export default {
mixins: [clickaway],
data: vm => ({
id: `dropdown-${vm._uid}`,
status: 'closed',
}),
mounted () {
let onKeydown = (event) => {
if (this.isClosed()) return
if (event.key === 'Escape') {
event.preventDefault()
this.close()
}
}
document.addEventListener('keydown', onKeydown)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keydown', onKeydown)
})
},
methods: {
open () {
this.status = 'opened'
},
close () {
this.status = 'closed'
},
isOpened () {
return this.status === 'opened'
},
isClosed () {
return this.status === 'closed'
}
},
}
</script>
<template>
<component
role="menu"
v-if="$parent.isOpened()"
:aria-labelledby="`${$parent.id}-toggle`"
:id="`${$parent.id}-menu`"
:is="as"
>
<slot />
</component>
</template>
<script>
import { createPopper } from '@popperjs/core'
export default {
props: {
as: {
type: String,
default: 'div'
},
placement: {
type: String,
default: 'bottom-end'
},
offset: {
type: Array,
default: function () {
return [0, 8]
}
},
},
data: (vm) => ({
focusedChild: -1,
popperOptions: {
placement: vm.$props.placement,
modifiers: {
name: 'offset',
options: {
offset: vm.$props.offset,
}
}
}
}),
mounted () {
let onKeydown = (event) => {
if (this.$parent.isClosed()) return
switch (event.key) {
case 'Tab':
event.preventDefault()
event.shiftKey ? this.previous() : this.next()
break;
case 'Enter':
event.preventDefault()
if (this.focusedChild === -1) return
this.focusableChildren()[this.focusedChild].click()
break;
case 'Escape':
event.preventDefault()
this.close()
break;
case 'ArrowUp':
event.preventDefault()
this.previous()
break;
case 'ArrowDown':
event.preventDefault()
this.next()
break;
case 'Home':
event.preventDefault()
this.focusedChild = 0
break;
case 'End':
event.preventDefault()
this.focusedChild = this.focusableChildren().length - 1
break;
}
}
document.addEventListener('keydown', onKeydown)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keydown', onKeydown)
})
},
methods: {
previous () {
this.focusedChild = this.focusedChild > 0
? this.focusedChild - 1
: this.focusableChildren().length - 1
},
next () {
this.focusedChild = this.focusedChild < (this.focusableChildren().length - 1)
? this.focusedChild + 1
: 0
},
focusableChildren () {
return this.$el.querySelectorAll([
'a, button, input, textarea, select',
'details, [tabindex]:not([tabindex="-1"])'
].join(','))
},
},
watch: {
focusedChild: function (value) {
if (value < 0) return
this.focusableChildren()[value].focus()
},
"$parent.status": function (value) {
if (value === 'closed') return this.popper.destroy()
this.$nextTick(() => this.popper = createPopper(
this.$parent.$el, this.$el, this.popperOptions
))
this.focusedChild = -1
},
}
}
</script>
<template>
<component
:is="as"
:id="`${$parent.id}-toggle`"
:aria-expanded="$parent.isOpened()"
:aria-controls="$parent.isOpened() ? `${$parent.id}-menu` : false"
v-on="$listeners"
>
<slot />
</component>
</template>
<script>
export default {
props: {
as: {
type: String,
default: 'button'
}
},
}
</script>