Development Vue Accessibility | 2017-12-28

Popover component in Vue using slots, props and accessibility

As a team we re-designed the look and feel of an app and wanted a component for Popovers so we can more efficently build views. To me, a popover is a popup that shows when you hover over an icon and disappears after hover or mouse has left the region.

Simple vue component for popovers

Mixins were used to pass data into the popover, initially there were two props, title and content. But during grooming we found popover designs with two paragraphs instead of just one like we originally thought.

Two paragraphs of content opening to the left

When design saw the story they also mentioned that the popover should also open up to the left or right side, depending on which side of the page it’s on. (if the icon is on the right, open to the left)

Importing the shared popover

import InfoPopover from '_shared_/components/InfoPopover.vue';
export default {
    components: {
        InfoPopover
    }
};

Using the popover and slot

<info-popover title="Title">
    <p slot="popover-content">
        I'm an example of simple popover content.
    </p>
</info-popover>

The simple vue component that takes in a title prop as well as a slot for its content

<template>
    <button 
        ref="trigger" 
        type="button" 
        class="btn btn-info popover-tooltip-trigger" 
        :aria-describedby="uid" 
        :aria-expanded="this.popoverVisible || 'false'"
        @focus="showPopover()" 
        @mouseover="showPopover()" 
        @mouseout="hidePopover()" 
        @blur="hidePopover()"
    >
        <transition name="fade">
            <div v-show="this.popoverVisible" ref="popup" :id="uid" class="popover-tooltip" :style="{left: this.left + 'px', top: this.top + 'px'}">
                <div v-if="title" class="popover-title">{{ title }}</div>
                <div class="popover-content">
                    <slot name="popover-content">{{ content }}</slot>
                </div>
            </div>
        </transition>
    </button>
</template>

//using uid to aria-describedby because several popovers can be on a page at once
export default {
    data() {
        return {
            uid: `popover-info-${this._uid}`,
            popoverVisible: false,
            left: '',
            top: ''
        };
    },
    props: {
        title: {
            type: String,
            example: 'Title'
        }
    },
    methods: {
        togglePopover() {
            this.popoverVisible = !this.popoverVisible;
        },
        hidePopover() {
            this.popoverVisible = false;
        },
        showPopover() {
            if (!this.popoverVisible) {
                //show popover for one tick, then move because positioning is not available on the DOM until it's visible.
                this.popoverVisible = true;
                this.$nextTick(() => {
                    this.popupEl = this.$refs.popup;
                    const positionObject = this.getPosition(this.triggerEl, this.popupEl);
                    this.left = positionObject.left;
                    this.top = positionObject.top;
                    this.show = false;
                });
            }
        },
        getPosition(target, popup) {
            const windowWidth = document.documentElement.clientWidth;
            const popupHeight = popup.clientHeight;
            const popupWidth = popup.clientWidth;
            const targetPos = target.getBoundingClientRect();

            //position calculations
            const top = -popupHeight - 15;
            let left = 0;
            if (targetPos.left > (windowWidth / 2)) {
                //button is right side of screen, open popover to the left
                left = -popupWidth + 15;
            }

            return {
                left,
                top
            };
        }
    },
    mounted() {
        this.triggerEl = this.$refs.trigger;
        this.popupEl = this.$refs.popup;
    }
};
<style lang="scss">
.popover-tooltip-trigger {
    border: 0 none;
    border-radius:50%;
    background: transparent;
    position: relative;
    padding:0;
}

.popover-tooltip {
    width: 400px; //may need to override this for wider or less wide
    background-color: white;
    z-index: 1500;
    padding: 15px 15px 20px 15px;
    position: absolute;
    display: block;
    text-align: left;
    white-space: normal;
    background-color: #fff;
    border: 1px solid rgba(0, 0, 0, .2);
    border-radius: 6px;
    box-shadow: 0 2px 3px rgba(0, 0, 0, .3);
    border-color: #D2DADE;
    outline-color: #4d90fe; //FocusRingColor;
    outline-style: auto;

    .popover-content {
        padding: 0;
        font-size: 14px;
        color: black;
    }

    .popover-title {
        background-color: white;
        border-bottom: 1px solid black;
        font-size: 18px;
        line-height: 23px;
        padding: 0 0 5px 0;
        margin:  0 0 5px 0;
        color: black;
        font-weight: bold;
        position: relative;
    }
}

.fade-enter-active,
.fade-leave-active {
    transition: opacity .25s
}

.fade-enter,
.fade-leave-to {
    opacity: 0
}

Accessible popovers are tricky to get right, here's the URLs I referenced with this implementation

Reference documentation