<button class="button modal-trigger" data-modal-trigger="myDialog" type="button" aria-expanded="false">
Modal trigger button
</button>
<div role="dialog" aria-hidden="true" id="myDialog" data-modal="myDialog" class="modal modal--tertiary" aria-labelledby="myTitle" aria-describedby="myDesc">
<div role="document" class="modal__container " tabindex="0">
<div class="modal__content ">
<div class="modal__top ">
<h3 class="modal__heading heading heading--third-level" id="myTitle">
Save "untitled" document?
</h3>
<p class="modal__subheading ">
Some text
</p>
</div>
<div class="modal__middle ">
<div class="
modal__description
margin-bottom-xs
" id="myDesc">
You have made changes to "untitled.txt" that have not been saved. What do you want to do?
</div>
<button class="button " type="button" aria-label="button">
I am a button
</button>
</div>
<div class="modal__bottom ">
<div class="modal__bottom-actions ">
<div class="
modal__bottom-action
">
<button class="button button--secondary" type="button" aria-label="button">
Cancel
</button>
</div>
<div class="
modal__bottom-action
">
<button class="button " type="button" aria-label="button">
Save
</button>
</div>
</div>
</div>
</div>
<button class="button button--icon button--rotate-icon modal__close-button" type="button" aria-label="click to close the modal">
<svg class="icon button__icon modal__close-button-icon" role="presentation" focusable="false">
<title>Close</title>
<use href="/images/icons-sprite.svg#close"></use>
</svg>
</button>
</div>
</div>
<script type="text/javascript">
new Modal(document.querySelector('[data-modal-trigger="myDialog"]'));
</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__container {{ modalContainer.class }}"
{{{ modalContainer.attribtues }}}
tabindex="0"
>
<div
class="modal__content {{ modalContent.class }}"
{{{ modalContent.attribtues }}}
>
{{#if modalTop }}
<div class="modal__top {{ modalTopClass }}">
{{#if heading.text }}
<{{ heading.tag }}
class="modal__heading {{ heading.class }}"
{{{ heading.attributes }}}
>
{{ heading.text }}
</{{ heading.tag }}>
{{/if}}
{{#if subheading.text }}
<{{ subheading.tag }}
class="modal__subheading {{ subheading.class }}"
{{{ subheading.attributes }}}
>
{{ subheading.text }}
</{{ subheading.tag }}>
{{/if}}
</div>
{{/if}}
{{#if modalMiddle }}
<div class="modal__middle {{ modalMiddleClass }}">
{{#if modalDescription.text }}
<{{ modalDescription.tag }}
class="
modal__description
{{ modalDescription.class }}
"
{{{ modalDescription.attributes }}}
>
{{ modalDescription.text }}
</{{ modalDescription.tag }}>
{{/if}}
{{#if modalComponent.content }}
{{ render (component modalComponent.content) modalComponent.contentContext }}
{{/if}}
</div>
{{ else }}
{{#if modalComponent.content }}
{{ render (component modalComponent.content) modalComponent.contentContext }}
{{/if}}
{{/if}}
{{#if modalBottom }}
<div class="modal__bottom {{ modalBottomClass }}">
<div class="modal__bottom-actions {{ modalBottomActionsClass }}">
{{#each modalBottomActions as |action| }}
<div
class="
modal__bottom-action
{{ action.class }}
"
{{ action.attributes }}
>
{{ render (component action.content) action.contentContext merge=true }}
</div>
{{/each}}
</div>
</div>
{{/if}}
</div>
{{#if buttonClose }}
{{ render '@button--rotate-icon' buttonClose merge=true }}
{{/if}}
</div>
</div>
{{#if modal.id}}
<script type="text/javascript">
new Modal(document.querySelector('[data-modal-trigger="{{ modal.id }}"]'));
</script>
{{/if}}
{
"modalContainer": {
"class": ""
},
"modal": {
"class": "modal--tertiary",
"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\" aria-expanded=\"false\""
}
},
"buttonClose": {
"tag": "button",
"text": "",
"class": "button--rotate-icon modal__close-button",
"icon": {
"id": "close",
"title": "Close",
"class": "button__icon modal__close-button-icon"
},
"attributes": "type=\"button\" aria-label=\"click to close the modal\""
},
"modalTop": true,
"heading": {
"attributes": "id=\"myTitle\"",
"tag": "h3",
"class": "heading heading--third-level",
"text": "Save \"untitled\" document?"
},
"subheading": {
"tag": "p",
"class": "",
"text": "Some text"
},
"modalMiddle": true,
"modalDescription": {
"attributes": "id=\"myDesc\"",
"class": "margin-bottom-xs",
"tag": "div",
"text": "You have made changes to \"untitled.txt\" that have not been saved. What do you want to do?"
},
"modalComponent": {
"content": "button",
"contentContext": ""
},
"modalBottom": true,
"modalBottomActions": [
{
"content": "button--secondary",
"contentContext": {
"text": "Cancel"
}
},
{
"content": "button",
"contentContext": {
"text": "Save"
}
}
]
}
// -----------------------------------------------------------
// Please keep balance between Magento_UI and Alpaca z-indexes
// -----------------------------------------------------------
// COMPONENT - z-index OVERLAY / CONTENT (method)
// -----------------------------------------------------------
// Alpaca Modal - 99 / 99 (Styles)
// Magento_UI Modal - 99 / 100 (Inlined via JS)
// Magento_UI Confirmation Modal - 100 / 101 (Inlined via JS)
// -----------------------------------------------------------
$modal__z-index : $z-index-highest !default;
$modal-ui-base__z-index : $z-index-highest + 1 !default;
$modal-ui-confirm__z-index : $z-index-highest + 2 !default;
// -----------------------------------------------------------
$modal__padding : 0 !default;
$modal__background-color : rgba(0, 0, 0, 0.7) !default;
$modal__justify-content : center !default;
$modal__align-items : center !default;
$modal__border : 0 !default;
//modal container
$modal__container-width : calc(100% - (2 * #{$spacer--medium})) !default;
$modal__container-max-width : 900px !default;
$modal__container-max-height : 100% !default;
$modal__container-padding : $spacer--semi-large $spacer--medium !default;
$modal__container-padding\@medium : $spacer--semi-large !default;
$modal__container-background-color : $white !default;
$modal__container-border : none !default;
$modal__container-box-shadow : $shadow !default;
$modal__container-animation : animatetop 0.4s !default;
//modal cookies container
$modal__cookies-container-max-width : 1328px !default;
$modal__cookies-container-max-height : calc(100vh - (2 * #{$spacer--medium})) !default;
//modal close
$modal__close-button-top : $spacer--medium !default;
$modal__close-button-right : $spacer--medium !default;
//modal top
$modal__top-padding : 0 0 $spacer--semi-medium 0 !default;
$modal__top-margin : 0 !default;
$modal__top-border : $border-base !default;
$modal__top-border-width : 0 0 $border-width-base 0 !default;
//modal middle
$modal__middle-padding : $spacer--semi-large !default;
$modal__middle-margin : 0 !default;
$modal__middle-border : 0 !default;
$modal__middle-border-width : 0 !default;
//modal bottom
$modal__bottom-padding : $spacer--semi-large 0 0 0 !default;
$modal__bottom-margin : 0 !default;
$modal__bottom-border : $border-base !default;
$modal__bottom-border-width : $border-width-base 0 0 0 !default;
//modal heading
$modal__heading-font-size : $font-size-large !default;
$modal__heading-font-weight : $font-weight-base !default;
$modal__heading-margin : 0 !default;
$modal__heading-height : 48px !default;
$modal__heading-padding-right : $spacer--extra-large !default;
$modal__heading-padding-left : 0 !default;
//modal subheading
$modal__subheading-font-size : $font-size-base !default;
$modal__subheading-font-weight : $font-weight-base !default;
$modal__subheading-margin : 0 !default;
$modal__subheading-height : 24px !default;
$modal__subheading-padding-right : 0 !default;
$modal__subheading-padding-left : 0 !default;
//modal bottom-actions
$modal__bottom-actions-padding : 0 !default;
$modal__bottom-actions-margin : 0 auto !default;
$modal__bottom-actions-justify-content : space-between !default;
$modal__bottom-actions-max-width : 320px !default;
//modal bottom-action
$modal__bottom-action-padding : 0 !default;
$modal__bottom-action-margin : 0 0 $spacer--medium 0 !default;
$modal__bottom-action-width : calc(50% - #{$spacer}) !default;
$modal__bottom-action-button-width : 100% !default;
//modal--secondary
$modal__justify-content--secondary : center !default;
$modal__align-items--secondary : flex-end !default;
$modal__container-animation--secondary : animateright 0.4s !default;
$modal__container-height--secondary : 100% !default;
$modal__container-width--secondary : 100% !default;
$modal__container-max-width--secondary : 640px !default;
$modal__container-max-width--secondary\@large: 768px !default;
$modal__content-height--secondary : 100% !default;
$modal__container-padding--secondary\@medium : $spacer--extra-large !default;
$modal__container-padding--secondary\@large : $spacer--extra-large 112px !default;
//modal--tertiary
$modal__justify-content--tertiary : center !default;
$modal__align-items--tertiary : flex-start !default;
$modal__container-animation--tertiary : animateleft 0.4s !default;
$modal__container-height--tertiary : 100vh !default;
$modal__container-width--tertiary : 100% !default;
$modal__container-max-width--tertiary : 100% !default;
$modal__container-box-shadow--tertiary : none !default;
@import 'modal-variables';
.modal {
display: none;
position: fixed;
left: 0;
top: 0;
z-index: $modal__z-index;
width: 100%;
height: 100%;
padding: $modal__padding;
background-color: $modal__background-color;
border: $modal__border;
&--active {
display: flex;
flex-direction: column;
justify-content: $modal__justify-content;
align-items: $modal__align-items;
}
&--secondary {
justify-content: $modal__justify-content--secondary;
align-items: $modal__align-items--secondary;
.modal__container {
height: $modal__container-height--secondary;
width: $modal__container-width--secondary;
max-width: $modal__container-max-width--secondary;
animation: $modal__container-animation--secondary;
@include mq($screen-m) {
padding: $modal__container-padding--secondary\@medium;
}
@include mq($screen-l) {
max-width: $modal__container-max-width--secondary\@large;
padding: $modal__container-padding--secondary\@large;
}
}
.modal__content {
height: $modal__content-height--secondary;
}
}
&--tertiary {
justify-content: $modal__justify-content--tertiary;
align-items: $modal__align-items--tertiary;
.modal__container {
height: $modal__container-height--tertiary;
width: $modal__container-width--tertiary;
max-width: $modal__container-max-width--tertiary;
animation: $modal__container-animation--tertiary;
box-shadow: $modal__container-box-shadow--tertiary;
}
}
&__container {
position: relative;
display: block;
width: $modal__container-width;
max-width: $modal__container-max-width;
max-height: $modal__container-max-height;
padding: $modal__container-padding;
border: $modal__container-border;
box-shadow: $modal__container-box-shadow;
background-color: $modal__container-background-color;
animation: $modal__container-animation;
@include mq($screen-m) {
padding: $modal__container-padding\@medium;
}
}
&__cookies-container {
max-width: $modal__cookies-container-max-width;
max-height: $modal__cookies-container-max-height;
overflow-y: auto;
}
&__content {
display: flex;
flex-direction: column;
max-height: 100%;
&--block {
display: block;
}
}
&__top {
padding: $modal__top-padding;
margin: $modal__top-margin;
border: $modal__top-border;
border-width: $modal__top-border-width;
}
&__middle {
padding: $modal__middle-padding;
margin: $modal__middle-margin;
border: $modal__middle-border;
border-width: $modal__middle-border-width;
overflow-y: auto;
}
&__bottom {
padding: $modal__bottom-padding;
margin: $modal__bottom-margin;
border: $modal__bottom-border;
border-width: $modal__bottom-border-width;
}
&__heading {
font-size: $modal__heading-font-size;
font-weight: $modal__heading-font-weight;
margin: $modal__heading-margin;
@include font-padding(
$modal__heading-font-size,
$modal__heading-height,
$modal__heading-padding-right,
$modal__heading-padding-left
);
}
&__subheading {
font-size: $modal__subheading-font-size;
font-weight: $modal__subheading-font-weight;
margin: $modal__subheading-margin;
@include font-padding(
$modal__subheading-font-size,
$modal__subheading-height,
$modal__subheading-padding-right,
$modal__subheading-padding-left
);
}
&__bottom-actions {
display: flex;
flex-wrap: wrap;
justify-content: $modal__bottom-actions-justify-content;
padding: $modal__bottom-actions-padding;
margin: $modal__bottom-actions-margin;
max-width: $modal__bottom-actions-max-width;
}
&__bottom-action {
padding: $modal__bottom-action-padding;
margin: $modal__bottom-action-margin;
flex-basis: $modal__bottom-action-width;
&:last-child {
margin-bottom: 0;
}
.button {
width: $modal__bottom-action-button-width;
}
}
&__close-button {
position: absolute;
top: $modal__close-button-top;
right: $modal__close-button-right;
}
}
// Magento_UI
.modal-custom,
.modal-popup {
z-index: $modal-ui-base__z-index;
}
.modal-popup {
&.confirm {
z-index: $modal-ui-confirm__z-index;
}
.button--rotate-icon {
&.modal__close-button {
position: absolute;
top: $modal__close-button-top;
right: $modal__close-button-right;
}
}
}
'use strict';
class Modal { // eslint-disable-line
constructor(modalTrigger) {
this.setListeners(modalTrigger);
}
trap(e, modal) {
if (e.which == 27) {
this.closeModal(modal);
}
if (e.which == 9) {
let currentFocus = document.activeElement;
let totalOfFocusable = modal.focusableChildren.length;
let 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.trigger.setAttribute('aria-expanded', true);
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.trigger.setAttribute('aria-expanded', false);
modal.el.classList.remove(modal.activeClass);
modal.focused.focus();
}
setListeners(modalTrigger) {
const modal = {};
modal.trigger = modalTrigger,
modal.el = document.querySelector(`.modal[data-modal=${modal.trigger.dataset.modalTrigger}]`),
modal.content = modal.el.querySelector('.modal__content'),
modal.closeButton = modal.el.querySelector('.modal__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 = '';
modal.trigger.addEventListener('click',
() => this.openModal(modal)
);
// clicking on button (x) closes the modal
if (modal.closeButton) {
modal.closeButton.addEventListener('click',
() => this.closeModal(modal)
);
}
// clicking anywhere outside of the modal closes 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)
}
});
// escape key closes the modal
window.addEventListener('keydown', (e) => {
if (e.which === 27
&& modal.el.classList.contains(modal.activeClass)
) {
this.closeModal(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.