<div class="modal js-modal " aria-modal="true" role="dialog" id="modal" aria-labelledby="modal_label" data-close-label="close">
<h1 id="modal_label" class="h3 modal__title ">
Leave feedback
</h1>
<div class="modal__content">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe.
</div>
</div>
<div class="modal {{ modifier }} {{ class }} "
aria-modal="true"
role="dialog"
id="{{ id }}"
aria-labelledby="{{ id }}_label"
data-close-label="{{ data.closeLabel }}"
{% if descriptionId %}
aria-describedby="{{ descriptionId }}"
{% endif %}
{% if focusableElementId %}
data-focusable-element="{{ focusableElementId }}"
{% endif %}
>
<{{ headingElement|default('h1') }} id="{{ id }}_label" class="h3 modal__title {{ data.title.isHidden ? 'h-visually-hidden' : '' }}">
{{ data.title.text }}
</{{ headingElement|default('h1') }}>
<div class="modal__content">
{{ data.content }}
</div>
</div>
{
"language": "en-US",
"class": "js-modal",
"id": "modal",
"data": {
"title": {
"text": "Leave feedback"
},
"closeLabel": "close",
"content": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe."
}
}
.modal-container {
display: none;
position: relative;
z-index: map-get($zindex, 'modal');
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba($L-text-black, .25);
&.is-visible {
display: flex;
flex-direction: column;
}
}
.modal-container__inner {
display: flex;
align-items: flex-start;
align-self: center;
margin: auto;
padding: 0;
width: 100%;
min-height: 100vh;
@include bp(md-min) {
padding: 72px 12px 64px;
}
}
.modal {
display: none;
position: relative;
padding: 52px 12px 12px;
margin: 0 auto;
background: $L-background;
border-radius: 0;
@include bp(md-min) {
padding: 64px 24px 24px;
border-radius: 8px;
width: columnWidth(9);
}
.modal-container & {
display: block;
pointer-events: auto;
}
}
.modal--size-small {
@include bp(md-min) {
width: columnWidth(5);
}
}
.modal--size-medium {
@include bp(md-min) {
width: columnWidth(6);
}
}
.modal__title {
margin-bottom: 16px;
text-align: center;
@include bp(md-min) {
margin-bottom: 24px;
}
.modal--modify-product & {
text-align: left;
text-transform: uppercase;
margin-top: 40px;
@include bp(lg-min) {
text-align: center;
}
}
}
.modal__close {
display: inline-flex;
position: absolute;
top: 12px;
right: 12px;
padding: 8px;
border: 0;
border-radius: $border-radius-round;
background: transparent;
color: $L-background-strong;
font-size: 24px;
transition: color $transition-duration $transition-easing;
cursor: pointer;
@include bp(md-min) {
top: 24px;
right: 24px;
padding: 0;
}
&:hover {
color: $L-background-strong-hover;
}
}
.modal--modify-product {
padding: 24px 12px 12px;
background: $L-background-light;
@include bp(lg-min) {
padding: 24px;
}
}
import { focusable, FocusableElement } from 'tabbable';
import Component from '../component/component';
import Helpers from '../helpers/helpers';
import Icon from '../icon/icon';
import './modal.scss';
interface IModalSettings {
closeLabel: string;
focusableElement?: string;
}
export default class Modal extends Component {
static initSelector: string = '.js-modal';
closeButton: JQuery;
container: JQuery;
hasContainer: boolean;
inner: JQuery;
keyboardHandler: () => void;
trigger: JQuery;
isOpen: boolean;
focusableElements: FocusableElement[];
firstFocusableElement: FocusableElement;
lastFocusableElement: FocusableElement;
settings: JQuery.PlainObject<IModalSettings>;
timeout: number;
constructor(target: HTMLElement, trigger: JQuery, open: boolean = true) {
super(target);
this.hasContainer = false;
this.isOpen = false;
this.container = $('<div class="modal-container"></div>');
this.inner = $('<div class="modal-container__inner"></div>');
this.settings = this.element.data();
this.closeButton = $(`<button type="button" class="modal__close" aria-label="${this.settings.closeLabel}">${Icon.render('close', 'modal__close-icon')}</button>`);
this.keyboardHandler = this.keyboard.bind(this);
if (open) {
this.open(trigger);
}
}
bindEventHandlers(): void {
this.container.on('click', (event: JQuery.TriggeredEvent): void => {
if (event.target === event.currentTarget || event.target === this.inner[0]) {
event.preventDefault();
this.close();
}
});
this.closeButton.on('click', (event: JQuery.Event): void => {
event.preventDefault();
this.close();
});
}
open(trigger: JQuery): void {
this.trigger = trigger;
if (!this.hasContainer) {
this.inner.append(this.element);
this.container.append(this.inner);
this.element.prepend(this.closeButton);
$('body').append(this.container);
this.bindEventHandlers();
this.hasContainer = true;
}
Helpers.disableScroll();
Helpers.disablePageInteractions();
this.container.addClass('is-visible');
this.setInitialFocus();
this.isOpen = true;
this.element.on('keyup.modal', this.keyboardHandler);
this.element.on('keydown.modal', this.keyboardHandler);
}
close(): void {
this.container.removeClass('is-visible');
Helpers.enablePageInteractions();
this.isOpen = false;
this.timeout = window.setTimeout(() => {
this.trigger.focus();
}, 10);
Helpers.enableScroll();
this.element.off('keyup.modal', this.keyboardHandler);
this.element.off('keydown.modal', this.keyboardHandler);
}
keyboard(event: JQuery.TriggeredEvent): void {
if (event.type === 'keyup' && event.key === 'Escape') {
this.close();
}
if (event.type === 'keydown') {
this.focusableElements = focusable(this.element.get(0));
this.firstFocusableElement = this.focusableElements[0];
this.lastFocusableElement = this.focusableElements[this.focusableElements.length - 1];
if (event.key === 'Tab' && !event.shiftKey) {
this.switchFocusedElements(event, 'next');
}
if (event.key === 'Tab' && event.shiftKey) {
this.switchFocusedElements(event, 'previous');
}
}
}
destroy(): void {
clearTimeout(this.timeout);
}
private switchFocusedElements(event: JQuery.TriggeredEvent, direction: string): void {
if (direction === 'next' && event.target.isEqualNode(this.lastFocusableElement)) {
event.preventDefault();
this.firstFocusableElement.focus();
return;
}
if (direction === 'previous' && event.target.isEqualNode(this.firstFocusableElement)) {
event.preventDefault();
this.lastFocusableElement.focus();
}
}
private setInitialFocus(): void {
if (typeof this.settings.focusableElement === 'string') {
const focusableElement: JQuery = this.element.find(`#${this.settings.focusableElement}`);
if (focusableElement.length > 0) {
focusableElement.focus();
return;
}
}
this.element.find('.modal__close').focus();
}
}
$(document).on('click', '[data-js="open-modal"]', (event: JQuery.TriggeredEvent): void => {
event.preventDefault();
const trigger: JQuery = $(event.currentTarget);
const element: JQuery = $(trigger.attr('href'));
const type: string = Component.getDataName(Modal.initSelector);
const dataAttr: string = type + 'Class';
if (element.data(dataAttr)) {
const instance: Modal = element.data(dataAttr);
instance.open(trigger);
} else {
const instance: Modal = new Modal(element[0], trigger, true);
element.data(dataAttr, instance);
}
});
<div class="modal modal--size-small js-modal " aria-modal="true" role="dialog" id="modal" aria-labelledby="modal_label" data-close-label="close">
<h1 id="modal_label" class="h3 modal__title ">
Leave feedback
</h1>
<div class="modal__content">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe.
</div>
</div>
<div class="modal {{ modifier }} {{ class }} "
aria-modal="true"
role="dialog"
id="{{ id }}"
aria-labelledby="{{ id }}_label"
data-close-label="{{ data.closeLabel }}"
{% if descriptionId %}
aria-describedby="{{ descriptionId }}"
{% endif %}
{% if focusableElementId %}
data-focusable-element="{{ focusableElementId }}"
{% endif %}
>
<{{ headingElement|default('h1') }} id="{{ id }}_label" class="h3 modal__title {{ data.title.isHidden ? 'h-visually-hidden' : '' }}">
{{ data.title.text }}
</{{ headingElement|default('h1') }}>
<div class="modal__content">
{{ data.content }}
</div>
</div>
{
"language": "en-US",
"class": "js-modal",
"id": "modal",
"data": {
"title": {
"text": "Leave feedback"
},
"closeLabel": "close",
"content": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe."
},
"modifier": "modal--size-small"
}
.modal-container {
display: none;
position: relative;
z-index: map-get($zindex, 'modal');
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba($L-text-black, .25);
&.is-visible {
display: flex;
flex-direction: column;
}
}
.modal-container__inner {
display: flex;
align-items: flex-start;
align-self: center;
margin: auto;
padding: 0;
width: 100%;
min-height: 100vh;
@include bp(md-min) {
padding: 72px 12px 64px;
}
}
.modal {
display: none;
position: relative;
padding: 52px 12px 12px;
margin: 0 auto;
background: $L-background;
border-radius: 0;
@include bp(md-min) {
padding: 64px 24px 24px;
border-radius: 8px;
width: columnWidth(9);
}
.modal-container & {
display: block;
pointer-events: auto;
}
}
.modal--size-small {
@include bp(md-min) {
width: columnWidth(5);
}
}
.modal--size-medium {
@include bp(md-min) {
width: columnWidth(6);
}
}
.modal__title {
margin-bottom: 16px;
text-align: center;
@include bp(md-min) {
margin-bottom: 24px;
}
.modal--modify-product & {
text-align: left;
text-transform: uppercase;
margin-top: 40px;
@include bp(lg-min) {
text-align: center;
}
}
}
.modal__close {
display: inline-flex;
position: absolute;
top: 12px;
right: 12px;
padding: 8px;
border: 0;
border-radius: $border-radius-round;
background: transparent;
color: $L-background-strong;
font-size: 24px;
transition: color $transition-duration $transition-easing;
cursor: pointer;
@include bp(md-min) {
top: 24px;
right: 24px;
padding: 0;
}
&:hover {
color: $L-background-strong-hover;
}
}
.modal--modify-product {
padding: 24px 12px 12px;
background: $L-background-light;
@include bp(lg-min) {
padding: 24px;
}
}
import { focusable, FocusableElement } from 'tabbable';
import Component from '../component/component';
import Helpers from '../helpers/helpers';
import Icon from '../icon/icon';
import './modal.scss';
interface IModalSettings {
closeLabel: string;
focusableElement?: string;
}
export default class Modal extends Component {
static initSelector: string = '.js-modal';
closeButton: JQuery;
container: JQuery;
hasContainer: boolean;
inner: JQuery;
keyboardHandler: () => void;
trigger: JQuery;
isOpen: boolean;
focusableElements: FocusableElement[];
firstFocusableElement: FocusableElement;
lastFocusableElement: FocusableElement;
settings: JQuery.PlainObject<IModalSettings>;
timeout: number;
constructor(target: HTMLElement, trigger: JQuery, open: boolean = true) {
super(target);
this.hasContainer = false;
this.isOpen = false;
this.container = $('<div class="modal-container"></div>');
this.inner = $('<div class="modal-container__inner"></div>');
this.settings = this.element.data();
this.closeButton = $(`<button type="button" class="modal__close" aria-label="${this.settings.closeLabel}">${Icon.render('close', 'modal__close-icon')}</button>`);
this.keyboardHandler = this.keyboard.bind(this);
if (open) {
this.open(trigger);
}
}
bindEventHandlers(): void {
this.container.on('click', (event: JQuery.TriggeredEvent): void => {
if (event.target === event.currentTarget || event.target === this.inner[0]) {
event.preventDefault();
this.close();
}
});
this.closeButton.on('click', (event: JQuery.Event): void => {
event.preventDefault();
this.close();
});
}
open(trigger: JQuery): void {
this.trigger = trigger;
if (!this.hasContainer) {
this.inner.append(this.element);
this.container.append(this.inner);
this.element.prepend(this.closeButton);
$('body').append(this.container);
this.bindEventHandlers();
this.hasContainer = true;
}
Helpers.disableScroll();
Helpers.disablePageInteractions();
this.container.addClass('is-visible');
this.setInitialFocus();
this.isOpen = true;
this.element.on('keyup.modal', this.keyboardHandler);
this.element.on('keydown.modal', this.keyboardHandler);
}
close(): void {
this.container.removeClass('is-visible');
Helpers.enablePageInteractions();
this.isOpen = false;
this.timeout = window.setTimeout(() => {
this.trigger.focus();
}, 10);
Helpers.enableScroll();
this.element.off('keyup.modal', this.keyboardHandler);
this.element.off('keydown.modal', this.keyboardHandler);
}
keyboard(event: JQuery.TriggeredEvent): void {
if (event.type === 'keyup' && event.key === 'Escape') {
this.close();
}
if (event.type === 'keydown') {
this.focusableElements = focusable(this.element.get(0));
this.firstFocusableElement = this.focusableElements[0];
this.lastFocusableElement = this.focusableElements[this.focusableElements.length - 1];
if (event.key === 'Tab' && !event.shiftKey) {
this.switchFocusedElements(event, 'next');
}
if (event.key === 'Tab' && event.shiftKey) {
this.switchFocusedElements(event, 'previous');
}
}
}
destroy(): void {
clearTimeout(this.timeout);
}
private switchFocusedElements(event: JQuery.TriggeredEvent, direction: string): void {
if (direction === 'next' && event.target.isEqualNode(this.lastFocusableElement)) {
event.preventDefault();
this.firstFocusableElement.focus();
return;
}
if (direction === 'previous' && event.target.isEqualNode(this.firstFocusableElement)) {
event.preventDefault();
this.lastFocusableElement.focus();
}
}
private setInitialFocus(): void {
if (typeof this.settings.focusableElement === 'string') {
const focusableElement: JQuery = this.element.find(`#${this.settings.focusableElement}`);
if (focusableElement.length > 0) {
focusableElement.focus();
return;
}
}
this.element.find('.modal__close').focus();
}
}
$(document).on('click', '[data-js="open-modal"]', (event: JQuery.TriggeredEvent): void => {
event.preventDefault();
const trigger: JQuery = $(event.currentTarget);
const element: JQuery = $(trigger.attr('href'));
const type: string = Component.getDataName(Modal.initSelector);
const dataAttr: string = type + 'Class';
if (element.data(dataAttr)) {
const instance: Modal = element.data(dataAttr);
instance.open(trigger);
} else {
const instance: Modal = new Modal(element[0], trigger, true);
element.data(dataAttr, instance);
}
});
<div class="modal modal--size-medium js-modal " aria-modal="true" role="dialog" id="modal" aria-labelledby="modal_label" data-close-label="close">
<h1 id="modal_label" class="h3 modal__title ">
Leave feedback
</h1>
<div class="modal__content">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe.
</div>
</div>
<div class="modal {{ modifier }} {{ class }} "
aria-modal="true"
role="dialog"
id="{{ id }}"
aria-labelledby="{{ id }}_label"
data-close-label="{{ data.closeLabel }}"
{% if descriptionId %}
aria-describedby="{{ descriptionId }}"
{% endif %}
{% if focusableElementId %}
data-focusable-element="{{ focusableElementId }}"
{% endif %}
>
<{{ headingElement|default('h1') }} id="{{ id }}_label" class="h3 modal__title {{ data.title.isHidden ? 'h-visually-hidden' : '' }}">
{{ data.title.text }}
</{{ headingElement|default('h1') }}>
<div class="modal__content">
{{ data.content }}
</div>
</div>
{
"language": "en-US",
"class": "js-modal",
"id": "modal",
"data": {
"title": {
"text": "Leave feedback"
},
"closeLabel": "close",
"content": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe."
},
"modifier": "modal--size-medium"
}
.modal-container {
display: none;
position: relative;
z-index: map-get($zindex, 'modal');
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba($L-text-black, .25);
&.is-visible {
display: flex;
flex-direction: column;
}
}
.modal-container__inner {
display: flex;
align-items: flex-start;
align-self: center;
margin: auto;
padding: 0;
width: 100%;
min-height: 100vh;
@include bp(md-min) {
padding: 72px 12px 64px;
}
}
.modal {
display: none;
position: relative;
padding: 52px 12px 12px;
margin: 0 auto;
background: $L-background;
border-radius: 0;
@include bp(md-min) {
padding: 64px 24px 24px;
border-radius: 8px;
width: columnWidth(9);
}
.modal-container & {
display: block;
pointer-events: auto;
}
}
.modal--size-small {
@include bp(md-min) {
width: columnWidth(5);
}
}
.modal--size-medium {
@include bp(md-min) {
width: columnWidth(6);
}
}
.modal__title {
margin-bottom: 16px;
text-align: center;
@include bp(md-min) {
margin-bottom: 24px;
}
.modal--modify-product & {
text-align: left;
text-transform: uppercase;
margin-top: 40px;
@include bp(lg-min) {
text-align: center;
}
}
}
.modal__close {
display: inline-flex;
position: absolute;
top: 12px;
right: 12px;
padding: 8px;
border: 0;
border-radius: $border-radius-round;
background: transparent;
color: $L-background-strong;
font-size: 24px;
transition: color $transition-duration $transition-easing;
cursor: pointer;
@include bp(md-min) {
top: 24px;
right: 24px;
padding: 0;
}
&:hover {
color: $L-background-strong-hover;
}
}
.modal--modify-product {
padding: 24px 12px 12px;
background: $L-background-light;
@include bp(lg-min) {
padding: 24px;
}
}
import { focusable, FocusableElement } from 'tabbable';
import Component from '../component/component';
import Helpers from '../helpers/helpers';
import Icon from '../icon/icon';
import './modal.scss';
interface IModalSettings {
closeLabel: string;
focusableElement?: string;
}
export default class Modal extends Component {
static initSelector: string = '.js-modal';
closeButton: JQuery;
container: JQuery;
hasContainer: boolean;
inner: JQuery;
keyboardHandler: () => void;
trigger: JQuery;
isOpen: boolean;
focusableElements: FocusableElement[];
firstFocusableElement: FocusableElement;
lastFocusableElement: FocusableElement;
settings: JQuery.PlainObject<IModalSettings>;
timeout: number;
constructor(target: HTMLElement, trigger: JQuery, open: boolean = true) {
super(target);
this.hasContainer = false;
this.isOpen = false;
this.container = $('<div class="modal-container"></div>');
this.inner = $('<div class="modal-container__inner"></div>');
this.settings = this.element.data();
this.closeButton = $(`<button type="button" class="modal__close" aria-label="${this.settings.closeLabel}">${Icon.render('close', 'modal__close-icon')}</button>`);
this.keyboardHandler = this.keyboard.bind(this);
if (open) {
this.open(trigger);
}
}
bindEventHandlers(): void {
this.container.on('click', (event: JQuery.TriggeredEvent): void => {
if (event.target === event.currentTarget || event.target === this.inner[0]) {
event.preventDefault();
this.close();
}
});
this.closeButton.on('click', (event: JQuery.Event): void => {
event.preventDefault();
this.close();
});
}
open(trigger: JQuery): void {
this.trigger = trigger;
if (!this.hasContainer) {
this.inner.append(this.element);
this.container.append(this.inner);
this.element.prepend(this.closeButton);
$('body').append(this.container);
this.bindEventHandlers();
this.hasContainer = true;
}
Helpers.disableScroll();
Helpers.disablePageInteractions();
this.container.addClass('is-visible');
this.setInitialFocus();
this.isOpen = true;
this.element.on('keyup.modal', this.keyboardHandler);
this.element.on('keydown.modal', this.keyboardHandler);
}
close(): void {
this.container.removeClass('is-visible');
Helpers.enablePageInteractions();
this.isOpen = false;
this.timeout = window.setTimeout(() => {
this.trigger.focus();
}, 10);
Helpers.enableScroll();
this.element.off('keyup.modal', this.keyboardHandler);
this.element.off('keydown.modal', this.keyboardHandler);
}
keyboard(event: JQuery.TriggeredEvent): void {
if (event.type === 'keyup' && event.key === 'Escape') {
this.close();
}
if (event.type === 'keydown') {
this.focusableElements = focusable(this.element.get(0));
this.firstFocusableElement = this.focusableElements[0];
this.lastFocusableElement = this.focusableElements[this.focusableElements.length - 1];
if (event.key === 'Tab' && !event.shiftKey) {
this.switchFocusedElements(event, 'next');
}
if (event.key === 'Tab' && event.shiftKey) {
this.switchFocusedElements(event, 'previous');
}
}
}
destroy(): void {
clearTimeout(this.timeout);
}
private switchFocusedElements(event: JQuery.TriggeredEvent, direction: string): void {
if (direction === 'next' && event.target.isEqualNode(this.lastFocusableElement)) {
event.preventDefault();
this.firstFocusableElement.focus();
return;
}
if (direction === 'previous' && event.target.isEqualNode(this.firstFocusableElement)) {
event.preventDefault();
this.lastFocusableElement.focus();
}
}
private setInitialFocus(): void {
if (typeof this.settings.focusableElement === 'string') {
const focusableElement: JQuery = this.element.find(`#${this.settings.focusableElement}`);
if (focusableElement.length > 0) {
focusableElement.focus();
return;
}
}
this.element.find('.modal__close').focus();
}
}
$(document).on('click', '[data-js="open-modal"]', (event: JQuery.TriggeredEvent): void => {
event.preventDefault();
const trigger: JQuery = $(event.currentTarget);
const element: JQuery = $(trigger.attr('href'));
const type: string = Component.getDataName(Modal.initSelector);
const dataAttr: string = type + 'Class';
if (element.data(dataAttr)) {
const instance: Modal = element.data(dataAttr);
instance.open(trigger);
} else {
const instance: Modal = new Modal(element[0], trigger, true);
element.data(dataAttr, instance);
}
});
Title needs to always exist for screen readers, therefore we hide it if we do not want to show it to the user
Title
<div class="modal js-modal modal--modify-product " aria-modal="true" role="dialog" id="modal" aria-labelledby="modal_label" data-close-label="close">
<h1 id="modal_label" class="h3 modal__title ">
Title
</h1>
<div class="modal__content">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe.
</div>
</div>
<div class="modal {{ modifier }} {{ class }} "
aria-modal="true"
role="dialog"
id="{{ id }}"
aria-labelledby="{{ id }}_label"
data-close-label="{{ data.closeLabel }}"
{% if descriptionId %}
aria-describedby="{{ descriptionId }}"
{% endif %}
{% if focusableElementId %}
data-focusable-element="{{ focusableElementId }}"
{% endif %}
>
<{{ headingElement|default('h1') }} id="{{ id }}_label" class="h3 modal__title {{ data.title.isHidden ? 'h-visually-hidden' : '' }}">
{{ data.title.text }}
</{{ headingElement|default('h1') }}>
<div class="modal__content">
{{ data.content }}
</div>
</div>
{
"language": "en-US",
"class": "js-modal modal--modify-product",
"id": "modal",
"data": {
"title": {
"text": "Title"
},
"closeLabel": "close",
"content": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe."
}
}
.modal-container {
display: none;
position: relative;
z-index: map-get($zindex, 'modal');
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba($L-text-black, .25);
&.is-visible {
display: flex;
flex-direction: column;
}
}
.modal-container__inner {
display: flex;
align-items: flex-start;
align-self: center;
margin: auto;
padding: 0;
width: 100%;
min-height: 100vh;
@include bp(md-min) {
padding: 72px 12px 64px;
}
}
.modal {
display: none;
position: relative;
padding: 52px 12px 12px;
margin: 0 auto;
background: $L-background;
border-radius: 0;
@include bp(md-min) {
padding: 64px 24px 24px;
border-radius: 8px;
width: columnWidth(9);
}
.modal-container & {
display: block;
pointer-events: auto;
}
}
.modal--size-small {
@include bp(md-min) {
width: columnWidth(5);
}
}
.modal--size-medium {
@include bp(md-min) {
width: columnWidth(6);
}
}
.modal__title {
margin-bottom: 16px;
text-align: center;
@include bp(md-min) {
margin-bottom: 24px;
}
.modal--modify-product & {
text-align: left;
text-transform: uppercase;
margin-top: 40px;
@include bp(lg-min) {
text-align: center;
}
}
}
.modal__close {
display: inline-flex;
position: absolute;
top: 12px;
right: 12px;
padding: 8px;
border: 0;
border-radius: $border-radius-round;
background: transparent;
color: $L-background-strong;
font-size: 24px;
transition: color $transition-duration $transition-easing;
cursor: pointer;
@include bp(md-min) {
top: 24px;
right: 24px;
padding: 0;
}
&:hover {
color: $L-background-strong-hover;
}
}
.modal--modify-product {
padding: 24px 12px 12px;
background: $L-background-light;
@include bp(lg-min) {
padding: 24px;
}
}
import { focusable, FocusableElement } from 'tabbable';
import Component from '../component/component';
import Helpers from '../helpers/helpers';
import Icon from '../icon/icon';
import './modal.scss';
interface IModalSettings {
closeLabel: string;
focusableElement?: string;
}
export default class Modal extends Component {
static initSelector: string = '.js-modal';
closeButton: JQuery;
container: JQuery;
hasContainer: boolean;
inner: JQuery;
keyboardHandler: () => void;
trigger: JQuery;
isOpen: boolean;
focusableElements: FocusableElement[];
firstFocusableElement: FocusableElement;
lastFocusableElement: FocusableElement;
settings: JQuery.PlainObject<IModalSettings>;
timeout: number;
constructor(target: HTMLElement, trigger: JQuery, open: boolean = true) {
super(target);
this.hasContainer = false;
this.isOpen = false;
this.container = $('<div class="modal-container"></div>');
this.inner = $('<div class="modal-container__inner"></div>');
this.settings = this.element.data();
this.closeButton = $(`<button type="button" class="modal__close" aria-label="${this.settings.closeLabel}">${Icon.render('close', 'modal__close-icon')}</button>`);
this.keyboardHandler = this.keyboard.bind(this);
if (open) {
this.open(trigger);
}
}
bindEventHandlers(): void {
this.container.on('click', (event: JQuery.TriggeredEvent): void => {
if (event.target === event.currentTarget || event.target === this.inner[0]) {
event.preventDefault();
this.close();
}
});
this.closeButton.on('click', (event: JQuery.Event): void => {
event.preventDefault();
this.close();
});
}
open(trigger: JQuery): void {
this.trigger = trigger;
if (!this.hasContainer) {
this.inner.append(this.element);
this.container.append(this.inner);
this.element.prepend(this.closeButton);
$('body').append(this.container);
this.bindEventHandlers();
this.hasContainer = true;
}
Helpers.disableScroll();
Helpers.disablePageInteractions();
this.container.addClass('is-visible');
this.setInitialFocus();
this.isOpen = true;
this.element.on('keyup.modal', this.keyboardHandler);
this.element.on('keydown.modal', this.keyboardHandler);
}
close(): void {
this.container.removeClass('is-visible');
Helpers.enablePageInteractions();
this.isOpen = false;
this.timeout = window.setTimeout(() => {
this.trigger.focus();
}, 10);
Helpers.enableScroll();
this.element.off('keyup.modal', this.keyboardHandler);
this.element.off('keydown.modal', this.keyboardHandler);
}
keyboard(event: JQuery.TriggeredEvent): void {
if (event.type === 'keyup' && event.key === 'Escape') {
this.close();
}
if (event.type === 'keydown') {
this.focusableElements = focusable(this.element.get(0));
this.firstFocusableElement = this.focusableElements[0];
this.lastFocusableElement = this.focusableElements[this.focusableElements.length - 1];
if (event.key === 'Tab' && !event.shiftKey) {
this.switchFocusedElements(event, 'next');
}
if (event.key === 'Tab' && event.shiftKey) {
this.switchFocusedElements(event, 'previous');
}
}
}
destroy(): void {
clearTimeout(this.timeout);
}
private switchFocusedElements(event: JQuery.TriggeredEvent, direction: string): void {
if (direction === 'next' && event.target.isEqualNode(this.lastFocusableElement)) {
event.preventDefault();
this.firstFocusableElement.focus();
return;
}
if (direction === 'previous' && event.target.isEqualNode(this.firstFocusableElement)) {
event.preventDefault();
this.lastFocusableElement.focus();
}
}
private setInitialFocus(): void {
if (typeof this.settings.focusableElement === 'string') {
const focusableElement: JQuery = this.element.find(`#${this.settings.focusableElement}`);
if (focusableElement.length > 0) {
focusableElement.focus();
return;
}
}
this.element.find('.modal__close').focus();
}
}
$(document).on('click', '[data-js="open-modal"]', (event: JQuery.TriggeredEvent): void => {
event.preventDefault();
const trigger: JQuery = $(event.currentTarget);
const element: JQuery = $(trigger.attr('href'));
const type: string = Component.getDataName(Modal.initSelector);
const dataAttr: string = type + 'Class';
if (element.data(dataAttr)) {
const instance: Modal = element.data(dataAttr);
instance.open(trigger);
} else {
const instance: Modal = new Modal(element[0], trigger, true);
element.data(dataAttr, instance);
}
});
<a href="#modal" data-js="open-modal">open modal</a>
<div class="modal " aria-modal="true" role="dialog" id="modal" aria-labelledby="modal_label" data-close-label="Close" aria-describedby="modal_description" data-focusable-element="name">
<h1 id="modal_label" class="h3 modal__title ">
Leave feedback
</h1>
<div class="modal__content">
<p id="modal_description" class="modal__description">
This is a description. This is picked up by screen readers.
</p>
<div class="text h-margin-top-sm">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe.
</div>
<div class="grid h-margin-top-md">
<div class="grid__col grid__col--md-6">
<div class="textfield modal__field">
<label class="textfield__label " for="text1">
Your name
</label>
<div class="textfield__inner">
<input class="textfield__input" type="text" id="text1" name="textfield" placeholder="Input placeholder">
</div>
</div>
</div>
<div class="grid__col grid__col--md-6">
<div class="textfield select modal__field">
<label class="textfield__label select__label " for="select1">
Country
</label>
<div class="textfield__inner">
<select name="select" id="select1" class="textfield__input select__input">
<option value="placeholder" disabled selected>
Pick a country
</option>
<option value="1">
Estonia
</option>
<option value="2">
Finland
</option>
<option value="3">
Latvia
</option>
</select>
<svg class="icon select__icon" focusable="false">
<use href="../../inc/svg/global.4609ec92109fc41e7ad4764ef897ea8e.svg#"></use>
</svg>
</div>
</div>
</div>
<div class="grid__col grid__col--md-12">
<div class="textfield modal__field">
<label class="textfield__label " for="text2">
Title for your feedback
</label>
<div class="textfield__inner">
<input class="textfield__input" type="text" id="text2" name="textfield" placeholder="Input placeholder">
</div>
</div>
</div>
<div class="grid__col grid__col--md-12">
<div class="textfield textarea">
<label class="textfield__label " for="text3">
Form label
</label>
<div class="textfield__inner">
<textarea class="textfield__input textarea__input" id="text3" name="textarea"></textarea>
</div>
</div>
</div>
<div class="grid__col grid__col--md-12">
<button type="button" class="button button--block modal__field">
<span class="button__inner">
<span class="button__text">Button text</span>
</span>
</button>
</div>
</div>
</div>
</div>
<a href="#modal" data-js="open-modal">open modal</a>
{% set modalContent %}
<p id="modal_description" class="modal__description">
{{ data.description }}
</p>
<div class="text h-margin-top-sm">
{{ data.content }}
</div>
{% if data.form %}
<div class="grid h-margin-top-md">
{% for field in data.form.fields %}
<div class="grid__col grid__col--md-{{ field.width|default('12') }}">
{% include '@' ~ field.name with {
class: 'modal__field',
modifier: field.modifier,
data: field.data
} %}
</div>
{% endfor %}
</div>
{% endif %}
{% endset %}
{% include '@modal' with {
data: {
title: {
text: 'Leave feedback'
},
closeLabel: 'Close',
content: modalContent
},
id: 'modal',
focusableElementId: 'name',
descriptionId: 'modal_description',
class: '',
headingElement: 'h1'
} %}
{
"language": "en-US",
"class": "js-modal",
"id": "modal",
"data": {
"title": {
"text": "Leave feedback"
},
"closeLabel": "close",
"content": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur nemo commodi eius minima ratione suscipit, itaque odit incidunt, porro doloribus cumque placeat, fugiat modi dignissimos beatae optio nulla ad saepe.",
"description": "This is a description. This is picked up by screen readers.",
"form": {
"fields": [
{
"name": "textfield",
"width": 6,
"data": {
"label": "Your name",
"id": "text1",
"name": "textfield",
"placeholder": "Input placeholder"
}
},
{
"name": "select",
"width": 6,
"data": {
"label": "Country",
"id": "select1",
"name": "select",
"placeholder": "Pick a country",
"options": [
{
"name": "Estonia",
"value": "1"
},
{
"name": "Finland",
"value": "2"
},
{
"name": "Latvia",
"value": "3"
}
]
}
},
{
"name": "textfield",
"data": {
"label": "Title for your feedback",
"id": "text2",
"name": "textfield",
"placeholder": "Input placeholder"
}
},
{
"name": "textarea",
"data": {
"label": "Form label",
"id": "text3",
"name": "textarea"
}
},
{
"name": "button",
"modifier": "button--block",
"data": {
"text": "Button text"
}
}
]
}
}
}
.modal-container {
display: none;
position: relative;
z-index: map-get($zindex, 'modal');
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba($L-text-black, .25);
&.is-visible {
display: flex;
flex-direction: column;
}
}
.modal-container__inner {
display: flex;
align-items: flex-start;
align-self: center;
margin: auto;
padding: 0;
width: 100%;
min-height: 100vh;
@include bp(md-min) {
padding: 72px 12px 64px;
}
}
.modal {
display: none;
position: relative;
padding: 52px 12px 12px;
margin: 0 auto;
background: $L-background;
border-radius: 0;
@include bp(md-min) {
padding: 64px 24px 24px;
border-radius: 8px;
width: columnWidth(9);
}
.modal-container & {
display: block;
pointer-events: auto;
}
}
.modal--size-small {
@include bp(md-min) {
width: columnWidth(5);
}
}
.modal--size-medium {
@include bp(md-min) {
width: columnWidth(6);
}
}
.modal__title {
margin-bottom: 16px;
text-align: center;
@include bp(md-min) {
margin-bottom: 24px;
}
.modal--modify-product & {
text-align: left;
text-transform: uppercase;
margin-top: 40px;
@include bp(lg-min) {
text-align: center;
}
}
}
.modal__close {
display: inline-flex;
position: absolute;
top: 12px;
right: 12px;
padding: 8px;
border: 0;
border-radius: $border-radius-round;
background: transparent;
color: $L-background-strong;
font-size: 24px;
transition: color $transition-duration $transition-easing;
cursor: pointer;
@include bp(md-min) {
top: 24px;
right: 24px;
padding: 0;
}
&:hover {
color: $L-background-strong-hover;
}
}
.modal--modify-product {
padding: 24px 12px 12px;
background: $L-background-light;
@include bp(lg-min) {
padding: 24px;
}
}
import { focusable, FocusableElement } from 'tabbable';
import Component from '../component/component';
import Helpers from '../helpers/helpers';
import Icon from '../icon/icon';
import './modal.scss';
interface IModalSettings {
closeLabel: string;
focusableElement?: string;
}
export default class Modal extends Component {
static initSelector: string = '.js-modal';
closeButton: JQuery;
container: JQuery;
hasContainer: boolean;
inner: JQuery;
keyboardHandler: () => void;
trigger: JQuery;
isOpen: boolean;
focusableElements: FocusableElement[];
firstFocusableElement: FocusableElement;
lastFocusableElement: FocusableElement;
settings: JQuery.PlainObject<IModalSettings>;
timeout: number;
constructor(target: HTMLElement, trigger: JQuery, open: boolean = true) {
super(target);
this.hasContainer = false;
this.isOpen = false;
this.container = $('<div class="modal-container"></div>');
this.inner = $('<div class="modal-container__inner"></div>');
this.settings = this.element.data();
this.closeButton = $(`<button type="button" class="modal__close" aria-label="${this.settings.closeLabel}">${Icon.render('close', 'modal__close-icon')}</button>`);
this.keyboardHandler = this.keyboard.bind(this);
if (open) {
this.open(trigger);
}
}
bindEventHandlers(): void {
this.container.on('click', (event: JQuery.TriggeredEvent): void => {
if (event.target === event.currentTarget || event.target === this.inner[0]) {
event.preventDefault();
this.close();
}
});
this.closeButton.on('click', (event: JQuery.Event): void => {
event.preventDefault();
this.close();
});
}
open(trigger: JQuery): void {
this.trigger = trigger;
if (!this.hasContainer) {
this.inner.append(this.element);
this.container.append(this.inner);
this.element.prepend(this.closeButton);
$('body').append(this.container);
this.bindEventHandlers();
this.hasContainer = true;
}
Helpers.disableScroll();
Helpers.disablePageInteractions();
this.container.addClass('is-visible');
this.setInitialFocus();
this.isOpen = true;
this.element.on('keyup.modal', this.keyboardHandler);
this.element.on('keydown.modal', this.keyboardHandler);
}
close(): void {
this.container.removeClass('is-visible');
Helpers.enablePageInteractions();
this.isOpen = false;
this.timeout = window.setTimeout(() => {
this.trigger.focus();
}, 10);
Helpers.enableScroll();
this.element.off('keyup.modal', this.keyboardHandler);
this.element.off('keydown.modal', this.keyboardHandler);
}
keyboard(event: JQuery.TriggeredEvent): void {
if (event.type === 'keyup' && event.key === 'Escape') {
this.close();
}
if (event.type === 'keydown') {
this.focusableElements = focusable(this.element.get(0));
this.firstFocusableElement = this.focusableElements[0];
this.lastFocusableElement = this.focusableElements[this.focusableElements.length - 1];
if (event.key === 'Tab' && !event.shiftKey) {
this.switchFocusedElements(event, 'next');
}
if (event.key === 'Tab' && event.shiftKey) {
this.switchFocusedElements(event, 'previous');
}
}
}
destroy(): void {
clearTimeout(this.timeout);
}
private switchFocusedElements(event: JQuery.TriggeredEvent, direction: string): void {
if (direction === 'next' && event.target.isEqualNode(this.lastFocusableElement)) {
event.preventDefault();
this.firstFocusableElement.focus();
return;
}
if (direction === 'previous' && event.target.isEqualNode(this.firstFocusableElement)) {
event.preventDefault();
this.lastFocusableElement.focus();
}
}
private setInitialFocus(): void {
if (typeof this.settings.focusableElement === 'string') {
const focusableElement: JQuery = this.element.find(`#${this.settings.focusableElement}`);
if (focusableElement.length > 0) {
focusableElement.focus();
return;
}
}
this.element.find('.modal__close').focus();
}
}
$(document).on('click', '[data-js="open-modal"]', (event: JQuery.TriggeredEvent): void => {
event.preventDefault();
const trigger: JQuery = $(event.currentTarget);
const element: JQuery = $(trigger.attr('href'));
const type: string = Component.getDataName(Modal.initSelector);
const dataAttr: string = type + 'Class';
if (element.data(dataAttr)) {
const instance: Modal = element.data(dataAttr);
instance.open(trigger);
} else {
const instance: Modal = new Modal(element[0], trigger, true);
element.data(dataAttr, instance);
}
});