Menu multi-déroulant horizontal

Extrait de code pour un menu déroulant, horizontal et adaptatif avec bouton dit « hamburger ».

Règles d’implémentation pour l’accessibilité

  • L’exemple présenté ici est celui d’un menu avec menu déroulant à double niveau (rubriques parentes, sous-rubriques et sous-sous-rubriques).
  • Le bouton de rubrique parente permettant d’afficher le sous-menu correspondant :
    • porte l’attribut aria aria-controls qui fera référence à l’ìd posé sur la liste contenant le sous-menu ;
    • porte l’attribut aria aria-haspopup="true" pour signifier aux technologies d’assistance qu’un sous-menu est présent ;
    • porte l’attribut aria aria-expanded qui prend la valeur true si le sous-menu est affiché, false si le sous-menu n’est pas affiché.
  • Pour que ce menu fonctionne, du code javascript sera nécessaire. Le script gère :
    • le changement d’état aria-expanded ; l’affichage/disparition du sous-menu est alors géré en CSS ;
    • la disparition du sous-menu quand l’utilisateur appuie sur la touche “Echap” ou clique n’importe où dans la page. De plus, le focus revient sur le bouton à l’origine de l’action.

Visuel du menu proposé

Voir le menu double-niveaux sur CodePen.

Extraits de code

Code HTML

<header role="navigation" id="navRub">
    <nav role="navigation" class="navbar" aria-label="Navigation dans la rubrique Campagne Esane">
      <ul id="menu">
        <li class="accueil">
          <a href="" aria-current="page">Accueil</a>
        </li>
        <li class="item1">
          <button aria-haspopup="true" aria-expanded="false" aria-controls="item1">Partie 1</button>
          <ul id="item1">
            <li><a href="">Aperçu</a></li>
            <li><a href="">Lorem ipsum</a></li>
            <li><a href="">Sic amet</a></li>
            <li><a href="">Ea laboris aliquip</a></li>
            <li><a href="">Minim commodo</a></li>
          </ul>
        </li>
        <li class="item2">
          <button aria-haspopup="true" aria-expanded="false" aria-controls="item2" id="parent2">Partie
            2</button>
          <ul id="item2">
            <li><a href="">Aperçu</a></li>
            <li>
              <button data-enfant="parent2" aria-haspopup="true" aria-expanded="false" aria-controls="item21">Végétaux</button>
              <ul id="item21">
                <li><a href="">Fleurs</a></li>
                <li><a href="">Arbres</a></li>
              </ul>
            </li>
            <li><a href="">Ea laboris aliquip</a></li>
            <li><button data-enfant="parent2" aria-haspopup="true" aria-expanded="false" aria-controls="item22">Animaux</button>
              <ul id="item22">
                <li><a href="">Girafe</a></li>
                <li><a href="">Koala</a></li>
              </ul>
            </li>
          </ul>
        </li>
        <li class="item3">
          <button aria-haspopup="true" aria-expanded="false" aria-controls="item3" id="parent3">Partie
            3</button>
          <ul id="item3">
            <li><a href="">Aperçu</a></li>
            <li><a href="">Duis ad culpa laboris</a></li>
            <li><a href="">Tempor eiusmod</a></li>
            <li>
              <button data-enfant="parent3" aria-haspopup="true" aria-expanded="false" aria-controls="item33">Fruits</button>
              <ul id="item33">
                <li><a href="">Ananas</a></li>
                <li><a href="">Banane</a></li>
                <li><a href="">Clémentine</a></li>
              </ul>
            </li>
          </ul>
        </li>
        <li class="item4"><a href="">Partie unique</a></li>

      </ul>
    </nav>
  </header>

Code CSS

Pour adapter la couleur de la barre de navigation et des liens, modifier les codes couleurs dans la déclaration :root.

:root {
  --FONT-FAMILY: Arial, Helvetica, sans-serif;

  --NAVBAR-height: 3;
  --icon-size: 1.5em;

  --WHITE-color: #fefefe;
  --WHITE-A-color: #ececec;
  --WHITE-B-color: #c9c9c9;
  --BLACK-color: #111111;
  --GRAY-DEEP-color: #1d1d1d;
  --GRAY-DARK-color: #3a3a3a;
  --GRAY-MEDIUM-color: #454545;
  --GRAY-LIGHT-color: #545454;
  --GRAY-SWEET-color: #7c7c7c;
  --GOLD-color: #ffd700;

  --SHADOW-BOX: 0 0 1em gray;
}

body {
  font-family: var(--FONT-FAMILY);
  font-size: 1em;
}
main {
  min-height: 50vh;
}
/** navRub */
#navRub {
  background-color: var(--GRAY-DARK-color);
  height: var(--NAVBAR-height) em;
}

