js/components/modal.js — an accessible modal
In the code column — js/components/modal.js. All the accessibility is in two functions:
export function openModal(date) {
overlay.classList.remove('hidden');
restoreBackground = hideBackground(overlay); // hide the background
releaseFocus = trapFocus(overlay, { initialFocus: notesInput }); // move in + trap
}
export function closeModal() {
overlay.classList.add('hidden');
restoreBackground?.();
releaseFocus?.(); // restores focus to the button that opened us
}
Plus Esc closes and a backdrop click closes:
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !overlay.classList.contains('hidden')) closeModal();
});
(Backdrop click: e.target === overlay — only if the overlay itself was clicked, not the modal inside it.)
The confirm dialog (confirm.js) and toasts (toast.js) are the same pattern: confirm.js uses the same trapFocus; toast.js just adds a transient <div> that removes itself after ~2.6 s.
The star keyboard (makeRadioGroup from a11y.js) — arrow keys move, Space/Enter selects:
makeRadioGroup(starsEl, { onSelect: (value) => { draftRating = value; renderStars(); } });
// role="radio", tabIndex management, ArrowLeft/Right → next star
Verify (keyboard, not mouse!).
1. Open the modal → focus is already inside (the notes field).
2. Press Tab several times → focus CYCLES within the modal, never leaves.
3. Close (Esc) → focus returns to the "Edit" button you came from.
4. Stars: move with arrows, pick with Space — no mouse.
Gotcha (focus escapes). In
openModal, comment out thetrapFocusline. Open the modal and pressTab— focus travels off to the nav links behind the modal (you'll see the focus ring in the wrong place). A keyboard user has now "vanished" into the background. RestoretrapFocus— focus cycles inside again. Every modal must trap focus; without it, it's broken for accessibility even if it looks fine with a mouse.