<button class="button modal-trigger" data-modal-trigger="myDialog" type="button">
Modal trigger button
</button>
<div role="dialog" aria-hidden="true" id="myDialog" data-modal="myDialog" class="modal " aria-labelledby="myTitle" aria-describedby="myDesc">
<div role="document" class="modal__content " tabindex="0">
<h3 class="heading heading--third-level" id="myTitle"">
Save "untitled" document?
</h3>
<div class=" modal__description "
">
You have made changes to "untitled.txt" that have not been saved. What do you want to do?
</div>
<button class="button button--icon modal__js-close-button modal__close-button" type="button" aria-label="close modal button, click to close the modal">
<svg class="icon button__icon modal__close-button-icon" role="img">
<title>Close</title>
<use xlink:href="/images/icons-sprite.svg#close"></use>
</svg>
</button>
</div>
</div>
<script src="/components/raw/modal/modal.js"></script>
{{#if trigger }}
{{ render '@modal-trigger' modalTrigger }}
{{/if}}
<div role="dialog"
aria-hidden="true"
id="{{ modal.id }}"
data-modal="{{ modal.id }}"
class="modal {{ modal.class }}"
{{{ modal.attributes }}}
>
<div role="document"
class="modal__content {{ modalContent.class }}"
{{{ modalContent.attribtues }}}
tabindex="0"
>
{{#if header.text }}
<{{header.tag}} class="{{header.class}}" {{{ header.attributes }}}">
{{header.text}}
</{{header.tag}}>
{{/if}}
{{#if description.text }}
<{{description.tag}} class="modal__description {{description.class}}"
{{{ attributes }}}"
>
{{description.text}}
</{{description.tag}}>
{{/if}}
{{#if main.content }}
{{ render (component main.content) main.contentContext }}
{{/if}}
{{#if buttonClose }}
{{ render '@button--icon' buttonClose merge=true}}
{{/if}}
</div>
</div>
{{#if script }}
<script src="{{static 'modal.js' }}"></script>
{{/if}}
{
"modalContent": {
"class": ""
},
"modal": {
"class": "",
"id": "myDialog",
"attributes": "aria-labelledby=\"myTitle\" aria-describedby=\"myDesc\""
},
"trigger": true,
"modalTrigger": {
"buttonModalTrigger": {
"tag": "button",
"class": "modal-trigger",
"text": "Modal trigger button",
"attributes": "data-modal-trigger=\"myDialog\" type=\"button\""
}
},
"header": {
"attributes": "id=\"myTitle\"",
"tag": "h3",
"class": "heading heading--third-level",
"text": "Save \"untitled\" document?"
},
"description": {
"attributes": "id=\"myDesc\"",
"class": "",
"tag": "div",
"text": "You have made changes to \"untitled.txt\" that have not been saved. What do you want to do?"
},
"main": {
"content": "",
"contentContext": ""
},
"buttonClose": {
"tag": "button",
"text": "",
"class": "button--icon modal__js-close-button modal__close-button",
"icon": {
"id": "close",
"title": "Close",
"class": "button__icon modal__close-button-icon"
},
"attributes": "type=\"button\" aria-label=\"close modal button, click to close the modal\""
},
"script": true
}
$modal__padding : 0 !default;
$modal__background-color : rgba(0, 0, 0, 0.7) !default;
$modal__description-margin : 0 0 $spacer 0 !default;
$modal__content-width : calc(100% - (2 * #{$spacer--medium})) !default;
$modal__content-width\@extra-large : 80% !default;
$modal__content-max-width : 900px !default;
$modal__content-background-color : $white !default;
$modal__content-border : none !default;
$modal__content-box-shadow : 0 4px 6px 0 rgba(0, 0, 0, 0.3) !default;
$modal__close-button-top : $spacer !default;
$modal__close-button-right : $spacer !default;
$modal__close-button-border : none !default;
$modal__close-button-background-color: $gray-darker !default;
$modal__close-button-icon-color : $white !default;
.modal {
position: fixed;
left: 0;
top: 0;
z-index: 99;
display: none;
width: 100%;
height: 100%;
padding: $modal__padding;
background-color: $modal__background-color;
&--active {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
&__description {
margin: $modal__description-margin;
}
&__content {
position: relative;
display: block;
width: $modal__content-width;
max-width: $modal__content-max-width;
padding: $spacer;
border: $modal__content-border;
box-shadow: $modal__content-box-shadow ;
background-color: $modal__content-background-color;
overflow-y: auto;
animation-name: animatetop;
animation-duration: 0.4s;
@include mq($screen-xl) {
width: $modal__content-width\@extra-large;
}
}
&__close-button {
border: $modal__close-button-border;
border-radius: 0;
background-color: $modal__close-button-background-color;
&-icon {
fill: $modal__close-button-icon-color;
width: 20px;
height: 20px;
}
}
@keyframes animatetop {
from {
top: -300px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
}
body .modal-popup .modal-inner-wrap {
margin: 1rem auto;
height: 97vh;
border-radius: 10px;
}
.modal-popup .modal-header {
padding-top: 2.1rem;
padding-bottom: 1.2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-popup .modal-footer {
margin-top: auto;
display: flex;
justify-content: flex-end;
padding-top: 0;
padding-bottom: 2rem;
}
class Modal {
constructor() {
this.triggers = document.querySelectorAll('.modal-trigger');
this.init();
}
trap(e, modal) {
if (e.which == 27) {
this.closeModal(modal);
}
if (e.which == 9) {
let currentFocus = document.activeElement,
totalOfFocusable = modal.focusableChildren.length,
focusedIndex = modal.focusableChildren.indexOf(currentFocus);
if (e.shiftKey) {
if (focusedIndex === 0) {
e.preventDefault();
modal.focusableChildren[totalOfFocusable - 1].focus();
}
}
else {
if (focusedIndex == totalOfFocusable - 1) {
e.preventDefault();
modal.focusableChildren[0].focus();
}
}
}
}
openModal(modal) {
modal.focused = document.activeElement;
modal.el.setAttribute('aria-hidden', false);
modal.el.classList.add(modal.activeClass);
modal.focusableChildren = Array.from(modal.el.querySelectorAll(modal.focusable));
modal.focusableChildren[0].focus();
modal.el.addEventListener('keydown', (e) => {
this.trap(e, modal);
});
}
closeModal(modal) {
modal.el.setAttribute('aria-hidden', true);
modal.el.classList.remove(modal.activeClass);
modal.focused.focus();
}
setListeners() {
this.triggers.forEach(trigger => {
const modal = {};
modal.triggerId = trigger.dataset.modalTrigger,
modal.el = document.querySelector(`.modal[data-modal=${modal.triggerId}]`),
modal.content = modal.el.querySelector('.modal__content'),
modal.closeButton = [...modal.el.querySelectorAll('.modal__js-close-button')],
modal.activeClass = 'modal--active',
modal.focusable = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), object, embed, *[tabindex], *[contenteditable]',
modal.focused = '';
/// When the user clicks on trigger, open the modal
trigger.addEventListener('click',
() => this.openModal(modal)
);
// When the user clicks on button (x), close the modal
if (modal.closeButton.length > 0) {
modal.closeButton.forEach(closeButton => {
closeButton.addEventListener('click',
() => this.closeModal(modal)
);
});
}
// When the user clicks anywhere outside of the modal, close the modal
window.addEventListener('click', (e) => {
if (e.target === modal.el
&& modal.el.classList.contains(modal.activeClass)
&& !modal.content.contains(e.target)
) {
this.closeModal(modal)
}
});
// When the user push escape, close the modal
window.addEventListener('keydown', (e) => {
if (e.which === 27
&& modal.el.classList.contains(modal.activeClass)
) {
this.closeModal(modal)
}
});
})
}
init() {
this.setListeners();
}
}
new Modal();
During implementation, please add to div[role="dialog"]
two aria attributes which help screen readers to tell what is modal about:
aria-labelledby
- as a value pass the id of title element, the text if this element will be writtenaria-describedby
- as a value pass the id of description element, the text if this element will be writtenCode example:
<div role="dialog"
aria-hidden="true"
id="myDialog"
data-modal="myDialog"
class="modal"
tabindex="-1"
aria-labelledby="myTitle"
aria-describedby="myDesc"
>
<div role="document" class="modal__content" tabindex="0">
<div id="myTitle">Save "untitled" document?</div>
<div id="myDesc">You have made changes to "untitled.txt" that have not been saved. What do you want to do?</div>
<button id="saveMe" type="button">Save changes</button>
<button id="discardMe" type="button">Discard changes</button>
<button id="neverMind" type="button">Cancel</button>
</div>
</div>
aria-label
attribute for it.