This is a Vue component, BpDropdown, for implementing dropdowns. It’s used in the navigation components in conjunctions with BpDirectional.
hoverable
, how long to delay before closing after the the mouse leaves the dropdown.
<bp-dropdown href="/about" label="Our Company" id="js-about">
<nav class="dropdown__menu menu">
<ul class="menu__list">
<li class="menu__item">
<a class="menu__link" href=""><span>Sit a</span></a>
</li>
<li class="menu__item">
<a class="menu__link" href=""><span>Amet voluptas?</span></a>
</li>
<li class="menu__item">
<a class="menu__link" href=""><span>Sit exercitationem?</span></a>
</li>
<li class="menu__item">
<a class="menu__link" href=""><span>Dolor magni</span></a>
</li>
</ul>
</nav>
</bp-dropdown>
<template>
<bp-directional>
<div
ref="dropdown"
class="dropdown"
:class="{'-open': expanded}"
v-on="{ mouseleave, focusout, mouseover }"
>
<a
ref="link"
:aria-controls="id"
:aria-expanded="String(expanded)"
class="dropdown__link"
:class="labelClass"
:href="href"
@click.prevent="click"
@keydown.space.prevent="click"
>
<slot name="link">{{ label }}</slot>
<button
:aria-controls="id"
:aria-expanded="String(expanded)"
class="dropdown__button"
@click.prevent.stop="click"
>
<slot name="button">
<svg
class="dropdown__icon"
viewBox="0 0 24 24"
stroke-width="1"
stroke="currentColor"
fill="none"
><polyline points="6 9 12 15 18 9" /></svg>
</slot>
</button>
</a>
<transition :name="transition">
<div
v-if="expanded"
:id="id"
class="dropdown__content"
@keydown.esc.prevent="close"
>
<slot />
</div>
</transition>
</div>
</bp-directional>
</template>
<script>
import BpDirectional from '../../utilities/directional/BpDirectional'
const Timer = function () {
return {
timeout: null,
start (callback, delay) {
if (!this.timeout) {
this.timeout = setTimeout(callback, delay)
}
},
clear () {
if (this.timeout) {
clearTimeout(this.timeout)
this.timeout = null
}
},
}
}
export default {
components: {
BpDirectional,
},
props: {
delay: { type: Number, default: 0 },
hoverable: { type: Boolean, default: false },
href: { type: String, required: true },
id: { type: String, required: true },
label: { type: String, required: true },
labelClass: { type: String, default: '' },
transition: { type: String, default: 'dropdown__transition' },
},
data: () => ({
timer: new Timer(),
expanded: false,
}),
methods: {
mouseleave (evt) {
if (this.hoverable) {
this.timer.start(() => this.close(false), this.delay)
}
},
focusout (evt) {
if (this.expanded && !this.$el.contains(evt.relatedTarget)) {
this.close(false)
}
},
click (evt) {
if (this.expanded) {
this.close()
} else {
this.open()
}
},
mouseover () {
if (this.hoverable) {
this.timer.clear()
if (!this.expanded) {
this.open()
}
}
},
open () {
this.$emit('open')
this.timer.clear()
this.expanded = true
},
close (refocus = true) {
this.$emit('close')
this.timer.clear()
this.expanded = false
if (refocus) {
this.$refs.link.focus()
}
},
},
}
</script>
.dropdown {
position: relative;
&__link {
display: inline-block;
padding: $thin-padding;
z-index: 2;
}
&__button {
background: transparent;
border: 0;
padding: 0;
}
&__icon {
height: 1em;
vertical-align: middle;
width: 1em;
}
&__content {
position: absolute;
z-index: 1;
}
&.-open {
.dropdown__content {
background-color: $white;
box-shadow: $low-shadow;
}
}
&.-rightEdge {
.dropdown__content {
left: auto;
right: 0;
}
}
&.-fullWidth {
position: static;
.dropdown__content {
left: 0;
right: 0;
}
}
&__transition {
&-enter-active {
transform-origin: top;
transition: opacity $moderate, transform $moderate;
transform: rotateX(0);
}
&-enter,
&-leave-to {
opacity: 0;
transform: rotateX(90deg);
}
}
}