cristinafsanz
2/25/2020 - 8:15 AM

Accessible Modal Dialogs

<template>
    <div
        class="c-modal-dialog"
        role="dialog"
        aria-labelledby="dialog-title"
        :class="[opened ? 'opened' : '']"
        @keydown.esc="closeModalDialog()"
    >
        <div class="c-modal-dialog__window">
            <div class="c-modal-dialog-content">
                <div class="c-modal-dialog-header">
                    <h2 id="dialog-title" class="c-modal-dialog-title">
                        <slot name="header"></slot>
                    </h2>
                </div>
                <div class="c-modal-dialog-body">
                    <slot name="body"></slot>
                    <div class="c-modal-dialog-actions">
                        <c-button
                            ref="cancel"
                            text="Cancel"
                            intent="secondary"
                            type="button"
                            class="c-modal-dialog-action-cancel"
                            @click.native="closeModalDialog()"
                        />
                        <slot name="ok"></slot>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import CButton from "~/components/cartier/CButton";
export default {
    components: {
        CButton: CButton,
    },

    data() {
        return {
            opened: false,
            previousActiveElement: null,
        };
    },

    watch: {
        "$store.state.dialog.opened": function() {
            this.opened = this.$store.state.dialog.opened;
        },
    },

    methods: {
        getFocusableElements(node) {
            return node.querySelectorAll(
                'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
            );
        },

        getFocusableElementsDialog() {
            return this.getFocusableElements(
                document.querySelector(".c-modal-dialog")
            );
        },

        getFocusableElementsPage() {
            return this.getFocusableElements(document);
        },

        openModalDialog() {
            // Grab a reference to the previous activeElement
            // We'll want to restore this when we close the dialog
            this.previousActiveElement = document.activeElement;

            // While Open, Prevent Tabbing to Outside the Dialog
            const elementsPage = Array.from(this.getFocusableElementsPage());
            const elementsDialog = Array.from(
                this.getFocusableElementsDialog()
            );

            elementsPage.forEach(child => {
                if (!elementsDialog.includes(child)) {
                    child.setAttribute("tabindex", "-1");
                }
            });

            // Make the dialog visible
            this.$store.commit("dialog/openDialog");

            // Move focus into the button cancel in the dialog
            this.$nextTick(() => this.$refs.cancel.$el.focus());
        },

        closeModalDialog() {
            // Close dialog
            this.$store.commit("dialog/closeDialog");

            // Allow Tabbing to Outside the Dialog
            const elementsPage = Array.from(this.getFocusableElementsPage());
            const elementsDialog = Array.from(
                this.getFocusableElementsDialog()
            );

            elementsPage.forEach(child => {
                if (!elementsDialog.includes(child)) {
                    child.setAttribute("tabindex", "0");
                }
            });

            // Restore focus to the previous active element
            if (this.previousActiveElement) {
                this.previousActiveElement.focus();
            }
        },
    },
};
</script>

<style scope lang="scss">
@import "~/assets/scss/variables";

.c-modal-dialog {
    display: none;

    &.opened {
        display: block;
    }

    &__window {
        display: inline-block;
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background-color: #ffffff;
        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
        border-radius: 4px;
        padding: 2rem;
        z-index: 101;
    }

    &-title {
        font-family: $font-secondary;
        font-size: 2.5rem;
        font-weight: 400;
        padding: 2rem 0;
    }

    &-actions {
        padding-top: 3rem;
        display: flex;
        justify-content: flex-end;
    }

    &-action-cancel {
        margin-right: 1rem;
    }
}
</style>
<template>
    <div
        class="c-modal-dialog-overlay"
        :class="[opened ? 'displayed' : '']"
        tabindex="-1"
    ></div>
</template>

<script>
export default {
    data() {
        return {
            opened: false,
        };
    },

    watch: {
        "$store.state.dialog.opened": function() {
            this.opened = this.$store.state.dialog.opened;
        },
    },
};
</script>

<style lang="scss" scoped>
.c-modal-dialog-overlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    background-color: #000000;
    opacity: 0.6;
    overflow: hidden;
    z-index: 100;

    &.displayed {
        display: block;
    }
}
</style>