Last active
May 23, 2018 12:02
-
-
Save SigurdMW/25275e59ab5b1a629635ad4771b62617 to your computer and use it in GitHub Desktop.
Accessible vue modal in pure js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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