Приклад модального вікна

Опис

Після появи модального вікна відбувається наступне:

  1. Фокус ставиться на кнопку закриття модального вікна.
  2. Заголовок і текст модального вікна автоматично озвучується скрінрідером.
  3. В другу чергу скрінрідер озвучить текстову мітку кнопки.
  4. При навігації клавіатурою фокус не може покинути модальне вікно. Поки модальне вікно існує, рух фокуса "закільцьований" у ньому.

Демонстрація

Відповідний код HTML

<button class="button" id="buttonWindowModalShow">Протестувати</button>

<div id="windowModal" role="dialog" aria-modal="true" aria-labelledby="windowModalTitle" aria-describedby="windowModalMessage" style="display: none">
    <div id="windowModalFog"></div>
    <div id="windowModalInner">
        <button class="button_no_borders" id="buttonWindowModalHideCross" aria-label="Закрити це повідомлення"></button>
        <div style="margin: 10px 0px 16px 0px">
            <h2 id="windowModalTitle">Повідомлення</h2>
            <p id="windowModalMessage">Цей текст має бути озвучений скрінрідером.</p>
        </div>
        <div>
            <button class="button" id="buttonWindowModalHide">Зрозуміло</button>
        </div>
    </div>
</div>

Відповідний код JS

const windowModal = document.getElementById("windowModal");
const windowModalMessage = document.getElementById("windowModalMessage");
const buttonWindowModalShow = document.getElementById("buttonWindowModalShow");
const buttonWindowModalHide = document.getElementById("buttonWindowModalHide");
const buttonWindowModalHideCross = document.getElementById("buttonWindowModalHideCross");

buttonWindowModalShow.addEventListener("click", () => {
    WindowModal.show();
});

buttonWindowModalHide.addEventListener("click", () => {
    WindowModal.hide();
});

buttonWindowModalHideCross.addEventListener("click", () => {
    WindowModal.hide();
});

document.addEventListener("keyup", function (event) {
    console.log("keyup detected");
    if (event.key === "Escape" && windowModal.style.display === "flex") {
        WindowModal.hide();
    }
});

class WindowModal
{
    static show() {
        console.log("WM.show was called");
        
        windowModal.style.display = "flex";
        buttonWindowModalHide.focus();
        
        let focusableElementsSelector = "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])";
        let focusableElements = windowModal.querySelectorAll(focusableElementsSelector);
        this.focusableElementFirst = focusableElements[0];
        this.focusableElementLast = focusableElements[focusableElements.length - 1];
        windowModal.addEventListener("keydown", bindFocus);
    }
    
    static hide() {
        console.log("WM.hide was called");
        
        windowModal.style.display = "none";
        buttonWindowModalShow.focus();
        
        windowModal.removeEventListener("keydown", bindFocus);
    }
}

function bindFocus(event) {
    console.log("bindFocus was called");
    
    let isTabPressed = event.key === "Tab";
    if (!isTabPressed) return;
    
    console.log("isTab");
    if (event.shiftKey) {
        if (document.activeElement === WindowModal.focusableElementFirst) {
            event.preventDefault();
            console.log("focusing at last element in modal window");
            WindowModal.focusableElementLast.focus();
        }
    } else {
        if (document.activeElement === WindowModal.focusableElementLast) {
            event.preventDefault();
            console.log("focusing at first element in modal window");
            WindowModal.focusableElementFirst.focus();
        }
    }
}

Відповідний код CSS

p {
    margin: 0px 0px 2px 0px;
    padding: 0px;
}

#windowModal {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    position: fixed;
    left: 0px;
    right: 0px;
    top: 0px;
    bottom: 0px;
}

#windowModalFog {
    background-color: #000;
    opacity: 0.5;
    position: absolute;
    z-index: 1;
    width: 100%;
    height: 100%;
}

#windowModalInner {
    background-color: #fff;
    border-radius: 12px;
    color: #000;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: 20px;
    position: relative;
    max-width: 500px;
    text-align: center;
    z-index: 2;
}

#windowModalTitle {
    margin: 0px 0px 8px 0px;
    padding: 0px;
}

#buttonWindowModalHideCross {
    font-size: 18px;
    position: absolute;
    right: 8px;
    top: 8px;
}

.button_no_borders {
    background: none;
    border: 0px #000 solid;
}

button {
    cursor: pointer;
}

button:focus {
    outline: 2px solid #4444FF;
    outline-offset: 2px;
}

.focusable:focus {
    outline: 2px solid #4444FF;
    outline-offset: 2px;
}