125 lines
3.2 KiB
TypeScript
125 lines
3.2 KiB
TypeScript
const isVisible = (elem: HTMLElement) =>
|
|
!!elem &&
|
|
!!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
|
|
|
|
export class Modal {
|
|
public triggers?: NodeListOf<HTMLElement>;
|
|
public modal?: HTMLElement;
|
|
public modalContent?: HTMLElement;
|
|
protected focusLock: HTMLElement[] = [];
|
|
protected trigger?: HTMLElement;
|
|
|
|
constructor(public name: string) {}
|
|
|
|
public reset(): void {
|
|
if (!this.modal) {
|
|
return;
|
|
}
|
|
|
|
this.modal.style.display = 'none';
|
|
this.removeFocusLock();
|
|
this.removeClickListener();
|
|
}
|
|
|
|
public open(): void {
|
|
if (!this.modal) {
|
|
return;
|
|
}
|
|
|
|
this.modal.style.display = 'block';
|
|
this.createFocusLock();
|
|
|
|
setTimeout(() =>
|
|
document.addEventListener('click', this.outsideClickListener),
|
|
);
|
|
}
|
|
|
|
public initialize(): void {
|
|
this.triggers = document.querySelectorAll(
|
|
`[data-modal-trigger="${this.name}"]`,
|
|
) as NodeListOf<HTMLElement>;
|
|
|
|
this.modal = document.querySelector(
|
|
`[data-modal="${this.name}"]`,
|
|
) as HTMLElement;
|
|
|
|
this.triggers.forEach((item) =>
|
|
item.addEventListener('click', (evt) => {
|
|
evt.preventDefault();
|
|
this.trigger = item;
|
|
this.open();
|
|
}),
|
|
);
|
|
|
|
if (this.modal) {
|
|
const attrLabel = `modal_${this.name}_label`;
|
|
const label = this.modal.querySelector('.modal__title');
|
|
this.modalContent = this.modal.querySelector('.modal__content');
|
|
this.modal.setAttribute('aria-modal', 'true');
|
|
this.modal.setAttribute('role', 'dialog');
|
|
this.modal.setAttribute('aria-labelledby', attrLabel);
|
|
label.setAttribute('id', attrLabel);
|
|
}
|
|
}
|
|
|
|
private outsideClickListener = (event: Event) => {
|
|
if (
|
|
!this.modalContent.contains(event.target as HTMLElement) &&
|
|
isVisible(this.modalContent)
|
|
) {
|
|
this.reset();
|
|
this.removeClickListener();
|
|
}
|
|
};
|
|
|
|
private removeClickListener = () => {
|
|
document.removeEventListener('click', this.outsideClickListener);
|
|
};
|
|
|
|
private getFocusable(): HTMLElement[] {
|
|
const focusable = Array.from(
|
|
this.modal.querySelectorAll(
|
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
|
) as NodeListOf<HTMLElement>,
|
|
).filter(
|
|
(item) => item.offsetParent !== null && !this.focusLock.includes(item),
|
|
);
|
|
|
|
const firstFocusable = focusable[0];
|
|
const lastFocusable = focusable[focusable.length - 1];
|
|
|
|
return [firstFocusable, lastFocusable];
|
|
}
|
|
|
|
private createFocusLock(): void {
|
|
const startFocus = document.createElement('div');
|
|
startFocus.setAttribute('tabindex', '0');
|
|
|
|
const stopFocus = document.createElement('div');
|
|
stopFocus.setAttribute('tabindex', '0');
|
|
|
|
this.modal.prepend(startFocus);
|
|
this.modal.appendChild(stopFocus);
|
|
this.focusLock = [startFocus, stopFocus];
|
|
|
|
stopFocus.addEventListener('focus', (event) => {
|
|
event.preventDefault();
|
|
this.getFocusable()[0].focus();
|
|
});
|
|
|
|
startFocus.addEventListener('focus', (event) => {
|
|
event.preventDefault();
|
|
this.getFocusable()[1].focus();
|
|
});
|
|
|
|
this.getFocusable()[0].focus();
|
|
}
|
|
|
|
private removeFocusLock(): void {
|
|
this.focusLock.forEach((item) => {
|
|
item.parentElement.removeChild(item);
|
|
});
|
|
this.trigger?.focus();
|
|
}
|
|
}
|