Skip to content

Instantly share code, notes, and snippets.

@SigurdMW
Last active May 23, 2018 12:02
Show Gist options
  • Save SigurdMW/25275e59ab5b1a629635ad4771b62617 to your computer and use it in GitHub Desktop.
Save SigurdMW/25275e59ab5b1a629635ad4771b62617 to your computer and use it in GitHub Desktop.
Accessible vue modal in pure js
<template>
<div class="modal" :class="{ 'modal--open': isOpen }" role="dialog" tabindex="-1" :aria-labelledby="id">
<div class="modal__content">
<div class="modal__header">
<div class="modal__header-left">
<h2 class="modal__heading" :class="{ 'sr-only': headingIsSrOnly }" :id="id">{{ heading }}</h2>
<slot name="header"></slot>
</div>
<div class="modal__header-right">
<button class="modal__close" @click="closeModal" aria-label="Closed modal">
<span class="icon icon-close" aria-hidden="true"></span>
</button>
</div>
</div>
<div class="modal__body">
<slot></slot>
</div>
<div class="modal__footer">
<slot name="footer"></slot>
</div>
</div>
<div class="modal__overlay" @click="closeModal"></div>
</div>
</template>
<script>
// Thanks to https://codepen.io/matuzo/pen/GrNdvK?editors=0010
export default {
name: "ModalComponent",
props: {
open: {
type: Boolean,
required: true
},
defaultOpen: {
type: Boolean,
required: false,
default: false
},
heading: {
type: String,
required: true
},
headingIsSrOnly: {
type: Boolean,
required: false,
default: false
}
},
data () {
return {
id: 'modal-component-' + this._uid,
lastActiveElement: null,
firstTabStop: null,
lastTabStop: null,
isOpen: false,
focusableElementString: 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]'
}
},
mounted () {
if (this.defaultOpen) this.openModal();
},
watch: {
open (newValue, oldValue) {
if (newValue) {
this.openModal();
} else {
this.closeModal();
}
}
},
methods: {
openModal () {
this.lastActiveElement = document.activeElement;
var focusableElements = this.$el.querySelectorAll(this.focusableElementString);
// The first focusable element within the modal window
this.firstTabStop = focusableElements[0];
// The last focusable element within the modal window
this.lastTabStop = focusableElements[focusableElements.length - 1];
// To resolve issue with timing for setting focus in fist focusable element in modal
new Promise(resolve => {
this.isOpen = true;
resolve();
}).then(() => {
this.firstTabStop.focus();
});
this.$el.addEventListener("keydown", this.addKeyEvents);
},
addKeyEvents (e) {
// Listen for the Tab key
if (e.keyCode === 9) {
// If Shift + Tab
if (e.shiftKey) {
// If the current element in focus is the first focusable element within the modal window...
if (document.activeElement === this.firstTabStop) {
e.preventDefault();
// ...jump to the last focusable element
this.lastTabStop.focus();
}
// if Tab
} else {
// If the current element in focus is the last focusable element within the modal window...
if (document.activeElement === this.lastTabStop) {
e.preventDefault();
// ...jump to the first focusable element
this.firstTabStop.focus();
}
}
}
// Close the window by pressing the Esc-key
if (e.keyCode === 27) {
this.closeModal();
}
},
closeModal () {
this.isOpen = false;
this.$emit("modalClosed", this);
if (this.lastActiveElement) this.lastActiveElement.focus();
}
},
destroyed () {
this.$el.removeEventListener("keydown", this.addKeyEvents);
}
}
</script>
<style scoped>
$modal-z-index: $max-z-index - 100;
.modal {
$root: &;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow-x: hidden;
overflow-y: auto;
pointer-events: none;
z-index: $modal-z-index - 2;
&__overlay {
display: block;
transition: background-color 0.2s ease-in-out;
position: fixed;
justify-content: center;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
z-index: $modal-z-index - 1;
}
@at-root {
body#{$root}__showing {
height: auto;
overflow-y: hidden;
}
}
&--small {
& #{$root} {
&__content {
max-width: 450px;
width: 100%;
}
}
}
&__content {
display: none;
pointer-events: all;
opacity: 0;
position: relative;
margin: auto;
max-width: 750px;
width: 100%;
padding: 1em;
background-color: $color-white;
z-index: $modal-z-index;
transform: translateY(10px);
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
// to get scroll working
/*max-height: calc(100vh - 1em);*/
}
&--open {
pointer-events: auto;
& #{$root}__overlay {
pointer-events: all;
background-color: rgba(0,0,0,0.3);
}
& #{$root}__content {
display: block;
animation: modalAppear 0.2s ease-in-out 0.1s;
animation-fill-mode: forwards;
}
}
&__header {
display: flex;
align-items: center;
&-right,
&-left {
display: flex;
align-items: center;
}
&-left {
flex-grow: 1;
}
}
&__close {
display: flex;
min-width: 0;
width: 40px;
height: 40px;
padding: 0;
font-size: 0.65em;
justify-content: center;
align-items: center;
line-height: 1;
letter-spacing: 0;
border-width: 1px;
& .icon {
margin-top: 2px;
}
}
}
@keyframes modalAppear {
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment