Site personnel courcellematthi.eu
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* navigation.js — Menu de navigation mobile
|
||||
* =============================================================================
|
||||
* Gère l'ouverture et la fermeture du panneau de navigation sous le point de
|
||||
* rupture mobile.
|
||||
*
|
||||
* Principe d'amélioration progressive : sans JavaScript, le menu reste une
|
||||
* liste de liens visible et fonctionnelle (le CSS ne la masque que si le
|
||||
* bouton, lui-même conditionné à ce script, est présent et actif).
|
||||
*
|
||||
* Accessibilité couverte :
|
||||
* - aria-expanded reflète l'état réel du panneau
|
||||
* - aria-controls relie le bouton au panneau qu'il commande
|
||||
* - la touche Échap ferme le menu et rend le focus au bouton
|
||||
* - le focus est piégé dans le panneau tant qu'il est ouvert
|
||||
* - le défilement de la page est bloqué pendant l'ouverture
|
||||
*/
|
||||
|
||||
/** Point de rupture, aligné sur celui de layout.css. */
|
||||
const MOBILE_QUERY = "(width <= 46rem)";
|
||||
|
||||
/** Sélecteur des éléments pouvant recevoir le focus clavier. */
|
||||
const FOCUSABLE =
|
||||
'a[href], button:not([disabled]), input:not([disabled]), ' +
|
||||
'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
/**
|
||||
* Initialise le menu mobile.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {HTMLElement} options.toggle - Bouton d'ouverture/fermeture.
|
||||
* @param {HTMLElement} options.nav - Conteneur <nav> à afficher/masquer.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function initNavigation({ toggle, nav }) {
|
||||
// Garde défensive : si le balisage change, le script ne casse pas la page.
|
||||
if (!toggle || !nav) return;
|
||||
|
||||
const mediaQuery = window.matchMedia(MOBILE_QUERY);
|
||||
|
||||
/**
|
||||
* Applique l'état d'ouverture au DOM.
|
||||
* @param {boolean} isOpen
|
||||
* @returns {void}
|
||||
*/
|
||||
const setOpen = (isOpen) => {
|
||||
nav.dataset.open = String(isOpen);
|
||||
toggle.setAttribute("aria-expanded", String(isOpen));
|
||||
|
||||
// Empêche le défilement de l'arrière-plan pendant que le panneau
|
||||
// recouvre l'écran — évite l'effet « scroll fantôme » sur mobile.
|
||||
document.body.style.overflow = isOpen && mediaQuery.matches ? "hidden" : "";
|
||||
};
|
||||
|
||||
const isOpen = () => toggle.getAttribute("aria-expanded") === "true";
|
||||
const close = () => setOpen(false);
|
||||
|
||||
/* --- Bascule au clic sur le bouton ---------------------------------- */
|
||||
toggle.addEventListener("click", () => setOpen(!isOpen()));
|
||||
|
||||
/* --- Fermeture après navigation vers une ancre ---------------------- */
|
||||
nav.addEventListener("click", (event) => {
|
||||
if (event.target.closest("a")) close();
|
||||
});
|
||||
|
||||
/* --- Clavier : Échap ferme, Tab reste piégé dans le panneau --------- */
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (!isOpen()) return;
|
||||
|
||||
if (event.key === "Escape") {
|
||||
close();
|
||||
toggle.focus(); // Le focus ne doit jamais être perdu dans le vide
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== "Tab") return;
|
||||
|
||||
// Piège de focus : la tabulation boucle entre le bouton et le dernier
|
||||
// lien du menu, au lieu de partir dans le contenu masqué derrière.
|
||||
const focusables = [toggle, ...nav.querySelectorAll(FOCUSABLE)];
|
||||
const first = focusables[0];
|
||||
const last = focusables[focusables.length - 1];
|
||||
|
||||
if (event.shiftKey && document.activeElement === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
});
|
||||
|
||||
/* --- Fermeture au clic hors du panneau ------------------------------ */
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!isOpen()) return;
|
||||
if (nav.contains(event.target) || toggle.contains(event.target)) return;
|
||||
close();
|
||||
});
|
||||
|
||||
/* --- Retour en affichage large : on réinitialise --------------------- */
|
||||
mediaQuery.addEventListener("change", (event) => {
|
||||
if (!event.matches) close();
|
||||
});
|
||||
|
||||
// État initial explicite plutôt qu'implicite dans le HTML.
|
||||
close();
|
||||
}
|
||||
Reference in New Issue
Block a user