Modale Accessibile

by murgiaMarco

HTML

<button id="openModal">Apri Modale</button>

<div class="overlay" id="overlay">
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDesc">
    <h2 id="modalTitle">Titolo Modale</h2>
    <p id="modalDesc">Questa è una modale accessibile per screen reader e gesti mobile.</p>
    <button id="closeModal">Chiudi</button>
  </div>
</div>

CSS

body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
  }
  .overlay {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background: rgba(0,0,0,0.5);
    display: none;
    align-items: center;
    justify-content: center;
  }
  .modal {
    background: #fff;
    padding: 20px;
    border-radius: 8px;
    width: 90%;
    max-width: 400px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  }
  .modal h2 {
    margin-top: 0;
  }
  .modal button {
    margin-top: 20px;
    padding: 10px;
    background: #0078d7;
    color: #fff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }
  .modal button:hover {
    background: #005fa3;
  }

JavaScript

const overlay = document.getElementById('overlay');
const openBtn = document.getElementById('openModal');
const closeBtn = document.getElementById('closeModal');
const modal = overlay.querySelector('.modal');

let focusableElements;
let firstFocusable;
let lastFocusable;
let startX = 0;

// Apri modale
openBtn.addEventListener('click', () => {
  overlay.style.display = 'flex';
  overlay.setAttribute('aria-hidden', 'false');
  trapFocus();
  firstFocusable.focus();
});

// Chiudi modale
function closeModal() {
  overlay.style.display = 'none';
  overlay.setAttribute('aria-hidden', 'true');
  openBtn.focus();
}

closeBtn.addEventListener('click', closeModal);

// Chiudi con ESC
document.addEventListener('keydown', (e) => {
  if (overlay.style.display === 'flex' && e.key === 'Escape') {
    closeModal();
  }
});

// Focus trap
function trapFocus() {
  focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  firstFocusable = focusableElements[0];
  lastFocusable = focusableElements[focusableElements.length - 1];

  document.addEventListener('keydown', (e) => {
    if (overlay.style.display === 'flex' && e.key === 'Tab') {
      if (e.shiftKey) {
        if (document.activeElement === firstFocusable) {
          e.preventDefault();
          lastFocusable.focus();
        }
      } else {
        if (document.activeElement === lastFocusable) {
          e.preventDefault();
          firstFocusable.focus();
        }
      }
    }
  });
}

// Swipe per chiudere (opzionale)
modal.addEventListener('touchstart', (e) => {
  startX = e.touches[0].clientX;
});

modal.addEventListener('touchend', (e) => {
  const endX = e.changedTouches[0].clientX;
  if (startX - endX > 80) { // Swipe verso sinistra
    closeModal();
  }
});