fix(a11y): les modales du site public déclaraient aria-modal sans piéger le focus
Les deux modales (groupes sans coordonnées, mentions légales / FAQ) portent role="dialog" et aria-modal="true" — ce qui affirme aux lecteurs d'écran que tout le reste de la page est inerte. C'était faux : aucune gestion du focus. Concrètement, à l'ouverture le focus restait derrière le voile, la tabulation ressortait sur des commandes invisibles, et la fermeture ne rendait pas le focus à son point de départ. Un utilisateur au clavier se retrouvait perdu dans une page dont on venait de lui dire qu'elle était inaccessible. Le dashboard admin faisait déjà tout cela correctement ; on porte le même traitement, factorisé en openBackdrop / closeBackdrop puisque les modales du site public sont des éléments statiques que l'on bascule, et non créés à la volée comme côté admin. Non couvert par un test : le site public n'a aucun harnais de bout en bout — les 89 parcours Playwright ne visent que l'admin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
582fdc5451
commit
1fd0c8933b
1 changed files with 51 additions and 9 deletions
|
|
@ -1187,9 +1187,55 @@ async function loadNoLocationBands() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Modales — focus
|
||||||
|
//
|
||||||
|
// Les deux modales déclarent aria-modal="true", ce qui affirme aux lecteurs
|
||||||
|
// d'écran que tout le reste de la page est inerte. Sans piège à focus, c'était
|
||||||
|
// faux : la tabulation ressortait derrière le voile, sur des commandes
|
||||||
|
// invisibles, et la fermeture ne rendait pas le focus à son point de départ.
|
||||||
|
// Même traitement que le dashboard admin, qui le faisait déjà correctement.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
const FOCUSABLES = 'button, [href], input, select, textarea, summary, [tabindex]:not([tabindex="-1"])';
|
||||||
|
let lastFocusedBeforeModal = null;
|
||||||
|
|
||||||
|
function trapModalFocus(backdrop) {
|
||||||
|
if (!backdrop || backdrop.__trap) return;
|
||||||
|
const onKeydown = (e) => {
|
||||||
|
if (e.key !== "Tab") return;
|
||||||
|
const f = backdrop.querySelectorAll(FOCUSABLES);
|
||||||
|
if (!f.length) return;
|
||||||
|
const first = f[0];
|
||||||
|
const last = f[f.length - 1];
|
||||||
|
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
||||||
|
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
||||||
|
};
|
||||||
|
backdrop.addEventListener("keydown", onKeydown);
|
||||||
|
backdrop.__trap = onKeydown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBackdrop(backdrop) {
|
||||||
|
if (!backdrop) return;
|
||||||
|
lastFocusedBeforeModal = document.activeElement;
|
||||||
|
backdrop.classList.add("on");
|
||||||
|
backdrop.setAttribute("aria-hidden", "false");
|
||||||
|
trapModalFocus(backdrop);
|
||||||
|
const f = backdrop.querySelectorAll(FOCUSABLES);
|
||||||
|
if (f.length) f[0].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBackdrop(backdrop) {
|
||||||
|
if (!backdrop || !backdrop.classList.contains("on")) return;
|
||||||
|
backdrop.classList.remove("on");
|
||||||
|
backdrop.setAttribute("aria-hidden", "true");
|
||||||
|
if (lastFocusedBeforeModal && document.contains(lastFocusedBeforeModal)) {
|
||||||
|
lastFocusedBeforeModal.focus();
|
||||||
|
lastFocusedBeforeModal = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
modalBackdrop.classList.remove("on");
|
closeBackdrop(modalBackdrop);
|
||||||
modalBackdrop.setAttribute("aria-hidden", "true");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modalClose?.addEventListener("click", closeModal);
|
modalClose?.addEventListener("click", closeModal);
|
||||||
|
|
@ -1209,14 +1255,11 @@ function openInfoModal(title, html) {
|
||||||
if (!infoBackdrop || !infoBody || !infoTitle) return;
|
if (!infoBackdrop || !infoBody || !infoTitle) return;
|
||||||
infoTitle.textContent = title;
|
infoTitle.textContent = title;
|
||||||
infoBody.innerHTML = html;
|
infoBody.innerHTML = html;
|
||||||
infoBackdrop.classList.add("on");
|
openBackdrop(infoBackdrop);
|
||||||
infoBackdrop.setAttribute("aria-hidden", "false");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeInfoModal() {
|
function closeInfoModal() {
|
||||||
if (!infoBackdrop) return;
|
closeBackdrop(infoBackdrop);
|
||||||
infoBackdrop.classList.remove("on");
|
|
||||||
infoBackdrop.setAttribute("aria-hidden", "true");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (infoClose) infoClose.addEventListener("click", closeInfoModal);
|
if (infoClose) infoClose.addEventListener("click", closeInfoModal);
|
||||||
|
|
@ -1332,8 +1375,7 @@ setupDropdown("themeToggle", "themeSelect");
|
||||||
// --- Buttons / controls ---
|
// --- Buttons / controls ---
|
||||||
$("btnNoLocation")?.addEventListener("click", () => {
|
$("btnNoLocation")?.addEventListener("click", () => {
|
||||||
modalTitle.textContent = t("without_coords_modal");
|
modalTitle.textContent = t("without_coords_modal");
|
||||||
modalBackdrop.classList.add("on");
|
openBackdrop(modalBackdrop);
|
||||||
modalBackdrop.setAttribute("aria-hidden", "false");
|
|
||||||
loadNoLocationBands();
|
loadNoLocationBands();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue