This is a Vue component, BpDropdown, for implementing dropdowns. It’s used in the navigation components in conjunctions with BpDirectional.

Usage

Props

  • delay (default: 0) – If hoverable, how long to delay before closing after the the mouse leaves the dropdown.
  • hoverable (default: false) – When set, the dropdown will open on hover, otherwise it will only open on click.
  • href (required) – The href for the link that’s overloaded to also open the dropdown.
  • id (required) – Required ID attribute value for accessibility purposes.
  • label (required) – Required next for the link.
  • labelClass (default: ‘’) – Allows adding additional classes to the link used to open the dropdown.
  • transition (default: ‘dropdown__transition’) – The name of a vue transition to use for the dropdown.

Slots

  • default – The default slot is used for the content within the dropdown.
  • link – Optional slot for providing more markup for the link than just ``.
  • button – Optional slot for overriding content of the button used to open the dropdown. The default value is an SVG chevron pointing downward.

Events

  • open – Emitted without a payload when the dropdown is opened.
  • close – Emitted without a payload when the dropdown is closed.
<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>
  • Content:
    <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>
    
  • URL: /components/raw/dropdown/BpDropdown.vue
  • Filesystem Path: resources/styles/atoms/dropdown/BpDropdown.vue
  • Size: 3.7 KB
  • Content:
    .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);
            }
        }
    }
    
  • URL: /components/raw/dropdown/dropdown.scss
  • Filesystem Path: resources/styles/atoms/dropdown/dropdown.scss
  • Size: 1.1 KB