Focus Trap - Multiple Dialogs Example

by tohuwabohu_io

HTML

<body>
    <dialog id="cookie-banner" class="popup-overlay modal" aria-modal="true">
        <div class="popup">
            <div>
                <h2>Cookies 🍪</h2>
                <p>
                    Very important information in regard to whom you will sell your soul to.
                </p>
                <p>
                    Allow or Prohibit:
                </p>
                <div class="popup-controls cookie-checkboxes">
                    <input id="technical" type="checkbox" checked>
                    <label for="technical">Technical Cookies</label>
                    <input id="analytical" type="checkbox" checked>
                    <label for="analytical">Analytical Cookies</label>
                    <input id="thirdParty" type="checkbox" checked>
                    <label for="thirdParty">3rd Party Cookies</label>
                </div>
            </div>
            <div class="popup-button-container">
                <button class="accept" onclick="hideOverlay(true, 'cookie-banner')">
                    Accept
                </button>
                <button onclick="hideOverlay(true, 'cookie-banner')">
                    Decline
                </button>
            </div>
        </div>
    </dialog>
    <dialog id="confirmation-dialog" class="popup-overlay" aria-modal="true" aria-hidden="true">
        <div class="popup">
            <div>
                <h2>Confirmation</h2>
                <p>Are you sure?</p>
            </div>
            <div class="popup-button-container">
                <button class="accept" onclick="hideOverlay(true, 'confirmation-dialog')">
                    Yes
                </button>
                <button onclick="hideOverlay(true, 'confirmation-dialog')">
                    No
                </button>
            </div>
        </div>
    </dialog>
    <dialog id="password-prompt" class="popup-overlay" aria-modal="true" aria-hidden="true">
        <div class="popup">
            <div>
 ...

CSS

.popup-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  background-color: rgba(255, 255, 255, 0.70);
  display: none;
}

.modal {
  display: flex;
  align-items: center;
}

.popup {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-content: center;
  border-radius: 10px;
  border: 1px solid black;
  padding: 1em;
  margin: auto;
  gap: 1em;
  max-width: 500px;
  background-color: #fff;
}

.popup-controls {
  display: grid;
  gap: 1em;
}

.cookie-checkboxes {
  grid-template-columns: 2em auto;
}

.popup-button-container {
  display: flex;
  flex-direction: row;
  gap: 1em;
}

button {
  border-radius: 5px;
  border: 1px solid black;
  padding: 0.25em;
  font-size: 1em;
  background: white;
}

.accept {
  background: #3584e4;
}

JavaScript

document.addEventListener('DOMContentLoaded', () => {
  const focusableElements = Array.from(document
   .querySelectorAll('div[class~=popup-controls] > input, div[class=popup-button-container] > button'));

  const elementsByDialog = new Map();

  focusableElements.forEach(element => {
    scrape(element, elementsByDialog);
  });

  document.addEventListener('keydown', (e) => {
    const overlay = document.querySelector('dialog.popup-overlay.modal');

    // focus trap is inactive
    if (!overlay) {
      return;
    }

    const dialogElements = elementsByDialog.get(overlay);

    const first = dialogElements[0];
    const last = dialogElements[dialogElements.length - 1];

    if (overlay && e.key === 'Tab') {
      if (!dialogElements.includes(e.target)) {
        trap(e, first);
      } else if (e.target === last && !e.shiftKey) {
        trap(e, first);
      } else if (e.target === first && e.shiftKey) {
        trap(e, last);
      }
    }
  });
});

const scrape = (element, map) => {
  const key = element.closest('dialog');
  let value = map.get(key);

  if (!value) {
    value = [];
  }

  value.push(element);

  map.set(key, value);
}

const trap = (e, element) => {
  e.preventDefault();
  element.focus();
}

const hideOverlay = (hide, id) => {
  const overlay = document.querySelector(`dialog[id=${id}]`);

  if (hide === true) {
    overlay.setAttribute('aria-hidden', 'true');

    overlay.classList.remove('modal');
  } else {
    overlay.setAttribute('aria-hidden', 'false');

    overlay.classList.add('modal');
  }
}