/** Navbar - Barre de navigation horizontale principale et menu déroulant */
.navbar {
  flex: 0 0 100%;
  margin: 0;
  padding: 0;
  color: var(--WHITE-color);
  display: flex;
  justify-content: space-between;
  align-items: stretch;
}

.navbar ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  height: var(--NAVBAR-height) em;
}

.navbar ul button[aria-expanded="false"] + ul {
  display: none;
}

.navbar ul li {
  border-right: 1px solid var(--WHITE-color);
}

.navbar ul li a,
.navbar ul li button {
  margin: 0;
  padding: 0 1.5em;
  font-size: 1em;
  display: block;
  height: var(--NAVBAR-height) em;
  line-height: var(--NAVBAR-height);
  text-align: left;
  color: var(--WHITE-color);
  border-bottom: 0.25em solid transparent;
}

.navbar ul li a {
  padding-top: 1px;
  text-decoration: none;
}

.navbar ul li button {
  padding-bottom: 1px;
  padding-right: calc(var(--icon-size) * 1.5);
  border: 0;
  border-bottom: 0.25em solid transparent;
  background-color: transparent;
  cursor: pointer;
  position: relative;
}

.navbar ul li button::after {
  position: absolute;
  top: 0.75em;
  right: 0.25em;
  content: "";
  background-color: currentColor;
  display: inline-block;
  height: var(--icon-size);
  width: var(--icon-size);
  mask-size: 100% 100%;
  vertical-align: middle;
  -webkit-mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDE2LTYtNmgxMmwtNiA2WiIvPjwvc3ZnPg==);
  mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0ibTEyIDE2LTYtNmgxMmwtNiA2WiIvPjwvc3ZnPg==);
}

.navbar ul li a[aria-current="page"],
.navbar ul li button[aria-current="true"] {
  background: var(--GRAY-LIGHT-color);
  border-bottom-color: var(--GOLD-color);
  color: var(--GOLD-color);
  text-transform: uppercase;
}

.navbar ul li a:hover,
.navbar ul li a:focus,
.navbar ul li button:hover,
.navbar ul li button:focus,
.navbar ul li button[aria-expanded="true"] {
  background: var(--GRAY-LIGHT-color);
  text-decoration: underline;
  outline: var(--WHITE-color);
}

.navbar ul li button[aria-expanded="true"]::after {
  transform: rotate(-180deg);
}

/** style pour les sous-menus */
.navbar ul button[aria-expanded="true"] + ul {
  display: block;
  box-shadow: var(--SHADOW-BOX);
  background-color: var(--WHITE-color);
  position: absolute;
  margin: 1px 0 0 0;
  z-index: 1030;
  padding: 0;
  background-color: var(--WHITE-A-color);
}

.navbar ul button[aria-expanded="true"] + ul li {
  padding: 0;
}

.navbar ul button[aria-expanded="true"] + ul li:not(:last-of-type) {
  border-bottom: 1px dotted var(--GRAY-MEDIUM-color);
}

.navbar ul button[aria-expanded="true"] + ul li a[aria-current="page"],
.navbar ul button[aria-expanded="true"] + ul li a {
  color: var(--GRAY-MEDIUM-color);
  background: transparent;
  padding: 0 1rem;
}

.navbar ul button[aria-expanded="true"] + ul li a:hover,
.navbar ul button[aria-expanded="true"] + ul li a:focus {
  text-decoration: underline;
  background-color: var(--WHITE-B-color);
  color: var(--GRAY-DARK-color);
  outline: 0;
}

.navbar ul button[aria-expanded="true"] + ul li a[aria-current="page"] {
  font-weight: bold;
  color: var(--GRAY-DARK-color);
}

.navbar
  ul
  button[aria-expanded="true"]
  + ul
  li
  a[aria-current="page"]:first-of-type
  a {
  padding-top: 0.75em;
}

/** Sous-sous-menu */
.navbar ul button[aria-expanded="true"] + ul button {
  color: var(--GRAY-MEDIUM-color);
  font-size: 1em;
  padding: 0 1em;
  width: 100%;
}

.navbar ul button[aria-expanded="true"] + ul button::after {
  transform: rotate(-90deg);
}

.navbar ul button[aria-expanded="true"] + ul button:hover,
.navbar ul button[aria-expanded="true"] + ul button:focus,
.navbar ul button[aria-expanded="true"] + ul button[aria-expanded="true"] {
  color: var(--GRAY-DARK-color);
  background-color: var(--WHITE-B-color);
  border-bottom-color: var(--GRAY-SWEET-color);
}

