const isVisible = (elem: HTMLElement) => !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); export class Modal { public triggers?: NodeListOf; 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; 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, ).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(); } }