.navbar ul button[aria-expanded="true"] + ul button[aria-expanded="true"] {
  position: relative;
}

.navbar ul button[aria-expanded="true"] + ul button[aria-expanded="true"] + ul {
  position: absolute;
  left: 101%;
  margin-top: -2.5em;
}

Code javascript

Le script ci-dessous sera intégré dans la page HTML via l’écriture suivante :

<script src="/js/script.js"></script>

Fichier js/script.js

function menuDynamique() {
  const theButtons = document.querySelectorAll(".navbar button[aria-expanded]");
  const theButtonsEnfant = document.querySelectorAll(
    ".navbar button[data-enfant]"
  );

  for (i = 0; i < theButtons.length; i++) {
    // apparition/disparition du sous-menu au clic
    theButtons[i].addEventListener("click", function (evt) {
      const thisButton = evt.target;
      const thisButtonEnfant = thisButton.getAttribute(["data-enfant"]);

      if (!thisButtonEnfant) {
        for (j = 0; j < theButtons.length; j++) {
          if (thisButton !== theButtons[j])
            theButtons[j].setAttribute("aria-expanded", "false");
        }
      } else {
        for (j = 0; j < theButtonsEnfant.length; j++) {
          if (thisButtonEnfant !== theButtonsEnfant[j].getAttribute)
            theButtonsEnfant[j].setAttribute("aria-expanded", "false");
        }
      }

      const stateButton =
        thisButton.getAttribute("aria-expanded") === "false" ? true : false;
      thisButton.setAttribute("aria-expanded", stateButton);
    });

    //disparition des sous-menu quand changement de focus sur bouton
    theButtons[i].addEventListener("focus", function (evt) {
      const thisButton = evt.target;
      const thisButtonEnfant = thisButton.getAttribute(["data-enfant"]);

      if (!thisButtonEnfant) {
        for (j = 0; j < theButtons.length; j++) {
          if (thisButton !== theButtons[j])
            theButtons[j].setAttribute("aria-expanded", "false");
        }
      } else {
        for (j = 0; j < theButtonsEnfant.length; j++) {
          if (thisButtonEnfant !== theButtonsEnfant[j].getAttribute)
            theButtonsEnfant[j].setAttribute("aria-expanded", "false");
        }
      }
    });
  }

  //disparition du sous-sous-menu au focus
  const theLinks = document.querySelectorAll(
    "button:not([data-enfant]) ~ ul li a"
  );
  for (i = 0; i < theLinks.length; i++) {
    theLinks[i].addEventListener("focus", function (e) {
      const thisLink = e.target;
      const thisButtonparent =
        thisLink.parentElement.parentElement.previousElementSibling;
      for (j = 0; j < theButtonsEnfant.length; j++) {
        theButtonsEnfant[j].setAttribute("aria-expanded", "false");
      }
      thisButtonparent.setAttribute("aria-expanded", "true");
    });
  }

  // disparition du sous-menu et focus sur le bouton correspondant au sous-menu
  document.addEventListener("keydown", (evt) => {
    let isEchap = false;
    if ("key" in evt) {
      isEchap = evt.key === "Escape" || evt.key === "Esc";
    } else {
      isEchap = evt.keyCode === 27;
    }

    if (isEchap) {
      const thisEvt = evt.target;
      const thisButtonParent =
        thisEvt.parentElement.parentElement.previousElementSibling;
      if (thisButtonParent.getAttribute(["data-enfant"])) {
        thisButtonParent.setAttribute("aria-expanded", "false");
        thisButtonParent.focus();
      } else {
        for (j = 0; j < theButtons.length; j++) {
          if (theButtons[j].getAttribute("aria-expanded") === "true") {
            theButtons[j].setAttribute("aria-expanded", "false");
            theButtons[j].focus();
          }
        }
      }
    }
  });

  // fermeture des tous les sous-menus quand clic dans body

  document.body.addEventListener("click", (evt) => {
    if (!evt.target.matches(".navbar button[aria-expanded]")) {
      for (i = 0; i < theButtons.length; i++) {
        theButtons[i].setAttribute("aria-expanded", "false");
      }
    }
  });

  // fermeture des tous les sous-menus quand focus dans lien ou bouton main et autre élément focusable
  const focusableElements = document.querySelectorAll(
    'main a[href], main button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
  );
  for (i = 0; i < focusableElements.length; i++) {
    focusableElements[i].addEventListener("focus", function () {
      for (j = 0; j < theButtons.length; j++) {
        if (theButtons[j].getAttribute("aria-expanded") === "true") {
          theButtons[j].setAttribute("aria-expanded", "false");
        }
      }
    });
  }
}
menuDynamique();